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
8 changes: 8 additions & 0 deletions .changeset/node-engine-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@proofkit/cli": patch
"create-proofkit": patch
"@proofkit/fmdapi": patch
"@proofkit/fmodata": patch
---

Restrict Node engines to 22, 24, or 26.
5 changes: 5 additions & 0 deletions .changeset/quiet-command-parsing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@proofkit/cli": patch
---

Parse generated package-manager commands with shell-style quoting.
2 changes: 1 addition & 1 deletion .github/workflows/check-skills.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22

- name: Install intent
run: npm install -g @tanstack/intent
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/validate-skills.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22

- name: Install intent CLI
run: npm install -g @tanstack/intent
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
},
"packageManager": "pnpm@11.1.1",
"engines": {
"node": ">=18"
"node": "^22.0.0 || ^24.0.0 || ^26.0.0"
},
"name": "with-changesets",
"lint-staged": {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli-old/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"package.json"
],
"engines": {
"node": "^20.0.0 || ^22.0.0"
"node": "^22.0.0 || ^24.0.0 || ^26.0.0"
},
"scripts": {
"typecheck": "tsc",
Expand Down
25 changes: 18 additions & 7 deletions packages/cli/src/cli/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { execa } from "execa";
import fs from "fs-extra";
import type { PackageJson } from "type-fest";

import { DEFAULT_APP_NAME } from "~/consts.js";
import { DEFAULT_APP_NAME, NODE_RUNTIME_VERSION } from "~/consts.js";
import { createPnpmWorkspaceFileContent } from "~/core/planInit.js";
import { addAuth } from "~/generators/auth.js";
import { runCodegenCommand } from "~/generators/fmdapi.js";
Expand All @@ -20,9 +20,10 @@ import { buildPkgInstallerMap } from "~/installers/index.js";
import { initProgramState, isNonInteractiveMode, state } from "~/state.js";
import { getVersion } from "~/utils/getProofKitVersion.js";
import { getUserPkgManager } from "~/utils/getUserPkgManager.js";
import { logger } from "~/utils/logger.js";
import { parseNameAndPath } from "~/utils/parseNameAndPath.js";
import { type Settings, setSettings } from "~/utils/parseSettings.js";
import { formatPackageManagerCommand } from "~/utils/projectFiles.js";
import { formatPackageManagerCommand, parseCommandString } from "~/utils/projectFiles.js";
import { validateAppName } from "~/utils/validateAppName.js";
import { promptForFileMakerDataSource } from "./add/data-source/filemaker.js";
import { select, text } from "./prompts.js";
Expand Down Expand Up @@ -147,8 +148,6 @@ type ProofKitPackageJSON = PackageJson & {
};
};

const NODE_RUNTIME_VERSION = "^24.11.0";

