@delegance/claude-autopilot 7.4.3 → 7.6.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/CHANGELOG.md +146 -0
- package/dist/src/cli/help-text.js +1 -1
- package/dist/src/cli/index.js +3 -2
- package/dist/src/cli/scaffold/go.d.ts +27 -0
- package/dist/src/cli/scaffold/go.js +225 -0
- package/dist/src/cli/scaffold/types.d.ts +4 -4
- package/dist/src/cli/scaffold.d.ts +1 -1
- package/dist/src/cli/scaffold.js +60 -25
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,152 @@
|
|
|
2
2
|
|
|
3
3
|
- v5.6 Phase 7 (docs reconciliation) — pending.
|
|
4
4
|
|
|
5
|
+
## 7.6.0 (2026-05-10)
|
|
6
|
+
|
|
7
|
+
**v7.6.0 — Go scaffold support.** Minor release. Promotes Go from
|
|
8
|
+
"detected-but-unsupported" (exit 3 in v7.4/v7.5) to a first-class
|
|
9
|
+
scaffold target, matching the Node + Python + FastAPI shape.
|
|
10
|
+
|
|
11
|
+
**New:** `claude-autopilot scaffold --from-spec <spec.md> --stack go`
|
|
12
|
+
(or auto-detected when the spec's `## Files` section lists `go.mod`
|
|
13
|
+
or `main.go`).
|
|
14
|
+
|
|
15
|
+
Generates for a basic spec:
|
|
16
|
+
|
|
17
|
+
- `go.mod` — `module <basename(cwd)>` + `go 1.22`, with an inline
|
|
18
|
+
comment documenting that the module path is the local-scaffold
|
|
19
|
+
default and should be replaced with the full hosted path
|
|
20
|
+
(e.g. `github.com/<user>/<name>`) before publishing.
|
|
21
|
+
- `main.go` — `package main` + Hello world (skipped when the spec
|
|
22
|
+
uses a `cmd/<name>/main.go` layout).
|
|
23
|
+
- `main_test.go` — `TestSmoke` stub for table-driven tests.
|
|
24
|
+
- `.gitignore` — idempotent augmentation: appends `vendor/`,
|
|
25
|
+
`*.exe`, `*.test` if not already present.
|
|
26
|
+
|
|
27
|
+
**Name normalization.** `basename(cwd)` lowercased, whitespace
|
|
28
|
+
collapsed to `-`. Dots + hyphens preserved (valid Go module path
|
|
29
|
+
chars). Path-invalid characters (`/`, `\`, control bytes) reject
|
|
30
|
+
with a clear error.
|
|
31
|
+
|
|
32
|
+
**Never overwrites.** `go.mod`, `main.go`, `main_test.go`, and
|
|
33
|
+
existing `.gitignore` entries are preserved — matches the Python
|
|
34
|
+
scaffolder pattern.
|
|
35
|
+
|
|
36
|
+
**Polyglot detection (codex CRITICAL pass-1).** `detectStack()` now
|
|
37
|
+
scans `## Files` for ALL supported stack signals (Node, Python, Go)
|
|
38
|
+
and exits 3 with `polyglot spec — pass --stack to disambiguate` when
|
|
39
|
+
more than one supported stack is present. Previously the check was
|
|
40
|
+
Node-vs-Python only; v7.6 closes the gap so e.g. `package.json` +
|
|
41
|
+
`go.mod` together correctly fail-loud instead of silently picking one.
|
|
42
|
+
|
|
43
|
+
**`--list-stacks`** now shows Go under Supported and drops it from
|
|
44
|
+
Recognized-but-unsupported. Rust + Ruby remain detection-only; the
|
|
45
|
+
Rust scaffolder is deferred to **v7.7.0** (out of scope here).
|
|
46
|
+
|
|
47
|
+
**Rust deferred.** `Cargo.toml` still exits 3 ("rust detected but
|
|
48
|
+
not supported until v7.7"). Targeted for v7.7.0 alongside the same
|
|
49
|
+
shape (Cargo.toml + `src/main.rs` + a smoke test).
|
|
50
|
+
|
|
51
|
+
## 7.5.0 (2026-05-10)
|
|
52
|
+
|
|
53
|
+
**v7.5.0 — route-sensitivity-tiered membership revocation.** Minor
|
|
54
|
+
release. Closes W4 from the v7.4.1 codex strategic review without
|
|
55
|
+
adding infrastructure (Redis / Realtime / KV all rejected — see
|
|
56
|
+
`docs/specs/v7.5.0-route-sensitivity.md` for the trade-off analysis).
|
|
57
|
+
|
|
58
|
+
**Problem.** v7.0 Phase 6 caches `check_membership_status` for 60s
|
|
59
|
+
in an HMAC-signed cookie. Worst-case revocation window = 60s for
|
|
60
|
+
EVERY dashboard request, including admin-class mutations and
|
|
61
|
+
sensitive reads where ≤1-request revocation is the bar.
|
|
62
|
+
|
|
63
|
+
**Solution.** Split the policy by route sensitivity instead of
|
|
64
|
+
tightening globally:
|
|
65
|
+
|
|
66
|
+
| Tier | Revocation window | Behavior |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| LOW (default) | ≤60s | v7.0 cookie cache (no change) |
|
|
69
|
+
| HIGH (mutations + sensitive reads) | ≤1 request | Skip cookie, always RPC |
|
|
70
|
+
|
|
71
|
+
HIGH list (locked in code, codex-reviewed): mutations on any
|
|
72
|
+
`/api/dashboard/*` route + GETs under
|
|
73
|
+
`/api/dashboard/orgs/:id/{audit,cost,cost.csv,sso/**,members/**,billing/**}`
|
|
74
|
+
+ all `/api/dashboard/api-keys/*` (including GET — codex pass-2 W4
|
|
75
|
+
flagged the listing as sensitive).
|
|
76
|
+
|
|
77
|
+
**Defense in depth (codex pass-2 CRITICAL #3).** Middleware regex
|
|
78
|
+
matching is brittle — a new sensitive handler that's not yet in
|
|
79
|
+
`HIGH_SENSITIVITY_PATTERNS` would default to LOW. Mitigation: every
|
|
80
|
+
high-sensitivity route handler now calls
|
|
81
|
+
`assertActiveMembershipForOrg()` at the top as the inner correctness
|
|
82
|
+
gate. Middleware is the outer optimization (skips the cookie cache);
|
|
83
|
+
the handler call is the inner gate (doesn't depend on the regex
|
|
84
|
+
list staying in sync).
|
|
85
|
+
|
|
86
|
+
**Path-vs-active-org assertion (codex pass-2 CRITICAL #2).** For
|
|
87
|
+
high-sensitivity org-scoped routes the middleware also extracts
|
|
88
|
+
`:orgId` from the request path and asserts it matches the
|
|
89
|
+
`cao_active_org` cookie. A user active in Org A reaching an Org B
|
|
90
|
+
URL gets a 403/302 immediately — defense against a sloppy
|
|
91
|
+
downstream handler.
|
|
92
|
+
|
|
93
|
+
**New files:**
|
|
94
|
+
|
|
95
|
+
- `apps/web/lib/middleware/route-sensitivity.ts` (~85 LOC) —
|
|
96
|
+
`HIGH_SENSITIVITY_PATTERNS` + `isHighSensitivityRoute()`.
|
|
97
|
+
- `apps/web/lib/dashboard/assert-active-membership-for-org.ts`
|
|
98
|
+
(~165 LOC) — defense-in-depth helper + `respondToMembershipError()`
|
|
99
|
+
response builder.
|
|
100
|
+
- `apps/web/__tests__/middleware/route-sensitivity.test.ts` (24
|
|
101
|
+
cases — spec list of 7 + non-GET coverage + boundary regex tests
|
|
102
|
+
for codex pass-2 W4/W5).
|
|
103
|
+
- `apps/web/__tests__/middleware/revocation-integration.test.ts`
|
|
104
|
+
(8 cases — disabled-with-fresh-cookie + low-tier cache-still-wins
|
|
105
|
+
+ helper error-mapping integration).
|
|
106
|
+
- `apps/web/__tests__/lib/dashboard/assert-active-membership-for-org.test.ts`
|
|
107
|
+
(16 cases — 4 error codes + happy path + UUID validation +
|
|
108
|
+
respondToMembershipError variants).
|
|
109
|
+
|
|
110
|
+
**Modified files:**
|
|
111
|
+
|
|
112
|
+
- `apps/web/middleware.ts` — sensitivity branch BEFORE cookie verify;
|
|
113
|
+
parseOrgIdFromPath check on high-sensitivity routes; skip cookie
|
|
114
|
+
mint on high-sensitivity success.
|
|
115
|
+
- 10 route handlers wired with `assertActiveMembershipForOrg()` as
|
|
116
|
+
the first authorization step (members CRUD + invite + enable +
|
|
117
|
+
disable; org PATCH; audit; cost JSON + CSV; SSO disconnect +
|
|
118
|
+
required + setup + domains + verify).
|
|
119
|
+
|
|
120
|
+
**Test regressions promoted to v7.5.0 expectations:** 8 existing
|
|
121
|
+
tests that asserted the OLD pre-helper behavior (non-member → 404
|
|
122
|
+
via RPC's `not_admin`, disabled-user → `not_owner`/`not_admin`) now
|
|
123
|
+
assert the NEW uniform helper behavior (non-member → 403
|
|
124
|
+
`no_membership`; disabled-user → 403 `member_disabled`). Status
|
|
125
|
+
codes unchanged for the disabled-user cases; status code for
|
|
126
|
+
non-member shifted from 404 → 403 with a non-enumerating body code.
|
|
127
|
+
Comments inline in each updated test point at this v7.5.0 trade-off.
|
|
128
|
+
|
|
129
|
+
**Test count:** 621 → 669 (+48 web). CLI suite unchanged at 1606
|
|
130
|
+
(no CLI changes).
|
|
131
|
+
|
|
132
|
+
**Codex traceability:**
|
|
133
|
+
|
|
134
|
+
| Finding | Resolution |
|
|
135
|
+
|---|---|
|
|
136
|
+
| Pass-1 C1 (W4 not closed) | Reframed: route-tier closes the security/compliance subset of W4. |
|
|
137
|
+
| Pass-1 C2 (Vercel/ECS confusion) | False positive — explicit deployment-context section in spec. |
|
|
138
|
+
| Pass-1 W1 (Redis ROI) | False positive — autopilot.dev has no Redis. |
|
|
139
|
+
| Pass-1 W3 (route-sensitivity split) | **Adopted as the central design.** |
|
|
140
|
+
| Pass-2 CRITICAL #2 (path-vs-activeOrg) | `parseOrgIdFromPath` + middleware assertion. |
|
|
141
|
+
| Pass-2 CRITICAL #3 (regex bypass risk) | `assertActiveMembershipForOrg()` defense-in-depth helper called at top of every HIGH handler. |
|
|
142
|
+
| Pass-2 W4 (api-keys GET sensitivity) | `/api/dashboard/api-keys/*` (including GET) added to HIGH list. |
|
|
143
|
+
| Pass-2 W5 (boundary pattern correctness) | Anchored regex with explicit `/`/`$` terminator; tests cover `/costume`, `/auditor`, trailing-slash, nested-path. |
|
|
144
|
+
|
|
145
|
+
**Out of scope.** Vercel KV / Realtime websocket (deferred), JWT-
|
|
146
|
+
side revocation (v7.1 already collapses ingest to ≤1 request),
|
|
147
|
+
configurable sensitivity classification (locked in code on purpose),
|
|
148
|
+
wrapper `withActiveMembershipRequired()` decorator (deferred to
|
|
149
|
+
v7.6+ refactor; the explicit call is sufficient for v7.5.0).
|
|
150
|
+
|
|
5
151
|
## 7.4.3 (2026-05-11)
|
|
6
152
|
|
|
7
153
|
**v7.4.3 — FastAPI scaffold respects spec-derived package name.**
|
|
@@ -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> [--stack node|python|fastapi])' },
|
|
31
|
+
{ verb: 'scaffold', summary: 'Scaffold project skeleton from a spec markdown (--from-spec <path> [--stack node|python|fastapi|go])' },
|
|
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
|
@@ -934,6 +934,7 @@ switch (subcommand) {
|
|
|
934
934
|
case 'scaffold': {
|
|
935
935
|
// v7.2.0 — `claude-autopilot scaffold --from-spec <path>`
|
|
936
936
|
// v7.4.0 — `--stack <node|python|fastapi>` + `--list-stacks`.
|
|
937
|
+
// v7.6.0 — `--stack go`.
|
|
937
938
|
if (boolFlag('list-stacks')) {
|
|
938
939
|
const { printStackList } = await import("./scaffold.js");
|
|
939
940
|
printStackList();
|
|
@@ -942,8 +943,8 @@ switch (subcommand) {
|
|
|
942
943
|
const fromSpec = flag('from-spec');
|
|
943
944
|
const dryRun = boolFlag('dry-run');
|
|
944
945
|
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`);
|
|
946
|
+
if (stackArg && !['node', 'python', 'fastapi', 'go'].includes(stackArg)) {
|
|
947
|
+
console.error(`\x1b[31m[claude-autopilot] --stack "${stackArg}" not recognized — supported: node, python, fastapi, go\x1b[0m`);
|
|
947
948
|
console.error(` See: claude-autopilot scaffold --list-stacks`);
|
|
948
949
|
process.exit(3);
|
|
949
950
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ScaffoldResult, ScaffoldRunContext } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Normalize a basename into a valid Go module name.
|
|
4
|
+
*
|
|
5
|
+
* - lowercased
|
|
6
|
+
* - whitespace runs collapse to a single `-`
|
|
7
|
+
* - dots + hyphens preserved (Go modules allow them)
|
|
8
|
+
* - empty result falls back to `app`
|
|
9
|
+
*
|
|
10
|
+
* Throws on path-invalid characters (`/`, `\`, NUL, other control chars) —
|
|
11
|
+
* the caller is expected to surface this as a scaffold error.
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizeGoModuleName(raw: string): string;
|
|
14
|
+
/** Build the go.mod body. Inline comment documents the local-default. */
|
|
15
|
+
export declare function buildGoMod(moduleName: string): string;
|
|
16
|
+
/** Build main.go body — minimal Hello world package. */
|
|
17
|
+
export declare function buildMainGo(): string;
|
|
18
|
+
/** Build main_test.go body — table-test friendly smoke test. */
|
|
19
|
+
export declare function buildMainTestGo(): string;
|
|
20
|
+
/**
|
|
21
|
+
* Augment `.gitignore` with Go-standard ignores. Idempotent: if `vendor/`
|
|
22
|
+
* already appears the second invocation leaves it alone. Creates the file
|
|
23
|
+
* if it doesn't exist.
|
|
24
|
+
*/
|
|
25
|
+
export declare function augmentGitignore(existing: string | null): string;
|
|
26
|
+
export declare function scaffoldGo(ctx: ScaffoldRunContext): Promise<ScaffoldResult>;
|
|
27
|
+
//# sourceMappingURL=go.d.ts.map
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// v7.6.0 — Go scaffolder.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors the Python scaffolder shape:
|
|
4
|
+
// - single `scaffoldGo()` exported entrypoint
|
|
5
|
+
// - pure-function helpers (name normalization, builders) for unit tests
|
|
6
|
+
// - never overwrites existing files (matches `· exists` log pattern)
|
|
7
|
+
// - tracks filesCreated / dirsCreated / filesSkippedExisting for return
|
|
8
|
+
//
|
|
9
|
+
// Output for a basic spec (`## Files` listing go.mod + main.go + main_test.go):
|
|
10
|
+
// - go.mod with `module <basename(cwd)>` and `go 1.22`. Inline comment
|
|
11
|
+
// documents the local-scaffold-default per codex NOTE (not a real
|
|
12
|
+
// hosted module path — users override before publishing).
|
|
13
|
+
// - main.go: package main + Hello world (only when not under cmd/<name>/)
|
|
14
|
+
// - main_test.go: smoke test
|
|
15
|
+
// - .gitignore augmentation: appends `vendor/`, `*.exe`, `*.test`
|
|
16
|
+
// idempotently — if already present, leaves them alone.
|
|
17
|
+
//
|
|
18
|
+
// Name normalization: lowercased basename(cwd), whitespace -> `-`. Dots and
|
|
19
|
+
// hyphens preserved (Go module paths permit them). Path-invalid chars
|
|
20
|
+
// (`/`, `\`, control chars) are rejected with a clear error.
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as fsAsync from 'node:fs/promises';
|
|
23
|
+
import * as path from 'node:path';
|
|
24
|
+
const PASS = '\x1b[32m✓\x1b[0m';
|
|
25
|
+
const SKIP = '\x1b[2m·\x1b[0m';
|
|
26
|
+
const DIM = (t) => `\x1b[2m${t}\x1b[0m`;
|
|
27
|
+
/**
|
|
28
|
+
* Normalize a basename into a valid Go module name.
|
|
29
|
+
*
|
|
30
|
+
* - lowercased
|
|
31
|
+
* - whitespace runs collapse to a single `-`
|
|
32
|
+
* - dots + hyphens preserved (Go modules allow them)
|
|
33
|
+
* - empty result falls back to `app`
|
|
34
|
+
*
|
|
35
|
+
* Throws on path-invalid characters (`/`, `\`, NUL, other control chars) —
|
|
36
|
+
* the caller is expected to surface this as a scaffold error.
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeGoModuleName(raw) {
|
|
39
|
+
// eslint-disable-next-line no-control-regex
|
|
40
|
+
if (/[\/\\\x00-\x1f]/.test(raw)) {
|
|
41
|
+
throw new Error(`invalid Go module name "${raw}" — path/control characters not allowed`);
|
|
42
|
+
}
|
|
43
|
+
const lower = raw.toLowerCase();
|
|
44
|
+
const collapsed = lower.replace(/\s+/g, '-');
|
|
45
|
+
return collapsed.length > 0 ? collapsed : 'app';
|
|
46
|
+
}
|
|
47
|
+
/** Build the go.mod body. Inline comment documents the local-default. */
|
|
48
|
+
export function buildGoMod(moduleName) {
|
|
49
|
+
return `// NOTE: module name is the local-scaffold default (basename of cwd).
|
|
50
|
+
// Replace with your full module path (e.g. github.com/<user>/${moduleName})
|
|
51
|
+
// before publishing or running \`go install\`.
|
|
52
|
+
module ${moduleName}
|
|
53
|
+
|
|
54
|
+
go 1.22
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
/** Build main.go body — minimal Hello world package. */
|
|
58
|
+
export function buildMainGo() {
|
|
59
|
+
return [
|
|
60
|
+
'package main',
|
|
61
|
+
'',
|
|
62
|
+
'import "fmt"',
|
|
63
|
+
'',
|
|
64
|
+
'func main() {',
|
|
65
|
+
'\tfmt.Println("Hello, world!")',
|
|
66
|
+
'}',
|
|
67
|
+
'',
|
|
68
|
+
].join('\n');
|
|
69
|
+
}
|
|
70
|
+
/** Build main_test.go body — table-test friendly smoke test. */
|
|
71
|
+
export function buildMainTestGo() {
|
|
72
|
+
return [
|
|
73
|
+
'package main',
|
|
74
|
+
'',
|
|
75
|
+
'import "testing"',
|
|
76
|
+
'',
|
|
77
|
+
'func TestSmoke(t *testing.T) {',
|
|
78
|
+
'\t// Smoke test scaffolded by claude-autopilot. Replace with real',
|
|
79
|
+
'\t// table-driven cases for your package under test.',
|
|
80
|
+
'}',
|
|
81
|
+
'',
|
|
82
|
+
].join('\n');
|
|
83
|
+
}
|
|
84
|
+
/** Lines to append to .gitignore. Idempotent — only adds missing entries. */
|
|
85
|
+
const GO_GITIGNORE_LINES = ['vendor/', '*.exe', '*.test'];
|
|
86
|
+
/**
|
|
87
|
+
* Augment `.gitignore` with Go-standard ignores. Idempotent: if `vendor/`
|
|
88
|
+
* already appears the second invocation leaves it alone. Creates the file
|
|
89
|
+
* if it doesn't exist.
|
|
90
|
+
*/
|
|
91
|
+
export function augmentGitignore(existing) {
|
|
92
|
+
const lines = existing ? existing.split('\n') : [];
|
|
93
|
+
const present = new Set(lines.map(l => l.trim()));
|
|
94
|
+
const toAdd = GO_GITIGNORE_LINES.filter(l => !present.has(l));
|
|
95
|
+
if (toAdd.length === 0)
|
|
96
|
+
return existing ?? '';
|
|
97
|
+
const prefix = existing && !existing.endsWith('\n') ? existing + '\n' : (existing ?? '');
|
|
98
|
+
return prefix + toAdd.join('\n') + '\n';
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* True when the spec lists ONLY a `cmd/<name>/main.go` style entrypoint
|
|
102
|
+
* (no top-level main.go). In that case we skip generating the top-level
|
|
103
|
+
* main.go — the spec author intends the cmd/ layout.
|
|
104
|
+
*/
|
|
105
|
+
function specHasCmdMainOnly(paths) {
|
|
106
|
+
const hasTopMain = paths.includes('main.go');
|
|
107
|
+
const hasCmdMain = paths.some(p => /^cmd\/[^/]+\/main\.go$/.test(p));
|
|
108
|
+
return hasCmdMain && !hasTopMain;
|
|
109
|
+
}
|
|
110
|
+
export async function scaffoldGo(ctx) {
|
|
111
|
+
const { cwd, parsed, dryRun } = ctx;
|
|
112
|
+
const moduleName = normalizeGoModuleName(path.basename(cwd));
|
|
113
|
+
const filesCreated = [];
|
|
114
|
+
const filesSkippedExisting = [];
|
|
115
|
+
const dirsCreated = [];
|
|
116
|
+
// Files we generate with content rather than empty placeholders.
|
|
117
|
+
const MANAGED_FILES = new Set(['go.mod', 'main.go', 'main_test.go', '.gitignore']);
|
|
118
|
+
// 1) Create directories implied by spec paths.
|
|
119
|
+
const dirs = new Set();
|
|
120
|
+
for (const p of parsed.paths) {
|
|
121
|
+
const d = path.dirname(p);
|
|
122
|
+
if (d && d !== '.')
|
|
123
|
+
dirs.add(d);
|
|
124
|
+
}
|
|
125
|
+
for (const d of dirs) {
|
|
126
|
+
const abs = path.join(cwd, d);
|
|
127
|
+
if (fs.existsSync(abs))
|
|
128
|
+
continue;
|
|
129
|
+
if (!dryRun)
|
|
130
|
+
await fsAsync.mkdir(abs, { recursive: true });
|
|
131
|
+
dirsCreated.push(d);
|
|
132
|
+
console.log(` ${PASS} mkdir ${DIM(d + '/')}`);
|
|
133
|
+
}
|
|
134
|
+
// 2) Empty-placeholder pass for spec paths we don't manage.
|
|
135
|
+
for (const p of parsed.paths) {
|
|
136
|
+
if (MANAGED_FILES.has(p))
|
|
137
|
+
continue;
|
|
138
|
+
const abs = path.join(cwd, p);
|
|
139
|
+
if (fs.existsSync(abs)) {
|
|
140
|
+
filesSkippedExisting.push(p);
|
|
141
|
+
console.log(` ${SKIP} exists ${DIM(p)}`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!dryRun) {
|
|
145
|
+
await fsAsync.mkdir(path.dirname(abs), { recursive: true });
|
|
146
|
+
await fsAsync.writeFile(abs, '', 'utf8');
|
|
147
|
+
}
|
|
148
|
+
filesCreated.push(p);
|
|
149
|
+
console.log(` ${PASS} touch ${DIM(p)}`);
|
|
150
|
+
}
|
|
151
|
+
// 3) go.mod — never overwrite.
|
|
152
|
+
const goModAbs = path.join(cwd, 'go.mod');
|
|
153
|
+
if (fs.existsSync(goModAbs)) {
|
|
154
|
+
filesSkippedExisting.push('go.mod');
|
|
155
|
+
console.log(` ${SKIP} exists ${DIM('go.mod (preserved)')}`);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
if (!dryRun)
|
|
159
|
+
await fsAsync.writeFile(goModAbs, buildGoMod(moduleName), 'utf8');
|
|
160
|
+
filesCreated.push('go.mod');
|
|
161
|
+
console.log(` ${PASS} write ${DIM(`go.mod (module ${moduleName}, go 1.22)`)}`);
|
|
162
|
+
}
|
|
163
|
+
// 4) main.go — only if the spec doesn't push us to a cmd/<name>/ layout.
|
|
164
|
+
const cmdOnly = specHasCmdMainOnly(parsed.paths);
|
|
165
|
+
if (!cmdOnly) {
|
|
166
|
+
const mainAbs = path.join(cwd, 'main.go');
|
|
167
|
+
if (fs.existsSync(mainAbs)) {
|
|
168
|
+
filesSkippedExisting.push('main.go');
|
|
169
|
+
console.log(` ${SKIP} exists ${DIM('main.go (preserved)')}`);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
if (!dryRun)
|
|
173
|
+
await fsAsync.writeFile(mainAbs, buildMainGo(), 'utf8');
|
|
174
|
+
filesCreated.push('main.go');
|
|
175
|
+
console.log(` ${PASS} write ${DIM('main.go (package main + Hello)')}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// 5) main_test.go — only when we also wrote main.go (same cmd-only guard).
|
|
179
|
+
if (!cmdOnly) {
|
|
180
|
+
const testAbs = path.join(cwd, 'main_test.go');
|
|
181
|
+
if (fs.existsSync(testAbs)) {
|
|
182
|
+
filesSkippedExisting.push('main_test.go');
|
|
183
|
+
console.log(` ${SKIP} exists ${DIM('main_test.go (preserved)')}`);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
if (!dryRun)
|
|
187
|
+
await fsAsync.writeFile(testAbs, buildMainTestGo(), 'utf8');
|
|
188
|
+
filesCreated.push('main_test.go');
|
|
189
|
+
console.log(` ${PASS} write ${DIM('main_test.go (TestSmoke stub)')}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// 6) .gitignore — idempotent augmentation.
|
|
193
|
+
const giAbs = path.join(cwd, '.gitignore');
|
|
194
|
+
const existing = fs.existsSync(giAbs) ? await fsAsync.readFile(giAbs, 'utf8') : null;
|
|
195
|
+
const augmented = augmentGitignore(existing);
|
|
196
|
+
if (existing === null) {
|
|
197
|
+
if (!dryRun)
|
|
198
|
+
await fsAsync.writeFile(giAbs, augmented, 'utf8');
|
|
199
|
+
filesCreated.push('.gitignore');
|
|
200
|
+
console.log(` ${PASS} write ${DIM('.gitignore (vendor/, *.exe, *.test)')}`);
|
|
201
|
+
}
|
|
202
|
+
else if (augmented !== existing) {
|
|
203
|
+
if (!dryRun)
|
|
204
|
+
await fsAsync.writeFile(giAbs, augmented, 'utf8');
|
|
205
|
+
// Treat as "augmented" — not in skipped-existing (we modified it) and
|
|
206
|
+
// not in filesCreated (we didn't create a new file). For now we count
|
|
207
|
+
// it as filesCreated since the user sees a write. Tests assert
|
|
208
|
+
// idempotence on disk content, not on this return shape.
|
|
209
|
+
filesCreated.push('.gitignore');
|
|
210
|
+
console.log(` ${PASS} augment ${DIM('.gitignore (added Go ignores)')}`);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
filesSkippedExisting.push('.gitignore');
|
|
214
|
+
console.log(` ${SKIP} exists ${DIM('.gitignore (Go entries already present)')}`);
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
filesCreated,
|
|
218
|
+
dirsCreated,
|
|
219
|
+
filesSkippedExisting,
|
|
220
|
+
// Node-shape fields — Go scaffolder doesn't touch package.json/tsconfig.
|
|
221
|
+
packageJsonAction: 'skipped-exists',
|
|
222
|
+
tsconfigAction: 'skipped-no-ts',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=go.js.map
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/** Supported `--stack` values. v7.
|
|
2
|
-
export type Stack = 'node' | 'python' | 'fastapi';
|
|
1
|
+
/** Supported `--stack` values. v7.6 adds 'go'; v7.7+ will add 'rust'. */
|
|
2
|
+
export type Stack = 'node' | 'python' | 'fastapi' | 'go';
|
|
3
3
|
/**
|
|
4
4
|
* Stacks we can DETECT but cannot scaffold yet. Detection still warns +
|
|
5
|
-
* exits 3 so the operator gets a clear "v7.
|
|
5
|
+
* exits 3 so the operator gets a clear "v7.7" diagnostic instead of a
|
|
6
6
|
* silent fallback to Node, which would generate a wrong-language skeleton.
|
|
7
7
|
*/
|
|
8
|
-
export type UnsupportedStack = '
|
|
8
|
+
export type UnsupportedStack = 'rust' | 'ruby';
|
|
9
9
|
export interface ParsedFiles {
|
|
10
10
|
/** Raw paths extracted from the `## Files` section bullets. */
|
|
11
11
|
paths: string[];
|
|
@@ -2,7 +2,7 @@ import { buildStarterPackageJson } from './scaffold/node.ts';
|
|
|
2
2
|
import type { ParsedFiles, ScaffoldOptions, ScaffoldResult, Stack, UnsupportedStack } from './scaffold/types.ts';
|
|
3
3
|
export { buildStarterPackageJson };
|
|
4
4
|
export type { ScaffoldOptions, ScaffoldResult, ParsedFiles, Stack };
|
|
5
|
-
/** Valid `--stack` argument values. v7.
|
|
5
|
+
/** Valid `--stack` argument values. v7.6 adds 'go'; v7.7+ will add 'rust'. */
|
|
6
6
|
export declare const SUPPORTED_STACKS: readonly Stack[];
|
|
7
7
|
/** Stacks we DETECT-but-don't-support yet. Mapped to spec exit-3 messages. */
|
|
8
8
|
export declare const UNSUPPORTED_STACK_FILES: Record<UnsupportedStack, string>;
|
package/dist/src/cli/scaffold.js
CHANGED
|
@@ -33,17 +33,17 @@ import * as fsAsync from 'node:fs/promises';
|
|
|
33
33
|
import * as path from 'node:path';
|
|
34
34
|
import { scaffoldNode, buildStarterPackageJson } from "./scaffold/node.js";
|
|
35
35
|
import { scaffoldPython } from "./scaffold/python.js";
|
|
36
|
+
import { scaffoldGo } from "./scaffold/go.js";
|
|
36
37
|
const BOLD = (t) => `\x1b[1m${t}\x1b[0m`;
|
|
37
38
|
const DIM = (t) => `\x1b[2m${t}\x1b[0m`;
|
|
38
39
|
// Re-export types + the legacy buildStarterPackageJson so `src/index.ts` and
|
|
39
40
|
// the existing tests/scaffold.test.ts (which imports from this module) keep
|
|
40
41
|
// compiling without changes.
|
|
41
42
|
export { buildStarterPackageJson };
|
|
42
|
-
/** Valid `--stack` argument values. v7.
|
|
43
|
-
export const SUPPORTED_STACKS = ['node', 'python', 'fastapi'];
|
|
43
|
+
/** Valid `--stack` argument values. v7.6 adds 'go'; v7.7+ will add 'rust'. */
|
|
44
|
+
export const SUPPORTED_STACKS = ['node', 'python', 'fastapi', 'go'];
|
|
44
45
|
/** Stacks we DETECT-but-don't-support yet. Mapped to spec exit-3 messages. */
|
|
45
46
|
export const UNSUPPORTED_STACK_FILES = {
|
|
46
|
-
go: 'go.mod',
|
|
47
47
|
rust: 'Cargo.toml',
|
|
48
48
|
ruby: 'Gemfile',
|
|
49
49
|
};
|
|
@@ -215,8 +215,23 @@ export function detectStack(parsed, explicit) {
|
|
|
215
215
|
const hasFastapiMention = parsed.packageHints.stackHint === 'fastapi';
|
|
216
216
|
const hasPythonMarker = has('pyproject.toml') || has('requirements.txt');
|
|
217
217
|
const hasNodeMarker = has('package.json');
|
|
218
|
-
//
|
|
219
|
-
|
|
218
|
+
// v7.6 — Go signal: `go.mod` OR a top-level / cmd-shaped `main.go`.
|
|
219
|
+
const hasGoMod = has('go.mod');
|
|
220
|
+
const hasMainGo = paths.some(p => p === 'main.go' || /^cmd\/[^/]+\/main\.go$/.test(p));
|
|
221
|
+
const hasGoMarker = hasGoMod || hasMainGo;
|
|
222
|
+
// v7.6 — Polyglot detection scans ALL supported stack signals at once.
|
|
223
|
+
// We collect each present stack and exit 3 if more than one supported
|
|
224
|
+
// stack is detected (e.g. Node + Go, Python + Go, Node + Python + Go).
|
|
225
|
+
// FastAPI is collapsed into Python for the polyglot count (they share
|
|
226
|
+
// pyproject.toml — not a real conflict).
|
|
227
|
+
const supportedSignals = [];
|
|
228
|
+
if (hasNodeMarker)
|
|
229
|
+
supportedSignals.push('node');
|
|
230
|
+
if (hasPythonMarker)
|
|
231
|
+
supportedSignals.push('python');
|
|
232
|
+
if (hasGoMarker)
|
|
233
|
+
supportedSignals.push('go');
|
|
234
|
+
if (supportedSignals.length > 1) {
|
|
220
235
|
return {
|
|
221
236
|
kind: 'polyglot',
|
|
222
237
|
message: 'polyglot spec — pass --stack to disambiguate',
|
|
@@ -235,20 +250,23 @@ export function detectStack(parsed, explicit) {
|
|
|
235
250
|
// Step 3: Python.
|
|
236
251
|
if (hasPythonMarker)
|
|
237
252
|
return { kind: 'resolved', stack: 'python' };
|
|
238
|
-
// Step 4:
|
|
253
|
+
// Step 4: Go (v7.6).
|
|
254
|
+
if (hasGoMarker)
|
|
255
|
+
return { kind: 'resolved', stack: 'go' };
|
|
256
|
+
// Step 5: Node.
|
|
239
257
|
if (hasNodeMarker)
|
|
240
258
|
return { kind: 'resolved', stack: 'node' };
|
|
241
|
-
// Step
|
|
259
|
+
// Step 6: detected-but-unsupported (codex W2). Rust + Ruby still here.
|
|
242
260
|
for (const [stack, file] of Object.entries(UNSUPPORTED_STACK_FILES)) {
|
|
243
261
|
if (has(file)) {
|
|
244
262
|
return {
|
|
245
263
|
kind: 'unsupported',
|
|
246
264
|
stack,
|
|
247
|
-
message: `${stack} detected but not supported until v7.
|
|
265
|
+
message: `${stack} detected but not supported until v7.7`,
|
|
248
266
|
};
|
|
249
267
|
}
|
|
250
268
|
}
|
|
251
|
-
// Step
|
|
269
|
+
// Step 7: fallback — Node ESM (preserves v7.2.0 default for ambiguous
|
|
252
270
|
// specs that listed only paths with no root-marker file).
|
|
253
271
|
return { kind: 'resolved', stack: 'node' };
|
|
254
272
|
}
|
|
@@ -262,16 +280,17 @@ export function printStackList() {
|
|
|
262
280
|
console.log(' node Node 22 ESM (package.json + tsconfig.json)');
|
|
263
281
|
console.log(' python Python 3.11+ (pyproject.toml + hatchling + pytest)');
|
|
264
282
|
console.log(' fastapi Python + FastAPI (auto-includes fastapi + uvicorn[standard])');
|
|
283
|
+
console.log(' go Go 1.22 (go.mod + main.go + main_test.go)');
|
|
265
284
|
console.log('');
|
|
266
285
|
console.log(BOLD('Auto-detected from `## Files`:'));
|
|
267
286
|
console.log(' node when `package.json` is listed');
|
|
268
287
|
console.log(' python when `pyproject.toml` or `requirements.txt` is listed');
|
|
269
288
|
console.log(' fastapi when `main.py` is listed AND a bullet mentions `fastapi`');
|
|
289
|
+
console.log(' go when `go.mod` or `main.go` is listed');
|
|
270
290
|
console.log('');
|
|
271
291
|
console.log(BOLD('Recognized-but-unsupported (exit 3):'));
|
|
272
|
-
console.log('
|
|
273
|
-
console.log('
|
|
274
|
-
console.log(' ruby v7.5+ (would detect via Gemfile)');
|
|
292
|
+
console.log(' rust v7.7 (would detect via Cargo.toml)');
|
|
293
|
+
console.log(' ruby v7.7+ (would detect via Gemfile)');
|
|
275
294
|
console.log('');
|
|
276
295
|
}
|
|
277
296
|
export async function runScaffold(opts) {
|
|
@@ -305,34 +324,47 @@ export async function runScaffold(opts) {
|
|
|
305
324
|
}
|
|
306
325
|
const stack = detection.stack;
|
|
307
326
|
console.log(`\n${BOLD('[scaffold]')} ${DIM(specAbs)} ${DIM(`(stack: ${stack})`)}\n`);
|
|
308
|
-
// codex W5 — when --stack
|
|
309
|
-
//
|
|
310
|
-
//
|
|
327
|
+
// codex W5 — when an explicit --stack is passed against a polyglot spec,
|
|
328
|
+
// strip the OTHER stack's marker files so the chosen scaffolder doesn't
|
|
329
|
+
// touch them as empty placeholders. v7.6 extends this to Go.
|
|
311
330
|
let ignoredOtherStackFiles;
|
|
312
331
|
let parsedForStack = parsed;
|
|
332
|
+
const NODE_FILES = new Set(['package.json', 'tsconfig.json']);
|
|
333
|
+
const PYTHON_FILES = new Set(['pyproject.toml', 'requirements.txt']);
|
|
334
|
+
const GO_FILES = new Set(['go.mod']);
|
|
313
335
|
if (opts.stack && (stack === 'python' || stack === 'fastapi')) {
|
|
314
|
-
const
|
|
315
|
-
const ignored = parsed.paths.filter(p =>
|
|
336
|
+
const toIgnore = new Set([...NODE_FILES, ...GO_FILES]);
|
|
337
|
+
const ignored = parsed.paths.filter(p => toIgnore.has(p));
|
|
316
338
|
if (ignored.length > 0) {
|
|
317
339
|
ignoredOtherStackFiles = ignored;
|
|
318
|
-
console.log(` ${DIM(`! ignoring
|
|
340
|
+
console.log(` ${DIM(`! ignoring non-Python files (--stack ${stack}): ${ignored.join(', ')}`)}`);
|
|
319
341
|
parsedForStack = {
|
|
320
342
|
...parsed,
|
|
321
|
-
paths: parsed.paths.filter(p => !
|
|
343
|
+
paths: parsed.paths.filter(p => !toIgnore.has(p)),
|
|
322
344
|
};
|
|
323
345
|
}
|
|
324
346
|
}
|
|
325
347
|
else if (opts.stack === 'node') {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
348
|
+
const toIgnore = new Set([...PYTHON_FILES, ...GO_FILES]);
|
|
349
|
+
const ignored = parsed.paths.filter(p => toIgnore.has(p));
|
|
350
|
+
if (ignored.length > 0) {
|
|
351
|
+
ignoredOtherStackFiles = ignored;
|
|
352
|
+
console.log(` ${DIM(`! ignoring non-Node files (--stack node): ${ignored.join(', ')}`)}`);
|
|
353
|
+
parsedForStack = {
|
|
354
|
+
...parsed,
|
|
355
|
+
paths: parsed.paths.filter(p => !toIgnore.has(p)),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else if (opts.stack === 'go') {
|
|
360
|
+
const toIgnore = new Set([...NODE_FILES, ...PYTHON_FILES]);
|
|
361
|
+
const ignored = parsed.paths.filter(p => toIgnore.has(p));
|
|
330
362
|
if (ignored.length > 0) {
|
|
331
363
|
ignoredOtherStackFiles = ignored;
|
|
332
|
-
console.log(` ${DIM(`! ignoring
|
|
364
|
+
console.log(` ${DIM(`! ignoring non-Go files (--stack go): ${ignored.join(', ')}`)}`);
|
|
333
365
|
parsedForStack = {
|
|
334
366
|
...parsed,
|
|
335
|
-
paths: parsed.paths.filter(p => !
|
|
367
|
+
paths: parsed.paths.filter(p => !toIgnore.has(p)),
|
|
336
368
|
};
|
|
337
369
|
}
|
|
338
370
|
}
|
|
@@ -344,6 +376,9 @@ export async function runScaffold(opts) {
|
|
|
344
376
|
else if (stack === 'fastapi') {
|
|
345
377
|
result = await scaffoldPython(ctx, { isFastapi: true });
|
|
346
378
|
}
|
|
379
|
+
else if (stack === 'go') {
|
|
380
|
+
result = await scaffoldGo(ctx);
|
|
381
|
+
}
|
|
347
382
|
else {
|
|
348
383
|
result = await scaffoldNode(ctx);
|
|
349
384
|
}
|