@delegance/claude-autopilot 7.3.0 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli/help-text.js +1 -1
- package/dist/src/cli/index.js +19 -2
- package/dist/src/cli/scaffold/node.d.ts +20 -0
- package/dist/src/cli/scaffold/node.js +162 -0
- package/dist/src/cli/scaffold/python.d.ts +71 -0
- package/dist/src/cli/scaffold/python.js +338 -0
- package/dist/src/cli/scaffold/types.d.ts +68 -0
- package/dist/src/cli/scaffold/types.js +6 -0
- package/dist/src/cli/scaffold.d.ts +43 -29
- package/dist/src/cli/scaffold.js +245 -174
- package/package.json +1 -1
|
@@ -28,7 +28,7 @@ export const HELP_GROUPS = [
|
|
|
28
28
|
verbs: [
|
|
29
29
|
{ verb: 'init', summary: 'Scaffold guardrail.config.yaml + auto-detect migrate stack (writes .autopilot/stack.md)' },
|
|
30
30
|
{ verb: 'setup', summary: 'Auto-detect stack, write config, install pre-push hook' },
|
|
31
|
-
{ verb: 'scaffold', summary: 'Scaffold project skeleton from a spec markdown (--from-spec <path>)' },
|
|
31
|
+
{ verb: 'scaffold', summary: 'Scaffold project skeleton from a spec markdown (--from-spec <path> [--stack node|python|fastapi])' },
|
|
32
32
|
{ verb: 'autopilot', summary: 'Multi-phase orchestrator — run scan → spec → plan → implement under one runId (v6.2.0)' },
|
|
33
33
|
{ verb: 'brainstorm', summary: 'Pipeline entry point (Claude Code skill — see /brainstorm)' },
|
|
34
34
|
{ verb: 'spec', summary: 'Spec-writing pointer (Claude Code skill — see /brainstorm)' },
|
package/dist/src/cli/index.js
CHANGED
|
@@ -197,7 +197,7 @@ These are aliases for the flat subcommands; they still work without the 'advance
|
|
|
197
197
|
// `run resume` form is handled BEFORE the default `run` -> review dispatch
|
|
198
198
|
// kicks in (see disambiguation block just below).
|
|
199
199
|
const SUBCOMMANDS = ['init', 'run', 'runs', 'scan', 'report', 'explain', 'ignore', 'ci', 'pr', 'fix', 'costs', 'watch', 'hook', 'autoregress', 'baseline', 'triage', 'lsp', 'worker', 'mcp', 'test-gen', 'pr-desc', 'doctor', 'preflight', 'setup', 'council', 'migrate-v4', 'migrate', 'migrate-doctor', 'deploy', 'brainstorm', 'spec', 'plan', 'implement', 'review', 'validate', 'autopilot', 'internal', 'help', '--help', '-h'];
|
|
200
|
-
const VALUE_FLAGS = ['base', 'config', 'files', 'format', 'output', 'debounce', 'ask', 'focus', 'fail-on', 'note', 'reason', 'expires', 'profile', 'severity', 'prompt', 'context-file', 'path', 'adapter', 'ref', 'sha', 'spec', 'context', 'mode', 'phases', 'budget'];
|
|
200
|
+
const VALUE_FLAGS = ['base', 'config', 'files', 'format', 'output', 'debounce', 'ask', 'focus', 'fail-on', 'note', 'reason', 'expires', 'profile', 'severity', 'prompt', 'context-file', 'path', 'adapter', 'ref', 'sha', 'spec', 'context', 'mode', 'phases', 'budget', 'stack'];
|
|
201
201
|
// Bare invocation — no subcommand, no flags → show welcome guide
|
|
202
202
|
if (args.length === 0) {
|
|
203
203
|
const hasKey = !!(process.env.ANTHROPIC_API_KEY || process.env.GEMINI_API_KEY ||
|
|
@@ -933,14 +933,31 @@ switch (subcommand) {
|
|
|
933
933
|
}
|
|
934
934
|
case 'scaffold': {
|
|
935
935
|
// v7.2.0 — `claude-autopilot scaffold --from-spec <path>`
|
|
936
|
+
// v7.4.0 — `--stack <node|python|fastapi>` + `--list-stacks`.
|
|
937
|
+
if (boolFlag('list-stacks')) {
|
|
938
|
+
const { printStackList } = await import("./scaffold.js");
|
|
939
|
+
printStackList();
|
|
940
|
+
process.exit(0);
|
|
941
|
+
}
|
|
936
942
|
const fromSpec = flag('from-spec');
|
|
937
943
|
const dryRun = boolFlag('dry-run');
|
|
944
|
+
const stackArg = flag('stack');
|
|
945
|
+
if (stackArg && !['node', 'python', 'fastapi'].includes(stackArg)) {
|
|
946
|
+
console.error(`\x1b[31m[claude-autopilot] --stack "${stackArg}" not recognized — supported: node, python, fastapi\x1b[0m`);
|
|
947
|
+
console.error(` See: claude-autopilot scaffold --list-stacks`);
|
|
948
|
+
process.exit(3);
|
|
949
|
+
}
|
|
938
950
|
if (!fromSpec) {
|
|
939
951
|
console.error(`\x1b[31m[claude-autopilot] scaffold requires --from-spec <path>\x1b[0m`);
|
|
940
952
|
console.error(` Example: claude-autopilot scaffold --from-spec docs/specs/foo.md`);
|
|
953
|
+
console.error(` Stacks: claude-autopilot scaffold --list-stacks`);
|
|
941
954
|
process.exit(1);
|
|
942
955
|
}
|
|
943
|
-
await runScaffold({
|
|
956
|
+
await runScaffold({
|
|
957
|
+
specPath: fromSpec,
|
|
958
|
+
dryRun,
|
|
959
|
+
...(stackArg ? { stack: stackArg } : {}),
|
|
960
|
+
});
|
|
944
961
|
process.exit(0);
|
|
945
962
|
break;
|
|
946
963
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ParsedFiles, ScaffoldResult, ScaffoldRunContext } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Build a minimal starter package.json. Caller passes in any explicit
|
|
4
|
+
* hints (parsed from spec); we layer Node 22 ESM defaults on top.
|
|
5
|
+
*
|
|
6
|
+
* Note: signature unchanged from v7.2.0 — re-exported through ../scaffold.ts
|
|
7
|
+
* so consumers that imported from the public scaffold module keep working.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildStarterPackageJson(projectName: string, hints: ParsedFiles['packageHints']): Record<string, unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* Node ESM scaffolder. Materializes directories + placeholder files,
|
|
12
|
+
* writes package.json (when listed in spec) and tsconfig.json (when listed),
|
|
13
|
+
* choosing JS vs TS tsconfig flavor based on which extension dominates the
|
|
14
|
+
* other listed paths.
|
|
15
|
+
*
|
|
16
|
+
* Behavior is intentionally byte-identical to v7.2.0 — the existing
|
|
17
|
+
* tests/scaffold.test.ts is the regression bar.
|
|
18
|
+
*/
|
|
19
|
+
export declare function scaffoldNode(ctx: ScaffoldRunContext): Promise<ScaffoldResult>;
|
|
20
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// v7.4.0 — Node ESM scaffolder, extracted from src/cli/scaffold.ts (was the
|
|
2
|
+
// monolithic v7.2.0 implementation). Pure module split: the buildStarterPackageJson
|
|
3
|
+
// + scaffoldNode functions here are byte-identical-in-behavior to v7.2.0; the
|
|
4
|
+
// existing 11 scaffold tests are the regression bar.
|
|
5
|
+
//
|
|
6
|
+
// Why split: v7.4.0 adds Python + FastAPI per-stack scaffolders (see ./python.ts).
|
|
7
|
+
// Keeping each stack in its own module makes the v7.5+ plan (Go, Rust, Ruby) a
|
|
8
|
+
// drop-in pattern: add a new file, register it in the dispatcher inside
|
|
9
|
+
// ../scaffold.ts.
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as fsAsync from 'node:fs/promises';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
const PASS = '\x1b[32m✓\x1b[0m';
|
|
14
|
+
const SKIP = '\x1b[2m·\x1b[0m';
|
|
15
|
+
const DIM = (t) => `\x1b[2m${t}\x1b[0m`;
|
|
16
|
+
/**
|
|
17
|
+
* Build a minimal starter package.json. Caller passes in any explicit
|
|
18
|
+
* hints (parsed from spec); we layer Node 22 ESM defaults on top.
|
|
19
|
+
*
|
|
20
|
+
* Note: signature unchanged from v7.2.0 — re-exported through ../scaffold.ts
|
|
21
|
+
* so consumers that imported from the public scaffold module keep working.
|
|
22
|
+
*/
|
|
23
|
+
export function buildStarterPackageJson(projectName, hints) {
|
|
24
|
+
const pkg = {
|
|
25
|
+
name: projectName,
|
|
26
|
+
version: '0.1.0',
|
|
27
|
+
private: true,
|
|
28
|
+
type: hints.type ?? 'module',
|
|
29
|
+
engines: { node: '>=22' },
|
|
30
|
+
scripts: {
|
|
31
|
+
test: 'node --test tests/*.test.js',
|
|
32
|
+
...hints.scripts,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
if (hints.bin)
|
|
36
|
+
pkg.bin = hints.bin;
|
|
37
|
+
if (hints.dependencies)
|
|
38
|
+
pkg.dependencies = hints.dependencies;
|
|
39
|
+
if (hints.devDependencies)
|
|
40
|
+
pkg.devDependencies = hints.devDependencies;
|
|
41
|
+
return pkg;
|
|
42
|
+
}
|
|
43
|
+
const STARTER_TSCONFIG_JS = {
|
|
44
|
+
compilerOptions: {
|
|
45
|
+
target: 'ES2022',
|
|
46
|
+
module: 'NodeNext',
|
|
47
|
+
moduleResolution: 'NodeNext',
|
|
48
|
+
allowJs: true,
|
|
49
|
+
checkJs: true,
|
|
50
|
+
noEmit: true,
|
|
51
|
+
strict: true,
|
|
52
|
+
esModuleInterop: true,
|
|
53
|
+
skipLibCheck: true,
|
|
54
|
+
types: ['node'],
|
|
55
|
+
},
|
|
56
|
+
include: ['bin/**/*', 'src/**/*', 'tests/**/*'],
|
|
57
|
+
};
|
|
58
|
+
const STARTER_TSCONFIG_TS = {
|
|
59
|
+
compilerOptions: {
|
|
60
|
+
target: 'ES2022',
|
|
61
|
+
module: 'NodeNext',
|
|
62
|
+
moduleResolution: 'NodeNext',
|
|
63
|
+
outDir: 'dist',
|
|
64
|
+
strict: true,
|
|
65
|
+
esModuleInterop: true,
|
|
66
|
+
skipLibCheck: true,
|
|
67
|
+
types: ['node'],
|
|
68
|
+
},
|
|
69
|
+
include: ['bin/**/*', 'src/**/*', 'tests/**/*'],
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Node ESM scaffolder. Materializes directories + placeholder files,
|
|
73
|
+
* writes package.json (when listed in spec) and tsconfig.json (when listed),
|
|
74
|
+
* choosing JS vs TS tsconfig flavor based on which extension dominates the
|
|
75
|
+
* other listed paths.
|
|
76
|
+
*
|
|
77
|
+
* Behavior is intentionally byte-identical to v7.2.0 — the existing
|
|
78
|
+
* tests/scaffold.test.ts is the regression bar.
|
|
79
|
+
*/
|
|
80
|
+
export async function scaffoldNode(ctx) {
|
|
81
|
+
const { cwd, parsed, dryRun } = ctx;
|
|
82
|
+
const projectName = path.basename(cwd);
|
|
83
|
+
const filesCreated = [];
|
|
84
|
+
const filesSkippedExisting = [];
|
|
85
|
+
const dirsCreated = [];
|
|
86
|
+
let packageJsonAction = 'skipped-exists';
|
|
87
|
+
let tsconfigAction = 'skipped-no-ts';
|
|
88
|
+
// 1) Create directories first.
|
|
89
|
+
const dirs = new Set();
|
|
90
|
+
for (const p of parsed.paths) {
|
|
91
|
+
const d = path.dirname(p);
|
|
92
|
+
if (d && d !== '.')
|
|
93
|
+
dirs.add(d);
|
|
94
|
+
}
|
|
95
|
+
for (const d of dirs) {
|
|
96
|
+
const abs = path.join(cwd, d);
|
|
97
|
+
if (fs.existsSync(abs))
|
|
98
|
+
continue;
|
|
99
|
+
if (!dryRun)
|
|
100
|
+
await fsAsync.mkdir(abs, { recursive: true });
|
|
101
|
+
dirsCreated.push(d);
|
|
102
|
+
console.log(` ${PASS} mkdir ${DIM(d + '/')}`);
|
|
103
|
+
}
|
|
104
|
+
// 2) Create placeholder files (skip ones we'll handle specially).
|
|
105
|
+
const SPECIAL = new Set(['package.json', 'tsconfig.json']);
|
|
106
|
+
for (const p of parsed.paths) {
|
|
107
|
+
if (SPECIAL.has(p))
|
|
108
|
+
continue;
|
|
109
|
+
const abs = path.join(cwd, p);
|
|
110
|
+
if (fs.existsSync(abs)) {
|
|
111
|
+
filesSkippedExisting.push(p);
|
|
112
|
+
console.log(` ${SKIP} exists ${DIM(p)}`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!dryRun) {
|
|
116
|
+
await fsAsync.mkdir(path.dirname(abs), { recursive: true });
|
|
117
|
+
// Touch — empty file. Real content is the agent's job.
|
|
118
|
+
await fsAsync.writeFile(abs, '', 'utf8');
|
|
119
|
+
}
|
|
120
|
+
filesCreated.push(p);
|
|
121
|
+
console.log(` ${PASS} touch ${DIM(p)}`);
|
|
122
|
+
}
|
|
123
|
+
// 3) package.json — only if the spec lists it.
|
|
124
|
+
if (parsed.paths.includes('package.json')) {
|
|
125
|
+
const pkgAbs = path.join(cwd, 'package.json');
|
|
126
|
+
if (fs.existsSync(pkgAbs)) {
|
|
127
|
+
packageJsonAction = 'skipped-exists';
|
|
128
|
+
console.log(` ${SKIP} exists ${DIM('package.json (preserved)')}`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
const pkg = buildStarterPackageJson(projectName, parsed.packageHints);
|
|
132
|
+
if (!dryRun) {
|
|
133
|
+
await fsAsync.writeFile(pkgAbs, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
134
|
+
}
|
|
135
|
+
packageJsonAction = 'created';
|
|
136
|
+
console.log(` ${PASS} write ${DIM('package.json (Node 22 ESM starter)')}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// 4) tsconfig.json — only if the spec lists it. JS-flavor when the
|
|
140
|
+
// other paths are predominantly .js, TS-flavor for .ts.
|
|
141
|
+
if (parsed.paths.includes('tsconfig.json')) {
|
|
142
|
+
const tsAbs = path.join(cwd, 'tsconfig.json');
|
|
143
|
+
if (fs.existsSync(tsAbs)) {
|
|
144
|
+
tsconfigAction = 'skipped-exists';
|
|
145
|
+
console.log(` ${SKIP} exists ${DIM('tsconfig.json (preserved)')}`);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
const otherPaths = parsed.paths.filter((p) => !SPECIAL.has(p));
|
|
149
|
+
const tsCount = otherPaths.filter((p) => /\.tsx?$/.test(p)).length;
|
|
150
|
+
const jsCount = otherPaths.filter((p) => /\.jsx?$/.test(p)).length;
|
|
151
|
+
const config = tsCount > jsCount ? STARTER_TSCONFIG_TS : STARTER_TSCONFIG_JS;
|
|
152
|
+
tsconfigAction = 'created';
|
|
153
|
+
if (!dryRun) {
|
|
154
|
+
await fsAsync.writeFile(tsAbs, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
155
|
+
}
|
|
156
|
+
const flavor = config === STARTER_TSCONFIG_TS ? 'compiled TS to dist/' : 'JS w/ JSDoc + checkJs';
|
|
157
|
+
console.log(` ${PASS} write ${DIM(`tsconfig.json (${flavor})`)}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { filesCreated, dirsCreated, filesSkippedExisting, packageJsonAction, tsconfigAction };
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=node.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ScaffoldResult, ScaffoldRunContext } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* PEP 503 distribution-name normalization, restricted to what we need
|
|
4
|
+
* here. Lowercase, runs of `[._-]+` collapse to a single `-`, leading +
|
|
5
|
+
* trailing `[._-]` stripped. Empty result falls back to 'app' (so the
|
|
6
|
+
* worst-case `cwd` of `___` still produces a buildable pyproject).
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeDistributionName(raw: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Convert a (PEP 503 normalized) distribution name into a valid Python
|
|
11
|
+
* identifier suitable for a top-level package directory:
|
|
12
|
+
* - replace `-` and `.` with `_`
|
|
13
|
+
* - prefix `_` if it starts with a digit (so `2cool` -> `_2cool`)
|
|
14
|
+
*
|
|
15
|
+
* Tests pin both transformations:
|
|
16
|
+
* my-pkg-2 -> my_pkg_2
|
|
17
|
+
* 2cool -> _2cool
|
|
18
|
+
*/
|
|
19
|
+
export declare function packageNameFromDistribution(distribution: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Parse a dependency string (`fastapi`, `fastapi>=0.110`,
|
|
22
|
+
* `uvicorn[standard]`, `pyramid==2.0`) into its PEP 503 normalized
|
|
23
|
+
* "name" portion — used purely as a dedup key. We don't care about the
|
|
24
|
+
* version specifier when keying; first-occurrence wins (per spec
|
|
25
|
+
* "Dedupe by PEP 503 normalized name; first occurrence wins").
|
|
26
|
+
*/
|
|
27
|
+
export declare function dependencyNameKey(dep: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Build the dependency list for the generated pyproject.toml. Honors
|
|
30
|
+
* the narrow contract from spec ("Dependency hint extraction (codex
|
|
31
|
+
* WARNING #6)"): values flow through verbatim, no version inference,
|
|
32
|
+
* deduped by PEP 503 normalized name. For FastAPI we ALSO seed
|
|
33
|
+
* `fastapi>=0.110` and `uvicorn[standard]>=0.27` if not already present.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildPythonDependencies(hintDeps: string[] | undefined, isFastapi: boolean): string[];
|
|
36
|
+
/**
|
|
37
|
+
* Generate the pyproject.toml body. Caller supplies the resolved
|
|
38
|
+
* distribution name, package name, dependency list, and FastAPI flag
|
|
39
|
+
* (only difference: FastAPI adds a `[project.scripts]` block).
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildPyproject(opts: {
|
|
42
|
+
distributionName: string;
|
|
43
|
+
packageName: string;
|
|
44
|
+
dependencies: string[];
|
|
45
|
+
isFastapi: boolean;
|
|
46
|
+
}): string;
|
|
47
|
+
/** FastAPI entrypoint — codex CRITICAL #2: must be runnable, not a stub. */
|
|
48
|
+
export declare function buildFastapiMain(packageName: string): string;
|
|
49
|
+
/** Smoke test for the FastAPI scaffold — also auto-included so pytest config isn't dead. */
|
|
50
|
+
export declare function buildFastapiTest(packageName: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* The Python scaffolder. Materializes:
|
|
53
|
+
* - src/<package_name>/__init__.py (empty)
|
|
54
|
+
* - tests/ directory
|
|
55
|
+
* - pyproject.toml (PEP 621 + hatchling)
|
|
56
|
+
* - README.md (only if missing)
|
|
57
|
+
*
|
|
58
|
+
* For FastAPI specs it also writes:
|
|
59
|
+
* - src/<package_name>/main.py (runnable FastAPI app)
|
|
60
|
+
* - tests/test_main.py (smoke test)
|
|
61
|
+
*
|
|
62
|
+
* Files explicitly listed in the spec's `## Files` get touched as empty
|
|
63
|
+
* placeholders if they don't already exist (matches the v7.2.0 Node
|
|
64
|
+
* behavior). The special files above are written with content even when
|
|
65
|
+
* not listed in `## Files` — without them the generated pyproject.toml
|
|
66
|
+
* is invalid (missing package dir) or has dead config (no tests).
|
|
67
|
+
*/
|
|
68
|
+
export declare function scaffoldPython(ctx: ScaffoldRunContext, opts: {
|
|
69
|
+
isFastapi: boolean;
|
|
70
|
+
}): Promise<ScaffoldResult>;
|
|
71
|
+
//# sourceMappingURL=python.d.ts.map
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// v7.4.0 — Python + FastAPI scaffolder.
|
|
2
|
+
//
|
|
3
|
+
// Two scaffold flavors share most of this module:
|
|
4
|
+
// - bare Python (pyproject.toml / requirements.txt detected)
|
|
5
|
+
// - FastAPI (Python + a main.py + a `fastapi` mention in the spec)
|
|
6
|
+
//
|
|
7
|
+
// FastAPI auto-includes `fastapi>=0.110` and `uvicorn[standard]>=0.27` in
|
|
8
|
+
// dependencies (deduped by PEP 503 normalized name) and emits a runnable
|
|
9
|
+
// `src/<package>/main.py` + `tests/test_main.py` so the generated
|
|
10
|
+
// `[project.scripts]` entrypoint actually resolves on `pip install -e .`
|
|
11
|
+
// — not a dangling stub. This was codex CRITICAL #2 on the v7.4.0 spec.
|
|
12
|
+
//
|
|
13
|
+
// All naming follows the deterministic algorithm described in the spec
|
|
14
|
+
// ("Name normalization (codex WARNING #1)"):
|
|
15
|
+
// - distribution_name = PEP 503 normalize(basename(cwd))
|
|
16
|
+
// - package_name = distribution_name with `-`/`.` -> `_`,
|
|
17
|
+
// prefix `_` if it would start with a digit.
|
|
18
|
+
import * as fs from 'node:fs';
|
|
19
|
+
import * as fsAsync from 'node:fs/promises';
|
|
20
|
+
import * as path from 'node:path';
|
|
21
|
+
const PASS = '\x1b[32m✓\x1b[0m';
|
|
22
|
+
const SKIP = '\x1b[2m·\x1b[0m';
|
|
23
|
+
const DIM = (t) => `\x1b[2m${t}\x1b[0m`;
|
|
24
|
+
/**
|
|
25
|
+
* PEP 503 distribution-name normalization, restricted to what we need
|
|
26
|
+
* here. Lowercase, runs of `[._-]+` collapse to a single `-`, leading +
|
|
27
|
+
* trailing `[._-]` stripped. Empty result falls back to 'app' (so the
|
|
28
|
+
* worst-case `cwd` of `___` still produces a buildable pyproject).
|
|
29
|
+
*/
|
|
30
|
+
export function normalizeDistributionName(raw) {
|
|
31
|
+
const lower = raw.toLowerCase();
|
|
32
|
+
const collapsed = lower.replace(/[._-]+/g, '-');
|
|
33
|
+
const stripped = collapsed.replace(/^[-]+/, '').replace(/[-]+$/, '');
|
|
34
|
+
return stripped.length > 0 ? stripped : 'app';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Convert a (PEP 503 normalized) distribution name into a valid Python
|
|
38
|
+
* identifier suitable for a top-level package directory:
|
|
39
|
+
* - replace `-` and `.` with `_`
|
|
40
|
+
* - prefix `_` if it starts with a digit (so `2cool` -> `_2cool`)
|
|
41
|
+
*
|
|
42
|
+
* Tests pin both transformations:
|
|
43
|
+
* my-pkg-2 -> my_pkg_2
|
|
44
|
+
* 2cool -> _2cool
|
|
45
|
+
*/
|
|
46
|
+
export function packageNameFromDistribution(distribution) {
|
|
47
|
+
const replaced = distribution.replace(/[-.]/g, '_');
|
|
48
|
+
return /^[0-9]/.test(replaced) ? `_${replaced}` : replaced;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Parse a dependency string (`fastapi`, `fastapi>=0.110`,
|
|
52
|
+
* `uvicorn[standard]`, `pyramid==2.0`) into its PEP 503 normalized
|
|
53
|
+
* "name" portion — used purely as a dedup key. We don't care about the
|
|
54
|
+
* version specifier when keying; first-occurrence wins (per spec
|
|
55
|
+
* "Dedupe by PEP 503 normalized name; first occurrence wins").
|
|
56
|
+
*/
|
|
57
|
+
export function dependencyNameKey(dep) {
|
|
58
|
+
// Strip extras + version specifier. Match the leading
|
|
59
|
+
// identifier (PEP 508 names are `[A-Za-z0-9._-]+`).
|
|
60
|
+
const m = /^[A-Za-z0-9._-]+/.exec(dep.trim());
|
|
61
|
+
if (!m)
|
|
62
|
+
return dep.trim().toLowerCase();
|
|
63
|
+
return normalizeDistributionName(m[0]);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build the dependency list for the generated pyproject.toml. Honors
|
|
67
|
+
* the narrow contract from spec ("Dependency hint extraction (codex
|
|
68
|
+
* WARNING #6)"): values flow through verbatim, no version inference,
|
|
69
|
+
* deduped by PEP 503 normalized name. For FastAPI we ALSO seed
|
|
70
|
+
* `fastapi>=0.110` and `uvicorn[standard]>=0.27` if not already present.
|
|
71
|
+
*/
|
|
72
|
+
export function buildPythonDependencies(hintDeps, isFastapi) {
|
|
73
|
+
const out = [];
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
const push = (raw) => {
|
|
76
|
+
const key = dependencyNameKey(raw);
|
|
77
|
+
if (seen.has(key))
|
|
78
|
+
return;
|
|
79
|
+
seen.add(key);
|
|
80
|
+
out.push(raw);
|
|
81
|
+
};
|
|
82
|
+
for (const d of hintDeps ?? [])
|
|
83
|
+
push(d);
|
|
84
|
+
if (isFastapi) {
|
|
85
|
+
// Only auto-add when not already supplied. If the spec listed
|
|
86
|
+
// `fastapi==0.115`, we keep that pin and don't override it with our
|
|
87
|
+
// default lower bound.
|
|
88
|
+
push('fastapi>=0.110');
|
|
89
|
+
push('uvicorn[standard]>=0.27');
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Format a TOML string array, one entry per line. Used for the
|
|
95
|
+
* `dependencies = [...]` block in pyproject.toml. Strings are quoted
|
|
96
|
+
* with double quotes; we escape backslashes + double quotes.
|
|
97
|
+
*/
|
|
98
|
+
function tomlStringArray(values) {
|
|
99
|
+
if (values.length === 0)
|
|
100
|
+
return '[]';
|
|
101
|
+
const lines = values.map(v => ` "${v.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`);
|
|
102
|
+
return `[\n${lines.join(',\n')},\n]`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Generate the pyproject.toml body. Caller supplies the resolved
|
|
106
|
+
* distribution name, package name, dependency list, and FastAPI flag
|
|
107
|
+
* (only difference: FastAPI adds a `[project.scripts]` block).
|
|
108
|
+
*/
|
|
109
|
+
export function buildPyproject(opts) {
|
|
110
|
+
const { distributionName, packageName, dependencies, isFastapi } = opts;
|
|
111
|
+
const lines = [];
|
|
112
|
+
lines.push('[project]');
|
|
113
|
+
lines.push(`name = "${distributionName}"`);
|
|
114
|
+
lines.push('version = "0.1.0"');
|
|
115
|
+
lines.push('requires-python = ">=3.11"');
|
|
116
|
+
lines.push(`dependencies = ${tomlStringArray(dependencies)}`);
|
|
117
|
+
lines.push('');
|
|
118
|
+
if (isFastapi) {
|
|
119
|
+
lines.push('[project.scripts]');
|
|
120
|
+
lines.push(`${distributionName}-server = "${packageName}.main:run"`);
|
|
121
|
+
lines.push('');
|
|
122
|
+
}
|
|
123
|
+
lines.push('[build-system]');
|
|
124
|
+
lines.push('requires = ["hatchling"]');
|
|
125
|
+
lines.push('build-backend = "hatchling.build"');
|
|
126
|
+
lines.push('');
|
|
127
|
+
// codex W1 — explicit packages list, no auto-discovery.
|
|
128
|
+
lines.push('[tool.hatch.build.targets.wheel]');
|
|
129
|
+
lines.push(`packages = ["src/${packageName}"]`);
|
|
130
|
+
lines.push('');
|
|
131
|
+
lines.push('[tool.pytest.ini_options]');
|
|
132
|
+
lines.push('testpaths = ["tests"]');
|
|
133
|
+
lines.push('');
|
|
134
|
+
return lines.join('\n');
|
|
135
|
+
}
|
|
136
|
+
/** FastAPI entrypoint — codex CRITICAL #2: must be runnable, not a stub. */
|
|
137
|
+
export function buildFastapiMain(packageName) {
|
|
138
|
+
return `"""FastAPI entrypoint — auto-scaffolded by claude-autopilot.
|
|
139
|
+
Override the prose docstring + add real routes; keep \`app\` and
|
|
140
|
+
\`run()\` exported so the [project.scripts] entry stays valid.
|
|
141
|
+
"""
|
|
142
|
+
from fastapi import FastAPI
|
|
143
|
+
import uvicorn
|
|
144
|
+
|
|
145
|
+
app = FastAPI()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.get("/health")
|
|
149
|
+
def health() -> dict[str, str]:
|
|
150
|
+
return {"status": "ok"}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def run() -> None:
|
|
154
|
+
uvicorn.run("${packageName}.main:app", host="0.0.0.0", port=8000)
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
/** Smoke test for the FastAPI scaffold — also auto-included so pytest config isn't dead. */
|
|
158
|
+
export function buildFastapiTest(packageName) {
|
|
159
|
+
return `from fastapi.testclient import TestClient
|
|
160
|
+
from ${packageName}.main import app
|
|
161
|
+
|
|
162
|
+
client = TestClient(app)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_health() -> None:
|
|
166
|
+
response = client.get("/health")
|
|
167
|
+
assert response.status_code == 200
|
|
168
|
+
assert response.json() == {"status": "ok"}
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
/** Generic Python README placeholder. Never overwrites an existing README. */
|
|
172
|
+
function buildReadme(distributionName, isFastapi) {
|
|
173
|
+
const stackLabel = isFastapi ? 'FastAPI' : 'Python';
|
|
174
|
+
return `# ${distributionName}
|
|
175
|
+
|
|
176
|
+
${stackLabel} project scaffolded by \`claude-autopilot scaffold --from-spec\`.
|
|
177
|
+
|
|
178
|
+
## Install
|
|
179
|
+
|
|
180
|
+
\`\`\`bash
|
|
181
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
182
|
+
pip install -e .
|
|
183
|
+
\`\`\`
|
|
184
|
+
|
|
185
|
+
## Test
|
|
186
|
+
|
|
187
|
+
\`\`\`bash
|
|
188
|
+
pytest
|
|
189
|
+
\`\`\`
|
|
190
|
+
`;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The Python scaffolder. Materializes:
|
|
194
|
+
* - src/<package_name>/__init__.py (empty)
|
|
195
|
+
* - tests/ directory
|
|
196
|
+
* - pyproject.toml (PEP 621 + hatchling)
|
|
197
|
+
* - README.md (only if missing)
|
|
198
|
+
*
|
|
199
|
+
* For FastAPI specs it also writes:
|
|
200
|
+
* - src/<package_name>/main.py (runnable FastAPI app)
|
|
201
|
+
* - tests/test_main.py (smoke test)
|
|
202
|
+
*
|
|
203
|
+
* Files explicitly listed in the spec's `## Files` get touched as empty
|
|
204
|
+
* placeholders if they don't already exist (matches the v7.2.0 Node
|
|
205
|
+
* behavior). The special files above are written with content even when
|
|
206
|
+
* not listed in `## Files` — without them the generated pyproject.toml
|
|
207
|
+
* is invalid (missing package dir) or has dead config (no tests).
|
|
208
|
+
*/
|
|
209
|
+
export async function scaffoldPython(ctx, opts) {
|
|
210
|
+
const { cwd, parsed, dryRun } = ctx;
|
|
211
|
+
const { isFastapi } = opts;
|
|
212
|
+
const distributionName = normalizeDistributionName(path.basename(cwd));
|
|
213
|
+
const packageName = packageNameFromDistribution(distributionName);
|
|
214
|
+
const filesCreated = [];
|
|
215
|
+
const filesSkippedExisting = [];
|
|
216
|
+
const dirsCreated = [];
|
|
217
|
+
// We treat these as "managed" — we generate them with content, not
|
|
218
|
+
// empty placeholders, so the spec's bullet-list entries for them
|
|
219
|
+
// don't get touched first.
|
|
220
|
+
const MANAGED_FILES = new Set([
|
|
221
|
+
'pyproject.toml',
|
|
222
|
+
'requirements.txt', // we don't generate this, but if listed we leave it
|
|
223
|
+
`src/${packageName}/__init__.py`,
|
|
224
|
+
`src/${packageName}/main.py`,
|
|
225
|
+
'tests/test_main.py',
|
|
226
|
+
'README.md',
|
|
227
|
+
]);
|
|
228
|
+
// 1) Create directories. Always include the package + tests dirs;
|
|
229
|
+
// plus any dirs implied by spec paths.
|
|
230
|
+
const dirs = new Set([`src/${packageName}`, 'tests']);
|
|
231
|
+
for (const p of parsed.paths) {
|
|
232
|
+
const d = path.dirname(p);
|
|
233
|
+
if (d && d !== '.')
|
|
234
|
+
dirs.add(d);
|
|
235
|
+
}
|
|
236
|
+
for (const d of dirs) {
|
|
237
|
+
const abs = path.join(cwd, d);
|
|
238
|
+
if (fs.existsSync(abs))
|
|
239
|
+
continue;
|
|
240
|
+
if (!dryRun)
|
|
241
|
+
await fsAsync.mkdir(abs, { recursive: true });
|
|
242
|
+
dirsCreated.push(d);
|
|
243
|
+
console.log(` ${PASS} mkdir ${DIM(d + '/')}`);
|
|
244
|
+
}
|
|
245
|
+
// 2) Touch placeholder files for any spec paths we don't manage.
|
|
246
|
+
for (const p of parsed.paths) {
|
|
247
|
+
if (MANAGED_FILES.has(p))
|
|
248
|
+
continue;
|
|
249
|
+
const abs = path.join(cwd, p);
|
|
250
|
+
if (fs.existsSync(abs)) {
|
|
251
|
+
filesSkippedExisting.push(p);
|
|
252
|
+
console.log(` ${SKIP} exists ${DIM(p)}`);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (!dryRun) {
|
|
256
|
+
await fsAsync.mkdir(path.dirname(abs), { recursive: true });
|
|
257
|
+
await fsAsync.writeFile(abs, '', 'utf8');
|
|
258
|
+
}
|
|
259
|
+
filesCreated.push(p);
|
|
260
|
+
console.log(` ${PASS} touch ${DIM(p)}`);
|
|
261
|
+
}
|
|
262
|
+
// 3) src/<package_name>/__init__.py
|
|
263
|
+
const initRel = `src/${packageName}/__init__.py`;
|
|
264
|
+
const initAbs = path.join(cwd, initRel);
|
|
265
|
+
if (fs.existsSync(initAbs)) {
|
|
266
|
+
filesSkippedExisting.push(initRel);
|
|
267
|
+
console.log(` ${SKIP} exists ${DIM(initRel)}`);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
if (!dryRun)
|
|
271
|
+
await fsAsync.writeFile(initAbs, '', 'utf8');
|
|
272
|
+
filesCreated.push(initRel);
|
|
273
|
+
console.log(` ${PASS} touch ${DIM(initRel)}`);
|
|
274
|
+
}
|
|
275
|
+
// 4) FastAPI-only: main.py + tests/test_main.py
|
|
276
|
+
if (isFastapi) {
|
|
277
|
+
const mainRel = `src/${packageName}/main.py`;
|
|
278
|
+
const mainAbs = path.join(cwd, mainRel);
|
|
279
|
+
if (fs.existsSync(mainAbs)) {
|
|
280
|
+
filesSkippedExisting.push(mainRel);
|
|
281
|
+
console.log(` ${SKIP} exists ${DIM(mainRel)}`);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
if (!dryRun)
|
|
285
|
+
await fsAsync.writeFile(mainAbs, buildFastapiMain(packageName), 'utf8');
|
|
286
|
+
filesCreated.push(mainRel);
|
|
287
|
+
console.log(` ${PASS} write ${DIM(`${mainRel} (FastAPI app + /health + run())`)}`);
|
|
288
|
+
}
|
|
289
|
+
const testRel = 'tests/test_main.py';
|
|
290
|
+
const testAbs = path.join(cwd, testRel);
|
|
291
|
+
if (fs.existsSync(testAbs)) {
|
|
292
|
+
filesSkippedExisting.push(testRel);
|
|
293
|
+
console.log(` ${SKIP} exists ${DIM(testRel)}`);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
if (!dryRun)
|
|
297
|
+
await fsAsync.writeFile(testAbs, buildFastapiTest(packageName), 'utf8');
|
|
298
|
+
filesCreated.push(testRel);
|
|
299
|
+
console.log(` ${PASS} write ${DIM(`${testRel} (smoke test for /health)`)}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// 5) pyproject.toml
|
|
303
|
+
const pyprojectAbs = path.join(cwd, 'pyproject.toml');
|
|
304
|
+
if (fs.existsSync(pyprojectAbs)) {
|
|
305
|
+
filesSkippedExisting.push('pyproject.toml');
|
|
306
|
+
console.log(` ${SKIP} exists ${DIM('pyproject.toml (preserved)')}`);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
const dependencies = buildPythonDependencies(parsed.packageHints.pythonDeps, isFastapi);
|
|
310
|
+
const body = buildPyproject({ distributionName, packageName, dependencies, isFastapi });
|
|
311
|
+
if (!dryRun)
|
|
312
|
+
await fsAsync.writeFile(pyprojectAbs, body, 'utf8');
|
|
313
|
+
filesCreated.push('pyproject.toml');
|
|
314
|
+
const flavor = isFastapi ? 'PEP 621 + hatchling + FastAPI deps' : 'PEP 621 + hatchling';
|
|
315
|
+
console.log(` ${PASS} write ${DIM(`pyproject.toml (${flavor})`)}`);
|
|
316
|
+
}
|
|
317
|
+
// 6) README.md — codex NOTE #1 (always create, never overwrite).
|
|
318
|
+
const readmeAbs = path.join(cwd, 'README.md');
|
|
319
|
+
if (fs.existsSync(readmeAbs)) {
|
|
320
|
+
filesSkippedExisting.push('README.md');
|
|
321
|
+
console.log(` ${SKIP} exists ${DIM('README.md (preserved)')}`);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
if (!dryRun)
|
|
325
|
+
await fsAsync.writeFile(readmeAbs, buildReadme(distributionName, isFastapi), 'utf8');
|
|
326
|
+
filesCreated.push('README.md');
|
|
327
|
+
console.log(` ${PASS} write ${DIM('README.md')}`);
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
filesCreated,
|
|
331
|
+
dirsCreated,
|
|
332
|
+
filesSkippedExisting,
|
|
333
|
+
// Node-shape fields — we never touch package.json / tsconfig in Python.
|
|
334
|
+
packageJsonAction: 'skipped-exists',
|
|
335
|
+
tsconfigAction: 'skipped-no-ts',
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
//# sourceMappingURL=python.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Supported `--stack` values. v7.5+ will add 'go', 'rust', 'ruby'. */
|
|
2
|
+
export type Stack = 'node' | 'python' | 'fastapi';
|
|
3
|
+
/**
|
|
4
|
+
* Stacks we can DETECT but cannot scaffold yet. Detection still warns +
|
|
5
|
+
* exits 3 so the operator gets a clear "v7.5" diagnostic instead of a
|
|
6
|
+
* silent fallback to Node, which would generate a wrong-language skeleton.
|
|
7
|
+
*/
|
|
8
|
+
export type UnsupportedStack = 'go' | 'rust' | 'ruby';
|
|
9
|
+
export interface ParsedFiles {
|
|
10
|
+
/** Raw paths extracted from the `## Files` section bullets. */
|
|
11
|
+
paths: string[];
|
|
12
|
+
/** Loosely-parsed package.json hints found anywhere in the section. */
|
|
13
|
+
packageHints: {
|
|
14
|
+
bin?: Record<string, string>;
|
|
15
|
+
type?: 'module' | 'commonjs';
|
|
16
|
+
dependencies?: Record<string, string>;
|
|
17
|
+
devDependencies?: Record<string, string>;
|
|
18
|
+
scripts?: Record<string, string>;
|
|
19
|
+
/**
|
|
20
|
+
* Best-effort stack hint from prose ("uses fastapi", "Python 3.12",
|
|
21
|
+
* "Node 22 ESM"). Used as a tie-breaker when path heuristics are
|
|
22
|
+
* ambiguous between Python and FastAPI.
|
|
23
|
+
*/
|
|
24
|
+
stackHint?: 'node' | 'python' | 'fastapi';
|
|
25
|
+
/**
|
|
26
|
+
* Extra Python dependency strings extracted via the narrow contract
|
|
27
|
+
* documented in the spec ("Dependency hint extraction"):
|
|
28
|
+
* - explicit `dependencies: [...]` block in spec prose
|
|
29
|
+
* - backticked package names with extras (`uvicorn[standard]`)
|
|
30
|
+
* - phrase `depends on <name>`
|
|
31
|
+
*
|
|
32
|
+
* Stored verbatim — never version-inferred. Deduped by
|
|
33
|
+
* PEP 503 normalized name in the Python scaffolder.
|
|
34
|
+
*/
|
|
35
|
+
pythonDeps?: string[];
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export interface ScaffoldOptions {
|
|
39
|
+
cwd?: string;
|
|
40
|
+
specPath: string;
|
|
41
|
+
/** When true, log what would happen but don't write anything. */
|
|
42
|
+
dryRun?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Explicit stack override. When provided, skips path-based detection
|
|
45
|
+
* (but still validates the value: unknown → exit 3). v7.4.0 ships
|
|
46
|
+
* 'node' | 'python' | 'fastapi'.
|
|
47
|
+
*/
|
|
48
|
+
stack?: Stack;
|
|
49
|
+
}
|
|
50
|
+
export interface ScaffoldResult {
|
|
51
|
+
filesCreated: string[];
|
|
52
|
+
dirsCreated: string[];
|
|
53
|
+
filesSkippedExisting: string[];
|
|
54
|
+
/** Node-only metadata; Python scaffolder leaves these as 'skipped-no-ts' / 'skipped-exists'. */
|
|
55
|
+
packageJsonAction: 'created' | 'merged' | 'skipped-exists';
|
|
56
|
+
tsconfigAction: 'created' | 'skipped-exists' | 'skipped-no-ts';
|
|
57
|
+
/** v7.4.0 — which stack was used. Useful for tests and for the CLI banner. */
|
|
58
|
+
stack?: Stack;
|
|
59
|
+
/** Names of files explicitly skipped because of `--stack` filtering (codex W5). */
|
|
60
|
+
ignoredOtherStackFiles?: string[];
|
|
61
|
+
}
|
|
62
|
+
/** Per-stack scaffolders share this small context. */
|
|
63
|
+
export interface ScaffoldRunContext {
|
|
64
|
+
cwd: string;
|
|
65
|
+
parsed: ParsedFiles;
|
|
66
|
+
dryRun: boolean;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// v7.4.0 — shared types for per-stack scaffolders. Lives in its own file
|
|
2
|
+
// (rather than ../scaffold.ts) so that node.ts and python.ts can both import
|
|
3
|
+
// from it without creating a circular dependency back to the public entry
|
|
4
|
+
// module.
|
|
5
|
+
export {};
|
|
6
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -1,39 +1,53 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
dirsCreated: string[];
|
|
10
|
-
filesSkippedExisting: string[];
|
|
11
|
-
packageJsonAction: 'created' | 'merged' | 'skipped-exists';
|
|
12
|
-
tsconfigAction: 'created' | 'skipped-exists' | 'skipped-no-ts';
|
|
13
|
-
}
|
|
14
|
-
interface ParsedFiles {
|
|
15
|
-
/** Raw paths extracted from the `## Files` section bullets. */
|
|
16
|
-
paths: string[];
|
|
17
|
-
/** Loosely-parsed package.json hints found anywhere in the section. */
|
|
18
|
-
packageHints: {
|
|
19
|
-
bin?: Record<string, string>;
|
|
20
|
-
type?: 'module' | 'commonjs';
|
|
21
|
-
dependencies?: Record<string, string>;
|
|
22
|
-
devDependencies?: Record<string, string>;
|
|
23
|
-
scripts?: Record<string, string>;
|
|
24
|
-
};
|
|
25
|
-
}
|
|
1
|
+
import { buildStarterPackageJson } from './scaffold/node.ts';
|
|
2
|
+
import type { ParsedFiles, ScaffoldOptions, ScaffoldResult, Stack, UnsupportedStack } from './scaffold/types.ts';
|
|
3
|
+
export { buildStarterPackageJson };
|
|
4
|
+
export type { ScaffoldOptions, ScaffoldResult, ParsedFiles, Stack };
|
|
5
|
+
/** Valid `--stack` argument values. v7.5+ adds 'go', 'rust', 'ruby'. */
|
|
6
|
+
export declare const SUPPORTED_STACKS: readonly Stack[];
|
|
7
|
+
/** Stacks we DETECT-but-don't-support yet. Mapped to spec exit-3 messages. */
|
|
8
|
+
export declare const UNSUPPORTED_STACK_FILES: Record<UnsupportedStack, string>;
|
|
26
9
|
/**
|
|
27
10
|
* Parse the `## Files` (or `## files`) section of a spec markdown file.
|
|
28
11
|
* Tolerant: missing section returns `null`; malformed bullets are skipped
|
|
29
12
|
* silently. Returns extracted file paths + best-effort package-hint blob.
|
|
13
|
+
*
|
|
14
|
+
* v7.4.0 also extracts:
|
|
15
|
+
* - `stackHint` — first prose mention of `fastapi` / `python` / `node`
|
|
16
|
+
* (case-insensitive). Used as a tie-breaker when path heuristics are
|
|
17
|
+
* ambiguous between Python and FastAPI.
|
|
18
|
+
* - `pythonDeps` — narrow extraction per spec ("Dependency hint
|
|
19
|
+
* extraction"): explicit `dependencies: [...]` block, backticked
|
|
20
|
+
* package names with extras, and the phrase `depends on <name>`.
|
|
30
21
|
*/
|
|
31
22
|
export declare function parseSpecFiles(markdown: string): ParsedFiles | null;
|
|
32
23
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
24
|
+
* Result of stack detection. `kind` is one of:
|
|
25
|
+
* - 'resolved' — `stack` is set; proceed.
|
|
26
|
+
* - 'unsupported' — detected an unsupported stack file (Go/Rust/Ruby).
|
|
27
|
+
* Caller exits 3 with `message`.
|
|
28
|
+
* - 'polyglot' — both Node + Python markers present without --stack.
|
|
29
|
+
* Caller exits 3 with `message`.
|
|
35
30
|
*/
|
|
36
|
-
export
|
|
31
|
+
export type StackDetection = {
|
|
32
|
+
kind: 'resolved';
|
|
33
|
+
stack: Stack;
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'unsupported';
|
|
36
|
+
stack: UnsupportedStack;
|
|
37
|
+
message: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'polyglot';
|
|
40
|
+
message: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Apply the precedence ladder documented at the top of this file. Pure
|
|
44
|
+
* function — no I/O — so it's directly unit-testable.
|
|
45
|
+
*/
|
|
46
|
+
export declare function detectStack(parsed: ParsedFiles, explicit?: Stack): StackDetection;
|
|
47
|
+
/**
|
|
48
|
+
* Print the `--list-stacks` output (codex NOTE #2). Three sections:
|
|
49
|
+
* Supported, Auto-detected, Recognized-but-unsupported.
|
|
50
|
+
*/
|
|
51
|
+
export declare function printStackList(): void;
|
|
37
52
|
export declare function runScaffold(opts: ScaffoldOptions): Promise<ScaffoldResult>;
|
|
38
|
-
export {};
|
|
39
53
|
//# sourceMappingURL=scaffold.d.ts.map
|
package/dist/src/cli/scaffold.js
CHANGED
|
@@ -1,60 +1,64 @@
|
|
|
1
|
-
// v7.
|
|
1
|
+
// v7.4.0 — `claude-autopilot scaffold --from-spec <path>` (per-stack).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// History:
|
|
4
|
+
// v7.2.0 — initial verb, Node ESM only.
|
|
5
|
+
// v7.4.0 — split per-stack scaffolders into ./scaffold/{node,python}.ts.
|
|
6
|
+
// This file remains the public entry point + stack detector +
|
|
7
|
+
// dispatcher; src/index.ts continues to re-export `runScaffold`,
|
|
8
|
+
// `parseSpecFiles`, `buildStarterPackageJson`, plus the
|
|
9
|
+
// `ScaffoldOptions` / `ScaffoldResult` types from here so library
|
|
10
|
+
// consumers don't break.
|
|
9
11
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
12
|
+
// Stack detection lives in `detectStack()`. Per the spec ("Stack detection"
|
|
13
|
+
// section), precedence is:
|
|
14
|
+
// 1. explicit --stack flag (validated; unknown -> exit 3)
|
|
15
|
+
// 2. FastAPI (path + 'fastapi' mention) — checked BEFORE generic Python so
|
|
16
|
+
// a FastAPI spec listing pyproject.toml isn't mis-classified
|
|
17
|
+
// 3. Python (pyproject.toml or requirements.txt)
|
|
18
|
+
// 4. Node (package.json)
|
|
19
|
+
// 5. Detected-but-unsupported (go.mod / Cargo.toml / Gemfile) -> exit 3
|
|
20
|
+
// 6. Fallback: Node ESM (preserves v7.2.0 default for ambiguous specs)
|
|
16
21
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// ## Files
|
|
20
|
-
//
|
|
21
|
-
// * `package.json` — `type: module`, `bin: { foo: bin/foo.js }`,
|
|
22
|
-
// `dependencies: { @anthropic-ai/sdk: ^0.91 }`, ...
|
|
23
|
-
// * `bin/foo.js` — argv parser + main loop.
|
|
24
|
-
// * `src/baz.js` — pure function.
|
|
25
|
-
// * `tests/foo.test.js` — node:test cases.
|
|
26
|
-
// * `README.md` — usage + install.
|
|
27
|
-
//
|
|
28
|
-
// Heuristics:
|
|
29
|
-
// - Backtick-quoted paths in `## Files` bullets become directories
|
|
30
|
-
// (parent of the path) and empty placeholder files (the path itself).
|
|
31
|
-
// - JSON-ish tokens in the bullet description (`type: module`,
|
|
32
|
-
// `dependencies: { foo: ^1 }`) get parsed loosely and merged into
|
|
33
|
-
// a starter package.json.
|
|
34
|
-
// - tsconfig is a Node 22 ESM default with `allowJs+checkJs+noEmit`
|
|
35
|
-
// when the spec lists `.js` files (matches v7.1.6 benchmark project),
|
|
36
|
-
// or compiled NodeNext when it lists `.ts`.
|
|
37
|
-
//
|
|
38
|
-
// What this DELIBERATELY does NOT do:
|
|
39
|
-
// - Run `npm install`. The user can decide which package manager.
|
|
40
|
-
// - Pick a test runner if the spec doesn't say. Echoes `npm test`.
|
|
41
|
-
// - Generate the CLAUDE.md (that's v7.1.7's job).
|
|
22
|
+
// Polyglot guard: package.json AND pyproject.toml together without --stack
|
|
23
|
+
// -> exit 3 with "polyglot spec — pass --stack to disambiguate".
|
|
42
24
|
//
|
|
43
25
|
// Exit codes:
|
|
44
|
-
// 0 — scaffolded
|
|
45
|
-
// 1 — spec file missing
|
|
46
|
-
// 2 — spec missing
|
|
26
|
+
// 0 — scaffolded
|
|
27
|
+
// 1 — spec file missing
|
|
28
|
+
// 2 — spec missing `## Files` section
|
|
29
|
+
// 3 (NEW v7.4.0) — `--stack` value not recognized, detected-but-unsupported
|
|
30
|
+
// stack (Go/Rust/Ruby), or polyglot spec without --stack
|
|
47
31
|
import * as fs from 'node:fs';
|
|
48
32
|
import * as fsAsync from 'node:fs/promises';
|
|
49
33
|
import * as path from 'node:path';
|
|
50
|
-
|
|
51
|
-
|
|
34
|
+
import { scaffoldNode, buildStarterPackageJson } from "./scaffold/node.js";
|
|
35
|
+
import { scaffoldPython } from "./scaffold/python.js";
|
|
52
36
|
const BOLD = (t) => `\x1b[1m${t}\x1b[0m`;
|
|
53
37
|
const DIM = (t) => `\x1b[2m${t}\x1b[0m`;
|
|
38
|
+
// Re-export types + the legacy buildStarterPackageJson so `src/index.ts` and
|
|
39
|
+
// the existing tests/scaffold.test.ts (which imports from this module) keep
|
|
40
|
+
// compiling without changes.
|
|
41
|
+
export { buildStarterPackageJson };
|
|
42
|
+
/** Valid `--stack` argument values. v7.5+ adds 'go', 'rust', 'ruby'. */
|
|
43
|
+
export const SUPPORTED_STACKS = ['node', 'python', 'fastapi'];
|
|
44
|
+
/** Stacks we DETECT-but-don't-support yet. Mapped to spec exit-3 messages. */
|
|
45
|
+
export const UNSUPPORTED_STACK_FILES = {
|
|
46
|
+
go: 'go.mod',
|
|
47
|
+
rust: 'Cargo.toml',
|
|
48
|
+
ruby: 'Gemfile',
|
|
49
|
+
};
|
|
54
50
|
/**
|
|
55
51
|
* Parse the `## Files` (or `## files`) section of a spec markdown file.
|
|
56
52
|
* Tolerant: missing section returns `null`; malformed bullets are skipped
|
|
57
53
|
* silently. Returns extracted file paths + best-effort package-hint blob.
|
|
54
|
+
*
|
|
55
|
+
* v7.4.0 also extracts:
|
|
56
|
+
* - `stackHint` — first prose mention of `fastapi` / `python` / `node`
|
|
57
|
+
* (case-insensitive). Used as a tie-breaker when path heuristics are
|
|
58
|
+
* ambiguous between Python and FastAPI.
|
|
59
|
+
* - `pythonDeps` — narrow extraction per spec ("Dependency hint
|
|
60
|
+
* extraction"): explicit `dependencies: [...]` block, backticked
|
|
61
|
+
* package names with extras, and the phrase `depends on <name>`.
|
|
58
62
|
*/
|
|
59
63
|
export function parseSpecFiles(markdown) {
|
|
60
64
|
const filesSectionRe = /^##\s+files\s*$/im;
|
|
@@ -78,12 +82,20 @@ export function parseSpecFiles(markdown) {
|
|
|
78
82
|
continue;
|
|
79
83
|
const raw = captured.trim();
|
|
80
84
|
// Skip prose-y entries by requiring path-shape: contains `/` or
|
|
81
|
-
// ends in known ext, OR is a known root-level file.
|
|
82
|
-
|
|
85
|
+
// ends in known ext, OR is a known root-level file. v7.4.0 adds
|
|
86
|
+
// `pyproject.toml`, `requirements.txt`, `Cargo.toml`, `Gemfile`,
|
|
87
|
+
// `go.mod` to the root-file allowlist so the stack detector can see
|
|
88
|
+
// them.
|
|
89
|
+
if (/[/.](?:js|ts|tsx|jsx|md|json|yaml|yml|sh|py|rs|go|rb|sql|toml)$/i.test(raw) ||
|
|
83
90
|
raw === 'package.json' ||
|
|
84
91
|
raw === 'tsconfig.json' ||
|
|
85
92
|
raw === 'README.md' ||
|
|
86
|
-
raw === '.gitignore'
|
|
93
|
+
raw === '.gitignore' ||
|
|
94
|
+
raw === 'pyproject.toml' ||
|
|
95
|
+
raw === 'requirements.txt' ||
|
|
96
|
+
raw === 'Cargo.toml' ||
|
|
97
|
+
raw === 'Gemfile' ||
|
|
98
|
+
raw === 'go.mod') {
|
|
87
99
|
paths.push(raw);
|
|
88
100
|
}
|
|
89
101
|
}
|
|
@@ -104,12 +116,14 @@ export function parseSpecFiles(markdown) {
|
|
|
104
116
|
if (Object.keys(entries).length > 0)
|
|
105
117
|
packageHints.bin = entries;
|
|
106
118
|
}
|
|
107
|
-
// dependencies: { foo: ^1 }
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
119
|
+
// dependencies: { foo: ^1 } — Node-shape (object form).
|
|
120
|
+
// Skip if it looks like an array (Python-shape `dependencies: [...]`)
|
|
121
|
+
// — that's handled below as a Python dep block.
|
|
122
|
+
const depObjMatch = /dependencies\s*:\s*\{\s*([^}]+)\s*\}/i.exec(sectionBody);
|
|
123
|
+
const depObjBody = depObjMatch?.[1];
|
|
124
|
+
if (depObjBody) {
|
|
111
125
|
const entries = {};
|
|
112
|
-
for (const part of
|
|
126
|
+
for (const part of depObjBody.split(',')) {
|
|
113
127
|
const [name, version] = part.split(':').map((s) => s.trim().replace(/['"`]/g, ''));
|
|
114
128
|
if (name && version)
|
|
115
129
|
entries[name] = version;
|
|
@@ -133,60 +147,133 @@ export function parseSpecFiles(markdown) {
|
|
|
133
147
|
if (Object.keys(entries).length > 0)
|
|
134
148
|
packageHints.scripts = entries;
|
|
135
149
|
}
|
|
150
|
+
// v7.4.0 — Python dep extraction (narrow contract, codex W6).
|
|
151
|
+
// Pattern 1: explicit `dependencies: [foo, bar, baz]` array form.
|
|
152
|
+
// We deliberately accept the Python-style array AFTER the Node-style
|
|
153
|
+
// object check above so a Node spec with `dependencies: { foo: ^1 }`
|
|
154
|
+
// still flows into packageHints.dependencies.
|
|
155
|
+
const pythonDeps = [];
|
|
156
|
+
const depArrayMatch = /dependencies\s*:\s*\[\s*([^\]]+)\s*\]/i.exec(sectionBody);
|
|
157
|
+
const depArrayBody = depArrayMatch?.[1];
|
|
158
|
+
if (depArrayBody) {
|
|
159
|
+
for (const raw of depArrayBody.split(',')) {
|
|
160
|
+
const cleaned = raw.trim().replace(/^[`'"]/, '').replace(/[`'"]$/, '');
|
|
161
|
+
if (cleaned)
|
|
162
|
+
pythonDeps.push(cleaned);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Pattern 2: backticked package names with extras. We look for
|
|
166
|
+
// backticks containing `name[extra]` (with or without a version
|
|
167
|
+
// suffix). This is intentionally narrow — `foo` alone in backticks
|
|
168
|
+
// could be anything (filename, prose), so we require the `[extra]`
|
|
169
|
+
// shape to fire this pattern.
|
|
170
|
+
const extrasRe = /`([A-Za-z][A-Za-z0-9._-]*\[[^\]`]+\][^`]*)`/g;
|
|
171
|
+
let em;
|
|
172
|
+
while ((em = extrasRe.exec(sectionBody)) !== null) {
|
|
173
|
+
const value = em[1]?.trim();
|
|
174
|
+
if (value)
|
|
175
|
+
pythonDeps.push(value);
|
|
176
|
+
}
|
|
177
|
+
// Pattern 3: phrase `depends on <name>`. We capture the next
|
|
178
|
+
// identifier-shaped token (PEP 508 names: letters / digits / `._-`).
|
|
179
|
+
const dependsOnRe = /depends\s+on\s+`?([A-Za-z][A-Za-z0-9._-]*(?:\[[^\]]+\])?(?:[<>=!~][^\s`]+)?)`?/gi;
|
|
180
|
+
let dm;
|
|
181
|
+
while ((dm = dependsOnRe.exec(sectionBody)) !== null) {
|
|
182
|
+
const name = dm[1]?.trim();
|
|
183
|
+
if (name)
|
|
184
|
+
pythonDeps.push(name);
|
|
185
|
+
}
|
|
186
|
+
if (pythonDeps.length > 0)
|
|
187
|
+
packageHints.pythonDeps = pythonDeps;
|
|
188
|
+
// v7.4.0 — stack hint extraction. First-match wins, FastAPI checked
|
|
189
|
+
// before generic Python (codex C1) so prose like "FastAPI app on
|
|
190
|
+
// Python 3.12" classifies as fastapi, not python.
|
|
191
|
+
if (/\bfastapi\b/i.test(sectionBody)) {
|
|
192
|
+
packageHints.stackHint = 'fastapi';
|
|
193
|
+
}
|
|
194
|
+
else if (/\bpython\b/i.test(sectionBody)) {
|
|
195
|
+
packageHints.stackHint = 'python';
|
|
196
|
+
}
|
|
197
|
+
else if (/\bnode(?:\.js)?\s+\d+\b/i.test(sectionBody)) {
|
|
198
|
+
packageHints.stackHint = 'node';
|
|
199
|
+
}
|
|
136
200
|
return { paths, packageHints };
|
|
137
201
|
}
|
|
138
202
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
203
|
+
* Apply the precedence ladder documented at the top of this file. Pure
|
|
204
|
+
* function — no I/O — so it's directly unit-testable.
|
|
205
|
+
*/
|
|
206
|
+
export function detectStack(parsed, explicit) {
|
|
207
|
+
// Step 1: explicit override always wins.
|
|
208
|
+
if (explicit)
|
|
209
|
+
return { kind: 'resolved', stack: explicit };
|
|
210
|
+
const paths = parsed.paths;
|
|
211
|
+
const has = (name) => paths.includes(name);
|
|
212
|
+
const hasMainPy = paths.some(p => p === 'main.py' ||
|
|
213
|
+
p === 'app/main.py' ||
|
|
214
|
+
/^src\/[^/]+\/main\.py$/.test(p));
|
|
215
|
+
const hasFastapiMention = parsed.packageHints.stackHint === 'fastapi';
|
|
216
|
+
const hasPythonMarker = has('pyproject.toml') || has('requirements.txt');
|
|
217
|
+
const hasNodeMarker = has('package.json');
|
|
218
|
+
// Polyglot guard (codex W3) — Node + Python without --stack.
|
|
219
|
+
if (hasNodeMarker && hasPythonMarker) {
|
|
220
|
+
return {
|
|
221
|
+
kind: 'polyglot',
|
|
222
|
+
message: 'polyglot spec — pass --stack to disambiguate',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// Step 2: FastAPI (BEFORE generic Python — codex C1).
|
|
226
|
+
if (hasMainPy && hasFastapiMention) {
|
|
227
|
+
return { kind: 'resolved', stack: 'fastapi' };
|
|
228
|
+
}
|
|
229
|
+
// Edge: spec lists pyproject.toml AND mentions FastAPI in prose but
|
|
230
|
+
// doesn't list main.py — still classify as FastAPI; we generate
|
|
231
|
+
// main.py ourselves anyway.
|
|
232
|
+
if (hasPythonMarker && hasFastapiMention) {
|
|
233
|
+
return { kind: 'resolved', stack: 'fastapi' };
|
|
234
|
+
}
|
|
235
|
+
// Step 3: Python.
|
|
236
|
+
if (hasPythonMarker)
|
|
237
|
+
return { kind: 'resolved', stack: 'python' };
|
|
238
|
+
// Step 4: Node.
|
|
239
|
+
if (hasNodeMarker)
|
|
240
|
+
return { kind: 'resolved', stack: 'node' };
|
|
241
|
+
// Step 5: detected-but-unsupported (codex W2).
|
|
242
|
+
for (const [stack, file] of Object.entries(UNSUPPORTED_STACK_FILES)) {
|
|
243
|
+
if (has(file)) {
|
|
244
|
+
return {
|
|
245
|
+
kind: 'unsupported',
|
|
246
|
+
stack,
|
|
247
|
+
message: `${stack} detected but not supported until v7.5`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Step 6: fallback — Node ESM (preserves v7.2.0 default for ambiguous
|
|
252
|
+
// specs that listed only paths with no root-marker file).
|
|
253
|
+
return { kind: 'resolved', stack: 'node' };
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Print the `--list-stacks` output (codex NOTE #2). Three sections:
|
|
257
|
+
* Supported, Auto-detected, Recognized-but-unsupported.
|
|
141
258
|
*/
|
|
142
|
-
export function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
pkg.devDependencies = hints.devDependencies;
|
|
160
|
-
return pkg;
|
|
259
|
+
export function printStackList() {
|
|
260
|
+
console.log('');
|
|
261
|
+
console.log(BOLD('Supported (--stack accepts these):'));
|
|
262
|
+
console.log(' node Node 22 ESM (package.json + tsconfig.json)');
|
|
263
|
+
console.log(' python Python 3.11+ (pyproject.toml + hatchling + pytest)');
|
|
264
|
+
console.log(' fastapi Python + FastAPI (auto-includes fastapi + uvicorn[standard])');
|
|
265
|
+
console.log('');
|
|
266
|
+
console.log(BOLD('Auto-detected from `## Files`:'));
|
|
267
|
+
console.log(' node when `package.json` is listed');
|
|
268
|
+
console.log(' python when `pyproject.toml` or `requirements.txt` is listed');
|
|
269
|
+
console.log(' fastapi when `main.py` is listed AND a bullet mentions `fastapi`');
|
|
270
|
+
console.log('');
|
|
271
|
+
console.log(BOLD('Recognized-but-unsupported (exit 3):'));
|
|
272
|
+
console.log(' go v7.5 (would detect via go.mod)');
|
|
273
|
+
console.log(' rust v7.5 (would detect via Cargo.toml)');
|
|
274
|
+
console.log(' ruby v7.5+ (would detect via Gemfile)');
|
|
275
|
+
console.log('');
|
|
161
276
|
}
|
|
162
|
-
const STARTER_TSCONFIG_JS = {
|
|
163
|
-
compilerOptions: {
|
|
164
|
-
target: 'ES2022',
|
|
165
|
-
module: 'NodeNext',
|
|
166
|
-
moduleResolution: 'NodeNext',
|
|
167
|
-
allowJs: true,
|
|
168
|
-
checkJs: true,
|
|
169
|
-
noEmit: true,
|
|
170
|
-
strict: true,
|
|
171
|
-
esModuleInterop: true,
|
|
172
|
-
skipLibCheck: true,
|
|
173
|
-
types: ['node'],
|
|
174
|
-
},
|
|
175
|
-
include: ['bin/**/*', 'src/**/*', 'tests/**/*'],
|
|
176
|
-
};
|
|
177
|
-
const STARTER_TSCONFIG_TS = {
|
|
178
|
-
compilerOptions: {
|
|
179
|
-
target: 'ES2022',
|
|
180
|
-
module: 'NodeNext',
|
|
181
|
-
moduleResolution: 'NodeNext',
|
|
182
|
-
outDir: 'dist',
|
|
183
|
-
strict: true,
|
|
184
|
-
esModuleInterop: true,
|
|
185
|
-
skipLibCheck: true,
|
|
186
|
-
types: ['node'],
|
|
187
|
-
},
|
|
188
|
-
include: ['bin/**/*', 'src/**/*', 'tests/**/*'],
|
|
189
|
-
};
|
|
190
277
|
export async function runScaffold(opts) {
|
|
191
278
|
const cwd = opts.cwd ?? process.cwd();
|
|
192
279
|
const specAbs = path.isAbsolute(opts.specPath) ? opts.specPath : path.join(cwd, opts.specPath);
|
|
@@ -200,88 +287,72 @@ export async function runScaffold(opts) {
|
|
|
200
287
|
process.stderr.write(`[scaffold] spec missing a "## Files" section: ${specAbs}\n`);
|
|
201
288
|
process.exit(2);
|
|
202
289
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
let tsconfigAction = 'skipped-no-ts';
|
|
210
|
-
// 1) Create directories first.
|
|
211
|
-
const dirs = new Set();
|
|
212
|
-
for (const p of parsed.paths) {
|
|
213
|
-
const d = path.dirname(p);
|
|
214
|
-
if (d && d !== '.')
|
|
215
|
-
dirs.add(d);
|
|
290
|
+
// Validate explicit --stack value. The CLI dispatch in src/cli/index.ts
|
|
291
|
+
// also validates, but doing it here too means library consumers calling
|
|
292
|
+
// `runScaffold({ stack: 'python' })` get the same guard.
|
|
293
|
+
if (opts.stack && !SUPPORTED_STACKS.includes(opts.stack)) {
|
|
294
|
+
process.stderr.write(`[scaffold] --stack "${opts.stack}" not recognized — supported: ${SUPPORTED_STACKS.join(', ')}\n`);
|
|
295
|
+
process.exit(3);
|
|
216
296
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (!opts.dryRun)
|
|
222
|
-
await fsAsync.mkdir(abs, { recursive: true });
|
|
223
|
-
dirsCreated.push(d);
|
|
224
|
-
console.log(` ${PASS} mkdir ${DIM(d + '/')}`);
|
|
297
|
+
const detection = detectStack(parsed, opts.stack);
|
|
298
|
+
if (detection.kind === 'unsupported') {
|
|
299
|
+
process.stderr.write(`[scaffold] ${detection.message}\n`);
|
|
300
|
+
process.exit(3);
|
|
225
301
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (SPECIAL.has(p))
|
|
230
|
-
continue;
|
|
231
|
-
const abs = path.join(cwd, p);
|
|
232
|
-
if (fs.existsSync(abs)) {
|
|
233
|
-
filesSkippedExisting.push(p);
|
|
234
|
-
console.log(` ${SKIP} exists ${DIM(p)}`);
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
if (!opts.dryRun) {
|
|
238
|
-
await fsAsync.mkdir(path.dirname(abs), { recursive: true });
|
|
239
|
-
// Touch — empty file. Real content is the agent's job.
|
|
240
|
-
await fsAsync.writeFile(abs, '', 'utf8');
|
|
241
|
-
}
|
|
242
|
-
filesCreated.push(p);
|
|
243
|
-
console.log(` ${PASS} touch ${DIM(p)}`);
|
|
302
|
+
if (detection.kind === 'polyglot') {
|
|
303
|
+
process.stderr.write(`[scaffold] ${detection.message}\n`);
|
|
304
|
+
process.exit(3);
|
|
244
305
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
306
|
+
const stack = detection.stack;
|
|
307
|
+
console.log(`\n${BOLD('[scaffold]')} ${DIM(specAbs)} ${DIM(`(stack: ${stack})`)}\n`);
|
|
308
|
+
// codex W5 — when --stack <python|fastapi> is explicit and the spec
|
|
309
|
+
// ALSO lists Node files, warn + filter them out so the Python
|
|
310
|
+
// scaffolder doesn't try to touch them.
|
|
311
|
+
let ignoredOtherStackFiles;
|
|
312
|
+
let parsedForStack = parsed;
|
|
313
|
+
if (opts.stack && (stack === 'python' || stack === 'fastapi')) {
|
|
314
|
+
const NODE_FILES = new Set(['package.json', 'tsconfig.json']);
|
|
315
|
+
const ignored = parsed.paths.filter(p => NODE_FILES.has(p));
|
|
316
|
+
if (ignored.length > 0) {
|
|
317
|
+
ignoredOtherStackFiles = ignored;
|
|
318
|
+
console.log(` ${DIM(`! ignoring Node files (--stack ${stack}): ${ignored.join(', ')}`)}`);
|
|
319
|
+
parsedForStack = {
|
|
320
|
+
...parsed,
|
|
321
|
+
paths: parsed.paths.filter(p => !NODE_FILES.has(p)),
|
|
322
|
+
};
|
|
259
323
|
}
|
|
260
324
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const config = tsCount > jsCount ? STARTER_TSCONFIG_TS : STARTER_TSCONFIG_JS;
|
|
274
|
-
tsconfigAction = 'created';
|
|
275
|
-
if (!opts.dryRun) {
|
|
276
|
-
await fsAsync.writeFile(tsAbs, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
277
|
-
}
|
|
278
|
-
const flavor = config === STARTER_TSCONFIG_TS ? 'compiled TS to dist/' : 'JS w/ JSDoc + checkJs';
|
|
279
|
-
console.log(` ${PASS} write ${DIM(`tsconfig.json (${flavor})`)}`);
|
|
325
|
+
else if (opts.stack === 'node') {
|
|
326
|
+
// Symmetric: when --stack node is forced and the spec also lists
|
|
327
|
+
// Python markers, drop them so we don't touch them as placeholders.
|
|
328
|
+
const PYTHON_FILES = new Set(['pyproject.toml', 'requirements.txt']);
|
|
329
|
+
const ignored = parsed.paths.filter(p => PYTHON_FILES.has(p));
|
|
330
|
+
if (ignored.length > 0) {
|
|
331
|
+
ignoredOtherStackFiles = ignored;
|
|
332
|
+
console.log(` ${DIM(`! ignoring Python files (--stack node): ${ignored.join(', ')}`)}`);
|
|
333
|
+
parsedForStack = {
|
|
334
|
+
...parsed,
|
|
335
|
+
paths: parsed.paths.filter(p => !PYTHON_FILES.has(p)),
|
|
336
|
+
};
|
|
280
337
|
}
|
|
281
338
|
}
|
|
282
|
-
|
|
339
|
+
const ctx = { cwd, parsed: parsedForStack, dryRun: !!opts.dryRun };
|
|
340
|
+
let result;
|
|
341
|
+
if (stack === 'python') {
|
|
342
|
+
result = await scaffoldPython(ctx, { isFastapi: false });
|
|
343
|
+
}
|
|
344
|
+
else if (stack === 'fastapi') {
|
|
345
|
+
result = await scaffoldPython(ctx, { isFastapi: true });
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
result = await scaffoldNode(ctx);
|
|
349
|
+
}
|
|
350
|
+
result.stack = stack;
|
|
351
|
+
if (ignoredOtherStackFiles)
|
|
352
|
+
result.ignoredOtherStackFiles = ignoredOtherStackFiles;
|
|
353
|
+
console.log(`\n${BOLD('Done.')} ${DIM(`${result.dirsCreated.length} dirs, ${result.filesCreated.length} files created, ${result.filesSkippedExisting.length} skipped.`)}\n`);
|
|
283
354
|
if (opts.dryRun)
|
|
284
355
|
console.log(DIM(`(--dry-run: no files were written)\n`));
|
|
285
|
-
return
|
|
356
|
+
return result;
|
|
286
357
|
}
|
|
287
358
|
//# sourceMappingURL=scaffold.js.map
|