const missingTypegenCommandPatterns = [
/ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL[\s\S]*Command\s+["'`]typegen["'`]\s+not found/i,
/Command\s+["'`]typegen["'`]\s+not found/i,
Expand Down Expand Up @@ -432,22 +431,34 @@ export const runInit = async (name?: string, opts?: CliFlags) => {
}

if (!noInstall) {
const [fixCommand, ...fixArgs] = formatPackageManagerCommand(pkgManager, "fix").split(" ");
const fixCommandString = formatPackageManagerCommand(pkgManager, "fix");
const [fixCommand, ...fixArgs] = parseCommandString(fixCommandString);
if (!fixCommand) {
throw new Error(`Unable to resolve fix command for ${pkgManager}.`);
}
await execa(fixCommand, fixArgs, {
cwd: projectDir,
stdio: "pipe",
}).catch(() => undefined);
}).catch((error: unknown) => {
if (state.debug) {
logger.warn(`Fix command failed; continuing. packageManager=${pkgManager} command=${fixCommandString}`);
logger.error(error);
}
});

const [lintCommand, ...lintArgs] = formatPackageManagerCommand(pkgManager, "lint").split(" ");
const lintCommandString = formatPackageManagerCommand(pkgManager, "lint");
const [lintCommand, ...lintArgs] = parseCommandString(lintCommandString);
if (!lintCommand) {
throw new Error(`Unable to resolve lint command for ${pkgManager}.`);
}
await execa(lintCommand, lintArgs, {
cwd: projectDir,
stdio: "pipe",
}).catch((error: unknown) => {
logger.warn(`Lint did not succeed; continuing setup. packageManager=${pkgManager} command=${lintCommandString}`);
if (state.debug) {
logger.error(error);
}
});
}

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const distPath = path.dirname(__filename);
export const PKG_ROOT = process.env.PROOFKIT_PKG_ROOT ?? path.join(distPath, "../");

export const DEFAULT_APP_NAME = "my-proofkit-app";
export const NODE_RUNTIME_VERSION = "^22.0.0 || ^24.0.0 || ^26.0.0";
export const cliName = "proofkit";
export const npmName = "@proofkit/cli";
export const DOCS_URL = "https://proofkit.proof.sh";
Expand Down
11 changes: 7 additions & 4 deletions packages/cli/src/core/executeInitPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { getBrowserOxlintConfig, getUltraciteInitCommand } from "~/helpers/ultra
import {
formatPackageManagerCommand,
normalizeImportAlias,
parseCommandString,
replaceTextInFiles,
updateTypegenConfig,
} from "~/utils/projectFiles.js";
Expand Down Expand Up @@ -86,7 +87,7 @@ function renderNextSteps(plan: InitPlan, additionalSteps: string[] = []) {
}

function getPackageScriptCommand(plan: InitPlan, scriptName: string) {
const [command, ...args] = formatPackageManagerCommand(plan.request.packageManager, scriptName).split(" ");
const [command, ...args] = parseCommandString(formatPackageManagerCommand(plan.request.packageManager, scriptName));
if (!command) {
throw new Error(`Unable to resolve ${scriptName} command for ${plan.request.packageManager}.`);
}
Expand Down Expand Up @@ -436,6 +437,8 @@ export const executeInitPlan = (plan: InitPlan) =>
yield* codegenService.runInitial(plan.targetDir, plan.request.packageManager);
}

// plan.tasks.runFix is non-blocking: getPackageScriptCommand/processService.run can fail on fresh scaffolds.
// Effect.either also catches lint failures below and logs warnings; other errors still propagate.
if (plan.tasks.runFix) {
const fixCommand = getPackageScriptCommand(plan, "fix");
yield* Effect.either(
Expand All @@ -448,16 +451,16 @@ export const executeInitPlan = (plan: InitPlan) =>
}

if (plan.tasks.runLint) {
const fixCommand = getPackageScriptCommand(plan, "fix");
const lintCommand = getPackageScriptCommand(plan, "lint");
const result = yield* Effect.either(
processService.run(fixCommand.command, fixCommand.args, {
processService.run(lintCommand.command, lintCommand.args, {
cwd: plan.targetDir,
stdout: "pipe",
stderr: "pipe",
}),
);
if (result._tag === "Left") {
consoleService.warn("Lint fix did not succeed; continuing setup.");
consoleService.warn("Lint did not succeed; continuing setup.");
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/core/planInit.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import path from "node:path";
import type { PackageJson } from "type-fest";

import { NODE_RUNTIME_VERSION } from "~/consts.js";
import type { InitPlan, InitRequest, ProofKitSettings } from "~/core/types.js";
import {
getFmdapiVersion,
Expand All @@ -25,7 +26,6 @@ const SHARED_PNPM_BUILD_POLICY = {
} as const;
const NPM_PACKAGE_MANAGER_WARNING =
"Warning: We strongly suggest using PNPM 11 or greater as your package manager to better protect your computer and your app.";
const NODE_RUNTIME_VERSION = "^24.11.0";
const NPM_MIN_RELEASE_AGE_DAYS = 1;

export function createPnpmWorkspaceFileContent(appType: InitRequest["appType"]) {
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/helpers/intent.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { PackageManager } from "~/utils/packageManager.js";
import { getTemplatePackageExecuteCommand } from "~/utils/projectFiles.js";
import { getTemplatePackageExecuteCommand, parseCommandString } from "~/utils/projectFiles.js";

function splitExecuteCommand(packageManager: PackageManager) {
const [command, ...args] = getTemplatePackageExecuteCommand(packageManager).split(" ");
const [command, ...args] = parseCommandString(getTemplatePackageExecuteCommand(packageManager));
if (!command) {
throw new Error(`Unable to resolve package execute command for ${packageManager}.`);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/helpers/ultracite.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { AppType } from "~/core/types.js";
import type { PackageManager } from "~/utils/packageManager.js";
import { getTemplatePackageExecuteCommand } from "~/utils/projectFiles.js";
import { getTemplatePackageExecuteCommand, parseCommandString } from "~/utils/projectFiles.js";

const ULTRACITE_EDITORS = ["universal", "cursor"] as const;
const ULTRACITE_AGENTS = ["universal", "claude", "codex"] as const;
const ULTRACITE_HOOKS = ["cursor", "windsurf", "codebuddy", "claude"] as const;
const ULTRACITE_INTEGRATIONS = ["husky", "lint-staged"] as const;

function splitExecuteCommand(packageManager: PackageManager) {
const [command, ...args] = getTemplatePackageExecuteCommand(packageManager).split(" ");
const [command, ...args] = parseCommandString(getTemplatePackageExecuteCommand(packageManager));
if (!command) {
throw new Error(`Unable to resolve package execute command for ${packageManager}.`);
}
Expand Down
55 changes: 55 additions & 0 deletions packages/cli/src/utils/projectFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { PackageManager } from "~/utils/packageManager.js";

const commonFileMakerLayoutPrefixes = ["API_", "API ", "dapi_", "dapi"];
const TRAILING_SLASH_REGEX = /[^/]$/;
const WHITESPACE_REGEX = /\s/;
const DEFAULT_FM_MCP_BASE_URL = "http://127.0.0.1:1365";
const textFileExtensions = new Set([
".ts",
Expand Down Expand Up @@ -54,6 +55,60 @@ export function formatPackageManagerCommand(packageManager: PackageManager, comm
return ["npm", "bun"].includes(packageManager) ? `${packageManager} run ${command}` : `${packageManager} ${command}`;
}

export function parseCommandString(command: string): string[] {
const tokens: string[] = [];
let current = "";
let quote: "'" | '"' | undefined;
let escaping = false;

for (const char of command) {
if (escaping) {
current += char;
escaping = false;
continue;
}

if (char === "\\") {
escaping = true;
continue;
}

if (quote) {
if (char === quote) {
quote = undefined;
} else {
current += char;
}
continue;
}

if (char === "'" || char === '"') {
quote = char;
continue;
}

if (WHITESPACE_REGEX.test(char)) {
if (current) {
tokens.push(current);
current = "";
}
continue;
}

current += char;
}

if (escaping) {
current += "\\";
}

if (current) {
tokens.push(current);
}

return tokens;
}

export function getTemplatePackageCommand(packageManager: PackageManager) {
if (packageManager === "npm") {
return "npm run";
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/template/vite-wv/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^22.0.0 || ^24.0.0 || ^26.0.0"
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"scripts": {
"build": "vite build",
"build:upload": "__PNPM_COMMAND__ build && __PNPM_COMMAND__ upload",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/template/vite-wv/src/routes/query-demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const getConnectionHint = (): string =>
export const QueryDemoPage = () => {
const hintQuery = useQuery({
queryFn: getConnectionHint,
queryKey: ["starter-connection-hint"],
queryKey: ["starter-connection-hint"] as const,
});

return (
Expand Down
13 changes: 7 additions & 6 deletions packages/cli/tests/executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe("executeInitPlan command paths", () => {
].join(" "),
"pnpx @tanstack/intent@latest install",
"pnpm fix",
"pnpm fix",
"pnpm lint",
]);
expect(tracker.filemakerBootstraps).toBe(1);
expect(tracker.codegens).toBe(1);
Expand Down Expand Up @@ -125,7 +125,7 @@ describe("executeInitPlan command paths", () => {
expect(npmrcFile).toContain("min-release-age=1");
});

it("warns and continues when final lint fix fails", async () => {
it("warns and continues when final lint fails", async () => {
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-lint-fix-warn-"));
const console = {
error: [] as string[],
Expand Down Expand Up @@ -161,13 +161,13 @@ describe("executeInitPlan command paths", () => {
makeTestLayer({
console,
cwd,
failProcessCommand: "pnpm fix",
failProcessCommand: "pnpm lint",
failures: {
processRun: new ExternalCommandError({
args: ["fix"],
args: ["lint"],
command: "pnpm",
cwd,
message: "fix failed",
message: "lint failed",
}),
},
packageManager: "pnpm",
Expand All @@ -177,7 +177,8 @@ describe("executeInitPlan command paths", () => {
);

expect(tracker.commands).toContain("pnpm fix");
expect(console.warn).toContain("Lint fix did not succeed; continuing setup.");
expect(tracker.commands).toContain("pnpm lint");
expect(console.warn).toContain("Lint did not succeed; continuing setup.");
});

it("supports force overwrite for an existing directory", async () => {
Expand Down
14 changes: 14 additions & 0 deletions packages/cli/tests/init-run-init-regression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ describe("runInit browser post-init typegen regression", () => {

it("writes pnpm build policy before install for pnpm 10", async () => {
mockState.appType = "webviewer";
execaMock.mockResolvedValue({ stdout: "10.0.0" });

await expect(
runInit("demo-webviewer", {
Expand All @@ -214,6 +215,19 @@ describe("runInit browser post-init typegen regression", () => {
expect.stringContaining(' "sharp": false'),
"utf8",
);
const workspaceWriteCallIndex = writeFileSyncMock.mock.calls.findIndex(([filePath]) =>
String(filePath).endsWith("pnpm-workspace.yaml"),
);
const firstPnpmScriptCallIndex = execaMock.mock.calls.findIndex(
([command, args]) => command === "pnpm" && Array.isArray(args) && args[0] !== "-v",
);
expect(workspaceWriteCallIndex).not.toBe(-1);
expect(firstPnpmScriptCallIndex).not.toBe(-1);
const workspaceWriteOrder = writeFileSyncMock.mock.invocationCallOrder[workspaceWriteCallIndex];
const firstPnpmScriptOrder = execaMock.mock.invocationCallOrder[firstPnpmScriptCallIndex];
expect(workspaceWriteOrder).toBeDefined();
expect(firstPnpmScriptOrder).toBeDefined();
expect(workspaceWriteOrder as number).toBeLessThan(firstPnpmScriptOrder as number);
expect(execaMock).toHaveBeenCalledWith("pnpm", ["fix"], {
cwd: "/tmp/proofkit-regression/demo-browser",
stdio: "pipe",
Expand Down
Loading
Loading