Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ninety-pets-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@proofkit/cli": patch
---

Publish prebuilt CLI binaries and cut install-time runtime deps so `npx` and `pnpm dlx` flows avoid dependency build approvals.
5 changes: 5 additions & 0 deletions .github/workflows/continuous-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ jobs:
node-version: 22
cache: "pnpm"

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11

- name: Install dependencies
run: pnpm install --frozen-lockfile

Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ jobs:
node-version: 22
cache: "pnpm"

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.13

Comment on lines +82 to +86
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that Bun setup action refs are pinned to 40-char commit SHAs.
rg -nP --glob '.github/workflows/*.yml' 'uses:\s*oven-sh/setup-bun@(?![0-9a-f]{40}\b).+'

Repository: proofsh/proofkit

Length of output: 209


🏁 Script executed:

# Find workflow files in the repository
fd -e yml -e yaml . .github/workflows/ 2>/dev/null || echo "Directory may not exist, checking alternatives"

# Also check if .github directory exists
ls -la .github/ 2>/dev/null || echo ".github directory not found"

# List all files in .github if it exists
find .github -type f 2>/dev/null | head -20

Repository: proofsh/proofkit

Length of output: 590


🏁 Script executed:

# Check the content of release.yml to see lines around 82-86 and 164-168
wc -l .github/workflows/release.yml

# Read lines 75-95 to see context around line 82-86
echo "=== Lines 75-95 (context for 82-86) ==="
sed -n '75,95p' .github/workflows/release.yml

# Read lines 155-175 to see context around line 164-168
echo ""
echo "=== Lines 155-175 (context for 164-168) ==="
sed -n '155,175p' .github/workflows/release.yml

# Search for all oven-sh/setup-bun references
echo ""
echo "=== All oven-sh/setup-bun references ==="
rg -n 'oven-sh/setup-bun' .github/workflows/release.yml

Repository: proofsh/proofkit

Length of output: 1314


🏁 Script executed:

# Search for any existing commit SHAs in the workflow file that might give us a pattern
rg -n 'uses:.*@[0-9a-f]{40}' .github/workflows/release.yml

# Check if there are any other GitHub actions pinned to commit SHAs in this file
rg -n 'uses:.*@[0-9a-f]{8,40}' .github/workflows/release.yml

# Look at other actions in the file to see the pinning pattern used
rg -n 'uses:' .github/workflows/release.yml | head -20

Repository: proofsh/proofkit

Length of output: 637


🏁 Script executed:

# Let me check if there's a way to find the commit SHA for oven-sh/setup-bun@v2
# First, let's see if there's any documentation or other files that might reference it
rg -i 'oven-sh/setup-bun' . --type md --type txt 2>/dev/null | head -10

# Check if there are any comments or notes about commit SHAs in the repo
rg -i 'commit.*sha|pin.*commit' .github/workflows/ 2>/dev/null | head -10

Repository: proofsh/proofkit

Length of output: 42


Pin oven-sh/setup-bun to a full commit SHA in both jobs.

Using @v2 is mutable and weakens release-pipeline supply-chain guarantees. Pin these references to immutable commit SHAs at lines 83 and 165.

Example change
-        uses: oven-sh/setup-bun@v2
+        uses: oven-sh/setup-bun@<full_commit_sha_for_v2>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 82 - 86, The workflow currently
uses the mutable action reference "uses: oven-sh/setup-bun@v2" in the "Setup
Bun" step; replace that mutable tag with the action's immutable commit SHA
(e.g., oven-sh/setup-bun@<full-commit-sha>) in both occurrences of the "Setup
Bun" step so the release job pins to a specific commit instead of `@v2`.

- name: Install Dependencies
run: pnpm install --frozen-lockfile

Expand Down Expand Up @@ -156,6 +161,11 @@ jobs:
cache: "pnpm"
registry-url: "https://registry.npmjs.org"

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Install Dependencies
run: pnpm install --frozen-lockfile

Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ public/dist
.turbo
packages/fmdapi/test/typegen/*
packages/typegen/schema/metadata.xml
packages/cli/bin/proofkit-*
!packages/cli/bin/proofkit.cjs
packages/cli/.*.bun-build
packages/cli/src/generated/

# FM ADD-ON BUILD ARTIFACTS
fm-addon/stage/
Expand Down
59 changes: 59 additions & 0 deletions packages/cli/bin/proofkit.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env node
"use strict";

const { existsSync } = require("node:fs");
const path = require("node:path");
const { spawnSync } = require("node:child_process");

const BINARIES = {
darwin: {
arm64: "proofkit-darwin-arm64",
x64: "proofkit-darwin-x64",
},
linux: {
arm64: "proofkit-linux-arm64",
x64: "proofkit-linux-x64",
},
win32: {
arm64: "proofkit-windows-arm64.exe",
x64: "proofkit-windows-x64.exe",
},
};

function run(command, args) {
const result = spawnSync(command, args, {
stdio: "inherit",
env: {
...process.env,
PROOFKIT_PKG_ROOT: path.resolve(__dirname, ".."),
},
});

if (result.error) {
throw result.error;
}

if (typeof result.status === "number") {
process.exit(result.status);
}

process.exit(1);
Comment on lines +23 to +40
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve fallback on spawn errors and propagate child signals.

run() currently throws on spawn errors, so a present-but-unexecutable bundled binary prevents reaching the JS fallback. Also, signal-terminated child processes are converted to exit code 1 instead of preserving signal semantics.

Suggested patch
 function run(command, args) {
   const result = spawnSync(command, args, {
     stdio: "inherit",
     env: {
       ...process.env,
       PROOFKIT_PKG_ROOT: path.resolve(__dirname, ".."),
     },
   });
 
   if (result.error) {
     throw result.error;
   }
 
   if (typeof result.status === "number") {
     process.exit(result.status);
   }
+
+  if (result.signal) {
+    process.kill(process.pid, result.signal);
+    return;
+  }
 
   process.exit(1);
 }
 
 if (process.env.PROOFKIT_DISABLE_BUNDLED_BINARY !== "1") {
   const binaryName = BINARIES[process.platform]?.[process.arch];
   if (binaryName) {
     const binaryPath = path.join(__dirname, binaryName);
     if (existsSync(binaryPath)) {
-      run(binaryPath, process.argv.slice(2));
+      try {
+        run(binaryPath, process.argv.slice(2));
+      } catch (error) {
+        if (!["EACCES", "ENOENT", "ENOEXEC"].includes(error?.code)) {
+          throw error;
+        }
+      }
     }
   }
 }

Also applies to: 47-49

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/bin/proofkit.cjs` around lines 23 - 40, The run() wrapper
currently throws on spawn errors and turns signal-terminated children into exit
code 1; instead, when spawnSync returns result.error, do not
throw—return/propagate a failure value so the JS fallback can run (update
callers that expect a boolean or use its return). Also, if result.signal is set,
re-emit that signal against the current process (e.g., process.kill(process.pid,
result.signal)) to preserve signal semantics; otherwise if typeof result.status
=== "number" call process.exit(result.status). Update the logic used around
spawnSync/result.error and the handling of result.signal and result.status in
run() (and the duplicate block at the other occurrence).

}

if (process.env.PROOFKIT_DISABLE_BUNDLED_BINARY !== "1") {
const binaryName = BINARIES[process.platform]?.[process.arch];
if (binaryName) {
const binaryPath = path.join(__dirname, binaryName);
if (existsSync(binaryPath)) {
run(binaryPath, process.argv.slice(2));
}
}
}

const fallbackPath = path.join(__dirname, "..", "dist", "index.js");
if (existsSync(fallbackPath)) {
run(process.execPath, [fallbackPath, ...process.argv.slice(2)]);
}

console.error(`No ProofKit executable found for ${process.platform}-${process.arch}.`);
process.exit(1);
21 changes: 10 additions & 11 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@
}
},
"bin": {
"proofkit": "dist/index.js"
"proofkit": "bin/proofkit.cjs"
},
"files": [
"bin",
"dist",
"template",
"README.md",
Expand All @@ -39,22 +40,23 @@
"node": "^20.0.0 || ^22.0.0"
},
"scripts": {
"typecheck": "tsc",
"build": "NODE_ENV=production tsdown && publint --strict",
"prepublishOnly": "pnpm build",
"typecheck": "node ./scripts/write-cli-version.mjs && tsc",
"build": "node ./scripts/write-cli-version.mjs && NODE_ENV=production tsdown && publint --strict",
"build:binaries": "node ./scripts/write-cli-version.mjs && node ./scripts/build-binaries.mjs",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"prepublishOnly": "pnpm build && pnpm build:binaries",
"dev": "tsdown --watch",
"clean": "rm -rf dist .turbo node_modules",
"clean": "rm -rf dist .turbo node_modules bin/proofkit-* bin/*.exe",
"start": "node dist/index.js",
"lint": "biome check . --write",
"lint:summary": "biome check . --reporter=summary",
"release": "changeset version",
"pub:beta": "NODE_ENV=production pnpm build && npm publish --tag beta --access public",
"pub:next": "NODE_ENV=production pnpm build && npm publish --tag next --access public",
"pub:release": "NODE_ENV=production pnpm build && npm publish --access public",
"test": "pnpm build && vitest run",
"test": "pnpm build && node ./scripts/build-current-binary.mjs && PROOFKIT_DISABLE_BUNDLED_BINARY=1 vitest run",
"test:smoke": "PROOFKIT_RUN_SMOKE_TESTS=1 vitest run --config vitest.smoke.config.ts"
},
"dependencies": {
"devDependencies": {
"@better-fetch/fetch": "1.1.17",
"@clack/core": "^0.3.5",
"@clack/prompts": "^0.11.0",
Expand Down Expand Up @@ -86,11 +88,8 @@
"randomstring": "^1.3.1",
"semver": "^7.7.3",
"shadcn": "^2.10.0",
"sort-package-json": "^2.15.1",
"ts-morph": "^26.0.0",
"type-fest": "^3.13.1"
},
"devDependencies": {
"type-fest": "^3.13.1",
"@auth/drizzle-adapter": "^1.11.1",
"@auth/prisma-adapter": "^1.6.0",
"@biomejs/biome": "2.3.11",
Expand Down
96 changes: 96 additions & 0 deletions packages/cli/scripts/build-binaries.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { spawnSync } from "node:child_process";
import { chmodSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packageRoot = path.resolve(__dirname, "..");
const binDir = path.join(packageRoot, "bin");
const entrypoint = path.join(packageRoot, "src", "index.ts");

const targets = [
{ target: "bun-darwin-arm64", file: "proofkit-darwin-arm64" },
{ target: "bun-darwin-x64", file: "proofkit-darwin-x64" },
{ target: "bun-linux-arm64", file: "proofkit-linux-arm64" },
{ target: "bun-linux-x64", file: "proofkit-linux-x64" },
{ target: "bun-windows-arm64", file: "proofkit-windows-arm64.exe" },
{ target: "bun-windows-x64", file: "proofkit-windows-x64.exe" },
];
const validTargets = new Set(targets.map((config) => config.target));
const requestedTargetsEnv = process.env.PROOFKIT_BINARY_TARGETS ?? "";

const selectedTargets = new Set(
requestedTargetsEnv
.split(",")
.map((target) => target.trim())
.filter(Boolean),
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const filteredSelectedTargets = new Set([...selectedTargets].filter((target) => validTargets.has(target)));

if (selectedTargets.size > 0 && filteredSelectedTargets.size === 0) {
console.error(
`No valid binary targets in PROOFKIT_BINARY_TARGETS="${requestedTargetsEnv}". Valid targets: ${targets
.map((config) => config.target)
.join(", ")}`,
);
process.exit(1);
}

mkdirSync(binDir, { recursive: true });
for (const file of readdirSync(binDir)) {
if (file === "proofkit.cjs") {
continue;
}
rmSync(path.join(binDir, file), { recursive: true, force: true });
}

let builtCount = 0;
for (const config of targets) {
if (filteredSelectedTargets.size > 0 && !filteredSelectedTargets.has(config.target)) {
continue;
}

const outfile = path.join(binDir, config.file);
const result = spawnSync(
"bun",
[
"build",
"--compile",
`--target=${config.target}`,
"--no-compile-autoload-dotenv",
"--no-compile-autoload-bunfig",
"--no-compile-autoload-tsconfig",
"--no-compile-autoload-package-json",
entrypoint,
`--outfile=${outfile}`,
],
{
cwd: packageRoot,
stdio: "inherit",
env: process.env,
},
);

if (result.error) {
throw result.error;
}

if (result.status !== 0) {
process.exit(result.status ?? 1);
}

if (existsSync(outfile) && !outfile.endsWith(".exe")) {
chmodSync(outfile, 0o755);
}

builtCount += 1;
}

if (builtCount === 0) {
console.error(
`No binary targets selected from PROOFKIT_BINARY_TARGETS="${requestedTargetsEnv}". Valid targets: ${targets
.map((config) => config.target)
.join(", ")}`,
);
process.exit(1);
}
22 changes: 22 additions & 0 deletions packages/cli/scripts/build-current-binary.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { spawnSync } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packageRoot = path.resolve(__dirname, "..");
const target = `bun-${process.platform}-${process.arch}`;

const result = spawnSync("node", ["./scripts/build-binaries.mjs"], {
cwd: packageRoot,
stdio: "inherit",
env: {
...process.env,
PROOFKIT_BINARY_TARGETS: target,
},
});

if (result.error) {
throw result.error;
}

process.exit(result.status ?? 1);
27 changes: 27 additions & 0 deletions packages/cli/scripts/write-cli-version.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packageRoot = path.resolve(__dirname, "..");
const readVersion = (packagePath) => {
const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
return packageJson.version ?? "0.0.0-private";
};

const outputPath = path.join(packageRoot, "src", "generated", "package-versions.ts");
const content = [
`export const CLI_VERSION = ${JSON.stringify(readVersion(path.join(packageRoot, "package.json")))} as const;`,
`export const FMDAPI_VERSION = ${JSON.stringify(readVersion(path.join(packageRoot, "..", "fmdapi", "package.json")))} as const;`,
`export const BETTER_AUTH_VERSION = ${JSON.stringify(
readVersion(path.join(packageRoot, "..", "better-auth", "package.json")),
)} as const;`,
`export const WEBVIEWER_VERSION = ${JSON.stringify(
readVersion(path.join(packageRoot, "..", "webviewer", "package.json")),
)} as const;`,
`export const TYPEGEN_VERSION = ${JSON.stringify(readVersion(path.join(packageRoot, "..", "typegen", "package.json")))} as const;`,
"",
].join("\n");

mkdirSync(path.dirname(outputPath), { recursive: true });
writeFileSync(outputPath, content, "utf8");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
2 changes: 1 addition & 1 deletion packages/cli/src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const distPath = path.dirname(__filename);
export const PKG_ROOT = path.join(distPath, "../");
export const PKG_ROOT = process.env.PROOFKIT_PKG_ROOT ?? path.join(distPath, "../");

export const DEFAULT_APP_NAME = "my-proofkit-app";
export const cliName = "proofkit";
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/core/doctor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from "node:path";
import { parse as parseDotenv } from "dotenv";
import { Effect } from "effect";
import { parse as parseJsonc } from "jsonc-parser";
import { parse as parseJsonc } from "jsonc-parser/lib/esm/main.js";
import { DOCS_URL } from "~/consts.js";
import { CliContext, ConsoleService, FileSystemService } from "~/core/context.js";

Expand Down
10 changes: 9 additions & 1 deletion packages/cli/src/core/executeInitPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import path from "node:path";
import { Chalk } from "chalk";
import { Cause, Effect, Exit } from "effect";
import { getOrUndefined } from "effect/Option";
import sortPackageJson from "sort-package-json";

import { AGENT_INSTRUCTIONS } from "~/consts.js";
import {
Expand All @@ -21,6 +20,7 @@ import { DirectoryConflictError, FileSystemError, isCliError, UserCancelledError
import { applyPackageJsonMutations } from "~/core/planInit.js";
import type { InitPlan } from "~/core/types.js";
import { normalizeImportAlias, replaceTextInFiles, updateTypegenConfig } from "~/utils/projectFiles.js";
import { sortPackageJson } from "~/utils/sortPackageJson.js";

const AGENT_METADATA_DIRS = new Set([".agents", ".claude", ".clawed", ".clinerules", ".cursor", ".windsurf"]);
const IMPORT_ALIAS_WILDCARD_REGEX = /\*/g;
Expand Down Expand Up @@ -358,6 +358,14 @@ export const executeInitPlan = (plan: InitPlan) =>
}

if (plan.tasks.runInstall) {
if (plan.request.packageManager === "pnpm") {
yield* processService.run("pnpm", ["self-update", "11"], {
cwd: plan.targetDir,
stdout: "pipe",
stderr: "pipe",
});
}

let installArgs: string[] = ["install"];
if (plan.request.packageManager === "yarn") {
installArgs = [];
Expand Down
Loading
Loading