@codedology/code-forge 0.1.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 +64 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/bin/code-forge.mjs +145 -0
- package/docs/README.md +37 -0
- package/docs/reference/blocks.json +288 -0
- package/docs/reference/config.md +162 -0
- package/examples/first-use/README.md +11 -0
- package/examples/first-use/clamp.acceptance.yml +8 -0
- package/examples/first-use/clamp.brief.md +16 -0
- package/examples/first-use/coder/src/math.mjs +31 -0
- package/examples/first-use/coder/test/clamp.test.mjs +26 -0
- package/examples/node-lib/.env.example +3 -0
- package/examples/node-lib/.gitattributes +3 -0
- package/examples/node-lib/.github/workflows/test.yml +11 -0
- package/examples/node-lib/CHANGELOG.md +13 -0
- package/examples/node-lib/README.md +17 -0
- package/examples/node-lib/gitignore +3 -0
- package/examples/node-lib/package.json +19 -0
- package/examples/node-lib/src/list.mjs +23 -0
- package/examples/node-lib/src/math.mjs +21 -0
- package/examples/node-lib/src/strings.mjs +34 -0
- package/examples/node-lib/test/list.test.mjs +12 -0
- package/examples/node-lib/test/math.test.mjs +13 -0
- package/examples/node-lib/test/package.test.mjs +18 -0
- package/examples/node-lib/test/strings.test.mjs +28 -0
- package/package.json +52 -0
- package/schema/code-forge.schema.json +331 -0
- package/schema/facts.schema.json +28 -0
- package/scripts/first-use.mjs +517 -0
- package/scripts/gen-config-doc.mjs +241 -0
- package/skill/SKILL.md +79 -0
- package/skill/references/adapters/claude-code.md +19 -0
- package/skill/references/adapters/solo.md +34 -0
- package/skill/references/adapters/subprocess.md +15 -0
- package/skill/references/code.md +64 -0
- package/skill/references/continuity.md +31 -0
- package/skill/references/decisions.md +50 -0
- package/skill/references/degraded.md +40 -0
- package/skill/references/harden.md +26 -0
- package/skill/references/ledger.md +23 -0
- package/skill/references/plan.md +43 -0
- package/skill/references/proof.md +34 -0
- package/skill/references/review.md +42 -0
- package/skill/references/rules-core.md +72 -0
- package/skill/references/security.md +29 -0
- package/skill/scripts/forge +40 -0
- package/skill/templates/block-record.md +19 -0
- package/skill/templates/facts-sheet.md +22 -0
- package/skill/templates/plan.md +53 -0
- package/src/cli/author.mjs +87 -0
- package/src/cli/block.mjs +262 -0
- package/src/cli/doctor.mjs +74 -0
- package/src/cli/facts.mjs +124 -0
- package/src/cli/gates.mjs +280 -0
- package/src/cli/help.mjs +48 -0
- package/src/cli/init.mjs +14 -0
- package/src/cli/jev.mjs +175 -0
- package/src/cli/keys.mjs +170 -0
- package/src/cli/ledger.mjs +160 -0
- package/src/cli/list.mjs +68 -0
- package/src/cli/models.mjs +131 -0
- package/src/cli/plan.mjs +70 -0
- package/src/cli/proof.mjs +316 -0
- package/src/cli/remove.mjs +76 -0
- package/src/cli/report.mjs +73 -0
- package/src/cli/resolve.mjs +79 -0
- package/src/cli/review-file.mjs +152 -0
- package/src/cli/run.mjs +159 -0
- package/src/cli/s2.mjs +93 -0
- package/src/cli/spawn.mjs +104 -0
- package/src/cli/upgrade.mjs +103 -0
- package/src/cli/validate.mjs +108 -0
- package/src/cli/version.mjs +18 -0
- package/src/cli/worker.mjs +76 -0
- package/src/config/defaults/anthropic.mjs +18 -0
- package/src/config/defaults/index.mjs +24 -0
- package/src/config/defaults/openai.mjs +16 -0
- package/src/config/defaults/xai.mjs +18 -0
- package/src/config/known-ids.mjs +171 -0
- package/src/config/load.mjs +135 -0
- package/src/config/migrate.mjs +46 -0
- package/src/config/refresh.mjs +276 -0
- package/src/config/schema-compile.mjs +211 -0
- package/src/config/schema-paths.mjs +84 -0
- package/src/config/secret-patterns.mjs +85 -0
- package/src/config/validate.mjs +694 -0
- package/src/decide/author-loop.mjs +90 -0
- package/src/decide/escalation.mjs +262 -0
- package/src/decide/fallback-rules.mjs +172 -0
- package/src/decide/jev-client.mjs +216 -0
- package/src/decide/questions.mjs +161 -0
- package/src/decide/thresholds.mjs +199 -0
- package/src/doctor/index.mjs +105 -0
- package/src/doctor/local.mjs +162 -0
- package/src/doctor/probes.mjs +358 -0
- package/src/doctor/rows.mjs +51 -0
- package/src/doctor/worker-probe.mjs +125 -0
- package/src/engines/alias-map.mjs +78 -0
- package/src/engines/builders/claude.mjs +224 -0
- package/src/engines/builders/codex.mjs +242 -0
- package/src/engines/builders/grok.mjs +132 -0
- package/src/engines/builders/index.mjs +35 -0
- package/src/engines/builders/validate-params.mjs +34 -0
- package/src/engines/codex-home.mjs +230 -0
- package/src/engines/isolation.mjs +84 -0
- package/src/engines/probe.mjs +116 -0
- package/src/engines/provider-cli.mjs +24 -0
- package/src/engines/render-rules.mjs +41 -0
- package/src/engines/sentinel.mjs +133 -0
- package/src/engines/usage-parse.mjs +149 -0
- package/src/gates/acceptance.mjs +95 -0
- package/src/gates/detect.mjs +187 -0
- package/src/gates/invariants.mjs +43 -0
- package/src/gates/run.mjs +72 -0
- package/src/gates/safe-edit.mjs +266 -0
- package/src/gates/scope.mjs +154 -0
- package/src/gates/secret-scan.mjs +79 -0
- package/src/gates/transcript-grep.mjs +27 -0
- package/src/install/agent-env.mjs +76 -0
- package/src/install/detect.mjs +121 -0
- package/src/install/harnesses.mjs +131 -0
- package/src/install/link.mjs +463 -0
- package/src/install/wizard/answers.mjs +389 -0
- package/src/install/wizard/flags.mjs +248 -0
- package/src/install/wizard/project-file.mjs +128 -0
- package/src/install/wizard/run.mjs +366 -0
- package/src/install/wizard/steps.mjs +201 -0
- package/src/keys/backends/constants.mjs +7 -0
- package/src/keys/backends/file.mjs +99 -0
- package/src/keys/backends/keychain.mjs +81 -0
- package/src/keys/backends/security-cli.mjs +105 -0
- package/src/keys/onepassword.mjs +46 -0
- package/src/keys/store.mjs +347 -0
- package/src/ledger/calibration.mjs +109 -0
- package/src/ledger/export.mjs +52 -0
- package/src/ledger/outcome.mjs +397 -0
- package/src/ledger/paths.mjs +30 -0
- package/src/ledger/prices.mjs +49 -0
- package/src/ledger/report.mjs +250 -0
- package/src/ledger/write.mjs +279 -0
- package/src/proof/export.mjs +358 -0
- package/src/proof/lock.mjs +138 -0
- package/src/proof/red-green.mjs +519 -0
- package/src/proof/red-parse.mjs +103 -0
- package/src/proof/tiers.mjs +62 -0
- package/src/review/budget.mjs +241 -0
- package/src/review/context.mjs +156 -0
- package/src/review/engine.mjs +264 -0
- package/src/review/finding.schema.json +47 -0
- package/src/review/fixloop.mjs +467 -0
- package/src/review/gate-check.mjs +249 -0
- package/src/review/lenses/A.md +22 -0
- package/src/review/lenses/B.md +23 -0
- package/src/review/lenses/full.md +23 -0
- package/src/review/lenses/judge.md +21 -0
- package/src/review/lenses/quick.md +21 -0
- package/src/review/lenses/recheck.md +23 -0
- package/src/review/packet.mjs +355 -0
- package/src/review/triage.mjs +149 -0
- package/src/review/validate-review.mjs +70 -0
- package/src/session/author.mjs +228 -0
- package/src/session/facts.mjs +855 -0
- package/src/session/plan-check.mjs +294 -0
- package/src/session/s2.mjs +164 -0
- package/src/session/spawn.mjs +610 -0
- package/src/state/block.mjs +289 -0
- package/src/state/cli-args.mjs +67 -0
- package/src/state/paths.mjs +92 -0
- package/src/state/registry.mjs +189 -0
- package/src/state/run.mjs +266 -0
- package/src/state/signer.mjs +132 -0
- package/src/util/exec.mjs +315 -0
- package/src/util/forbidden.mjs +787 -0
- package/src/util/git.mjs +381 -0
- package/src/util/log.mjs +100 -0
- package/src/util/reaper.mjs +300 -0
- package/src/util/redact.mjs +212 -0
- package/src/util/tmp.mjs +409 -0
- package/src/worker/engine.mjs +338 -0
- package/src/worker/loop.mjs +392 -0
- package/src/worker/queue.mjs +186 -0
- package/src/worker/review-state.mjs +145 -0
- package/src/worker/ticket.mjs +195 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wizard's answers (plan §2, block B13a): what each of the nine steps proposes, from the
|
|
3
|
+
* project on disk and the machine, then an existing `.code-forge.yml` on top (so a re-run
|
|
4
|
+
* proposes what is already there — the idempotent diff), then the flags on top of that.
|
|
5
|
+
* `sources[key]` records where each answer came from: `default`, `existing` or `flag`.
|
|
6
|
+
* Pure apart from the reads in {@link gatherContext}.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { defaultsForProvider } from '../../config/defaults/index.mjs';
|
|
12
|
+
import { detectGates } from '../../gates/detect.mjs';
|
|
13
|
+
import { detectAgentEnv } from '../agent-env.mjs';
|
|
14
|
+
import { detectAll } from '../detect.mjs';
|
|
15
|
+
import { ANSWER_FLAGS, PROVIDERS } from './flags.mjs';
|
|
16
|
+
|
|
17
|
+
/** Harnesses that expose a subagent tool (§5.1: Claude Code's `Agent`, [A13]). */
|
|
18
|
+
export const SUBAGENT_HARNESSES = Object.freeze(['claude']);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `copy_untracked` proposed per stack (§2 step 8, C5). Laravel is any PHP project (`composer.json`)
|
|
22
|
+
* with an `artisan` file, whatever test runner it uses; a PHP project without one gets the plain
|
|
23
|
+
* `.env`.
|
|
24
|
+
*/
|
|
25
|
+
export const COPY_UNTRACKED = Object.freeze({
|
|
26
|
+
laravel: ['.env', '.env.testing'],
|
|
27
|
+
php: ['.env'],
|
|
28
|
+
node: ['.env', '.env.test'],
|
|
29
|
+
python: ['.env'],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** `link_dirs` per stack: the dependency directories an export links instead of copying. */
|
|
33
|
+
const LINK_DIRS = Object.freeze({
|
|
34
|
+
laravel: ['vendor', 'node_modules'],
|
|
35
|
+
php: ['vendor'],
|
|
36
|
+
node: ['node_modules'],
|
|
37
|
+
python: ['.venv'],
|
|
38
|
+
rust: ['target'],
|
|
39
|
+
go: [],
|
|
40
|
+
unknown: ['vendor', 'node_modules'],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** High-risk paths per stack (v1 §2 step 8: migrations, policies, money, auth/middleware, webhooks). */
|
|
44
|
+
const HIGH_PATHS = Object.freeze({
|
|
45
|
+
laravel: ['database/migrations/**', 'app/Policies/**', 'app/Http/Middleware/**', '**/*Money*', '**/*Webhook*'],
|
|
46
|
+
php: ['**/*Money*', '**/*Webhook*', '**/Auth/**'],
|
|
47
|
+
node: ['**/migrations/**', '**/auth/**', '**/*money*', '**/*webhook*'],
|
|
48
|
+
python: ['**/migrations/**', '**/auth/**', '**/*money*', '**/*webhook*'],
|
|
49
|
+
rust: ['**/auth/**', '**/*money*'],
|
|
50
|
+
go: ['**/auth/**', '**/*money*'],
|
|
51
|
+
unknown: [],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The proof profile is decided from the project, not from the gate detector's stack name alone:
|
|
56
|
+
* `detectGates` (B5) names only `php-pest`, so a Laravel app on PHPUnit reaches here as `node`
|
|
57
|
+
* (its Vite `package.json`) or `unknown` — `composer.json` + `artisan` is Laravel either way.
|
|
58
|
+
* @param {string} stack - `detectGates().stack`
|
|
59
|
+
* @param {string} cwd
|
|
60
|
+
* @returns {string} the proof profile: `laravel`, `php`, `node`, `python`, `rust`, `go` or `unknown`.
|
|
61
|
+
*/
|
|
62
|
+
export function proofProfile(stack, cwd) {
|
|
63
|
+
const composer = existsSync(path.join(cwd, 'composer.json'));
|
|
64
|
+
const php = stack.startsWith('php') || composer;
|
|
65
|
+
if (php && existsSync(path.join(cwd, 'artisan'))) return 'laravel';
|
|
66
|
+
if (stack.startsWith('php')) return 'php';
|
|
67
|
+
// a detected non-PHP stack keeps its word; only an undetected project falls back on composer.json
|
|
68
|
+
if (stack !== 'unknown' && Object.hasOwn(LINK_DIRS, stack)) return stack;
|
|
69
|
+
return composer ? 'php' : 'unknown';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The provider a level runs on: its own `provider` override (R5), else the default provider.
|
|
74
|
+
* @param {{provider: string, levels: Record<string, any>}} values
|
|
75
|
+
* @param {'L0'|'L1'|'L2'|'L3'} level
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
export function effectiveProvider(values, level) {
|
|
79
|
+
const own = values.levels?.[level]?.provider;
|
|
80
|
+
return typeof own === 'string' && own.length > 0 ? own : values.provider;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Consensus review (§4.4) needs three families: reviewer 1 (the effective L2), reviewer 2
|
|
85
|
+
* (`second_provider`) and a judge (the effective L3) that shares a provider with neither
|
|
86
|
+
* (validator rule 7, `judge-family-collision`). With three providers in the catalog the judge's
|
|
87
|
+
* provider is therefore determined once both reviewers are known.
|
|
88
|
+
* @param {{provider: string, levels: Record<string, any>, multimodel: boolean, second_provider: string|null}} values
|
|
89
|
+
* @returns {{judge: string, reviewers: string[], third: string|null} | null} the collision, with
|
|
90
|
+
* the one provider left for the judge; null when multimodel is off, the second reviewer is not
|
|
91
|
+
* chosen yet, or the judge already differs from both reviewers.
|
|
92
|
+
*/
|
|
93
|
+
export function judgeCollision(values) {
|
|
94
|
+
if (values.multimodel !== true || !values.second_provider) return null;
|
|
95
|
+
const reviewers = [effectiveProvider(values, 'L2'), values.second_provider];
|
|
96
|
+
const judge = effectiveProvider(values, 'L3');
|
|
97
|
+
if (!reviewers.includes(judge)) return null;
|
|
98
|
+
return { judge, reviewers, third: PROVIDERS.find((p) => !reviewers.includes(p)) ?? null };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The judge the wizard proposes: `provider`'s shipped default L3, pinned to that provider.
|
|
103
|
+
* @param {string} provider
|
|
104
|
+
* @returns {{model: string, effort?: string, provider: string}}
|
|
105
|
+
*/
|
|
106
|
+
export function proposeJudge(provider) {
|
|
107
|
+
const levels = /** @type {Record<string, any>} */ (defaultsForProvider(provider)?.levels ?? {});
|
|
108
|
+
return { ...levels.L3, provider };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Step 8's proposal for a stack.
|
|
113
|
+
* @param {string} profile
|
|
114
|
+
* @returns {{high: string[], isolation: 'export', link_dirs: string[], copy_untracked: string[]}}
|
|
115
|
+
*/
|
|
116
|
+
export function proposeProof(profile) {
|
|
117
|
+
return {
|
|
118
|
+
high: [...(HIGH_PATHS[profile] ?? [])],
|
|
119
|
+
isolation: 'export',
|
|
120
|
+
link_dirs: [...(LINK_DIRS[profile] ?? LINK_DIRS.unknown)],
|
|
121
|
+
copy_untracked: [...(COPY_UNTRACKED[profile] ?? [])],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Variables the Codex CLI sets in the environment of the commands it runs. Source: the strings of
|
|
127
|
+
* the installed Codex CLI 0.155.1 binary on this machine (read-only `grep`, 2026-09-25), which
|
|
128
|
+
* contains `CODEX_SANDBOX` and `CODEX_SANDBOX_NETWORK_DISABLED`. User-config variables
|
|
129
|
+
* (`CODEX_HOME`, `CODEX_API_KEY`) are deliberately not here.
|
|
130
|
+
*/
|
|
131
|
+
export const CODEX_SESSION_VARS = Object.freeze(['CODEX_SANDBOX', 'CODEX_SANDBOX_NETWORK_DISABLED']);
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The harness the wizard runs under, from the agent env vars. Codex is recognised only by the
|
|
135
|
+
* variables it sets for its own sessions ({@link CODEX_SESSION_VARS}), never by `CODEX_HOME` or
|
|
136
|
+
* `CODEX_API_KEY`, which people set in their normal shells.
|
|
137
|
+
* @param {NodeJS.ProcessEnv} env
|
|
138
|
+
* @returns {{agent: boolean, harness: string|null}}
|
|
139
|
+
*/
|
|
140
|
+
export function currentHarness(env) {
|
|
141
|
+
const { source } = detectAgentEnv(env);
|
|
142
|
+
if (source === 'CLAUDECODE' || source === 'CLAUDE_CODE') return { agent: true, harness: 'claude' };
|
|
143
|
+
if (source === 'CURSOR_AGENT') return { agent: true, harness: 'cursor' };
|
|
144
|
+
if (CODEX_SESSION_VARS.some((k) => typeof env[k] === 'string' && env[k].length > 0)) return { agent: true, harness: 'codex' };
|
|
145
|
+
if (source === 'AI_AGENT') return { agent: true, harness: 'unknown' };
|
|
146
|
+
return { agent: false, harness: null };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Solo is present when `SOLO_MCP_PATH` is set or `~/.claude.json` declares an MCP server whose
|
|
151
|
+
* name contains `solo`.
|
|
152
|
+
* @param {NodeJS.ProcessEnv} env @param {string} home
|
|
153
|
+
* @returns {boolean}
|
|
154
|
+
*/
|
|
155
|
+
export function soloPresent(env, home) {
|
|
156
|
+
if (typeof env.SOLO_MCP_PATH === 'string' && env.SOLO_MCP_PATH.length > 0) return true;
|
|
157
|
+
try {
|
|
158
|
+
const parsed = JSON.parse(readFileSync(path.join(home, '.claude.json'), 'utf8'));
|
|
159
|
+
const servers = parsed?.mcpServers && typeof parsed.mcpServers === 'object' ? Object.keys(parsed.mcpServers) : [];
|
|
160
|
+
return servers.some((name) => /solo/i.test(name));
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** @param {string} dir @returns {boolean} `dir` or an ancestor holds `.git`. */
|
|
167
|
+
function insideGitRepo(dir) {
|
|
168
|
+
let cur = path.resolve(dir);
|
|
169
|
+
for (;;) {
|
|
170
|
+
if (existsSync(path.join(cur, '.git'))) return true;
|
|
171
|
+
const parent = path.dirname(cur);
|
|
172
|
+
if (parent === cur) return false;
|
|
173
|
+
cur = parent;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** @param {string} name @returns {string} */
|
|
178
|
+
function slugify(name) {
|
|
179
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
180
|
+
return slug.length > 0 ? slug : 'project';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Project name: `package.json` name, else the last segment of `composer.json`'s name, else the
|
|
185
|
+
* directory name.
|
|
186
|
+
* @param {string} cwd
|
|
187
|
+
* @returns {{name: string, slug: string}}
|
|
188
|
+
*/
|
|
189
|
+
export function projectIdentity(cwd) {
|
|
190
|
+
for (const [file, pick] of /** @type {Array<[string, (v: any) => unknown]>} */ ([
|
|
191
|
+
['package.json', (v) => v?.name],
|
|
192
|
+
['composer.json', (v) => (typeof v?.name === 'string' ? v.name.split('/').pop() : undefined)],
|
|
193
|
+
])) {
|
|
194
|
+
try {
|
|
195
|
+
const name = pick(JSON.parse(readFileSync(path.join(cwd, file), 'utf8')));
|
|
196
|
+
if (typeof name === 'string' && name.length > 0) {
|
|
197
|
+
const bare = name.startsWith('@') ? name.split('/').pop() : name;
|
|
198
|
+
return { name: bare, slug: slugify(bare) };
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
// absent or not JSON: next source
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const base = path.basename(path.resolve(cwd));
|
|
205
|
+
return { name: base, slug: slugify(base) };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @typedef {object} WizardContext
|
|
210
|
+
* @property {string} cwd
|
|
211
|
+
* @property {string} home
|
|
212
|
+
* @property {NodeJS.ProcessEnv} env
|
|
213
|
+
* @property {ReturnType<typeof detectGates>} detected
|
|
214
|
+
* @property {string} profile
|
|
215
|
+
* @property {boolean} gitRepo
|
|
216
|
+
* @property {string[]} detectedHarnesses
|
|
217
|
+
* @property {{agent: boolean, harness: string|null}} current
|
|
218
|
+
* @property {boolean} solo
|
|
219
|
+
* @property {{name: string, slug: string}} project
|
|
220
|
+
*/
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @param {{cwd: string, home: string, env: NodeJS.ProcessEnv}} opts
|
|
224
|
+
* @returns {Promise<WizardContext>}
|
|
225
|
+
*/
|
|
226
|
+
export async function gatherContext({ cwd, home, env }) {
|
|
227
|
+
const detected = detectGates(cwd);
|
|
228
|
+
const found = await detectAll({ home, pathEnv: env.PATH ?? '' });
|
|
229
|
+
return {
|
|
230
|
+
cwd,
|
|
231
|
+
home,
|
|
232
|
+
env,
|
|
233
|
+
detected,
|
|
234
|
+
profile: proofProfile(detected.stack, cwd),
|
|
235
|
+
gitRepo: insideGitRepo(cwd),
|
|
236
|
+
detectedHarnesses: found.filter((d) => d.detected).map((d) => d.id),
|
|
237
|
+
current: currentHarness(env),
|
|
238
|
+
solo: soloPresent(env, home),
|
|
239
|
+
project: projectIdentity(cwd),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** @param {unknown} v @returns {any} a deep plain copy */
|
|
244
|
+
function clone(v) {
|
|
245
|
+
return v === undefined ? undefined : JSON.parse(JSON.stringify(v));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** @param {unknown} gate @returns {string[]|null} a schema-valid argv (a `{argv}` descriptor keeps its argv). */
|
|
249
|
+
function gateArgv(gate) {
|
|
250
|
+
if (Array.isArray(gate)) return [...gate];
|
|
251
|
+
if (gate && typeof gate === 'object' && Array.isArray(/** @type {any} */ (gate).argv)) return [.../** @type {any} */ (gate).argv];
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* @typedef {object} Answers
|
|
257
|
+
* @property {'current'|'recommended'} tools
|
|
258
|
+
* @property {string[]} yes_tools
|
|
259
|
+
* @property {string[]} harnesses
|
|
260
|
+
* @property {'project'|'global'} scope
|
|
261
|
+
* @property {'symlink'|'copy'} method
|
|
262
|
+
* @property {string} provider
|
|
263
|
+
* @property {Record<string, any>} levels
|
|
264
|
+
* @property {boolean} refresh_models
|
|
265
|
+
* @property {boolean} multimodel
|
|
266
|
+
* @property {string|null} second_provider
|
|
267
|
+
* @property {{mode: 'auto'} | {mode: 'ref', ref: string} | {mode: 'none'}} jev
|
|
268
|
+
* @property {string} engine
|
|
269
|
+
* @property {number|null} solo_project
|
|
270
|
+
* @property {{test: string[]|null, lint: string[]|null, types: string[]|null, format: string[]|null}} gates
|
|
271
|
+
* @property {{high: string[], isolation: string, link_dirs: string[], copy_untracked: string[]}} proof
|
|
272
|
+
* @property {boolean} doctor
|
|
273
|
+
*/
|
|
274
|
+
|
|
275
|
+
/** The answer keys in step order. */
|
|
276
|
+
export const ANSWER_KEYS = Object.freeze([
|
|
277
|
+
'tools', 'yes_tools', 'harnesses', 'scope', 'method', 'provider', 'levels', 'refresh_models',
|
|
278
|
+
'multimodel', 'second_provider', 'jev', 'engine', 'solo_project', 'gates', 'proof', 'doctor',
|
|
279
|
+
]);
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @param {WizardContext} ctx
|
|
283
|
+
* @param {Record<string, any>|null} existing - the current `.code-forge.yml`, when there is one.
|
|
284
|
+
* @param {Record<string, any>|null} user - the current user-level config (`~/.code-forge/`).
|
|
285
|
+
* @param {Array<{flag: string, value: any}>} given - parsed answer flags.
|
|
286
|
+
* @returns {{values: Answers, sources: Record<string, 'default'|'existing'|'flag'>}}
|
|
287
|
+
*/
|
|
288
|
+
export function resolveAnswers(ctx, existing, user, given) {
|
|
289
|
+
/** @type {Record<string, 'default'|'existing'|'flag'>} */
|
|
290
|
+
const sources = {};
|
|
291
|
+
const ex = existing ?? {};
|
|
292
|
+
/** @type {any} */
|
|
293
|
+
const v = {};
|
|
294
|
+
/** @param {string} key @param {unknown} fromExisting @param {() => unknown} fallback */
|
|
295
|
+
const set = (key, fromExisting, fallback) => {
|
|
296
|
+
if (fromExisting !== undefined && fromExisting !== null) {
|
|
297
|
+
v[key] = clone(fromExisting);
|
|
298
|
+
sources[key] = 'existing';
|
|
299
|
+
} else {
|
|
300
|
+
v[key] = fallback();
|
|
301
|
+
sources[key] = 'default';
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const flagsFor = (/** @type {string} */ key) => given.filter((g) => ANSWER_FLAGS[g.flag] === key);
|
|
305
|
+
const lastFlag = (/** @type {string} */ key) => flagsFor(key).at(-1);
|
|
306
|
+
const override = (/** @type {string} */ key, /** @type {unknown} */ value) => {
|
|
307
|
+
v[key] = value;
|
|
308
|
+
sources[key] = 'flag';
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
set('tools', user?.tools, () => 'current');
|
|
312
|
+
if (lastFlag('tools')) override('tools', lastFlag('tools').value);
|
|
313
|
+
set('yes_tools', undefined, () => []);
|
|
314
|
+
if (flagsFor('yes_tools').length > 0) override('yes_tools', [...new Set(flagsFor('yes_tools').map((g) => g.value))]);
|
|
315
|
+
|
|
316
|
+
set('harnesses', ex.harnesses, () =>
|
|
317
|
+
v.tools === 'current' && ctx.current.harness && ctx.current.harness !== 'unknown' ? [ctx.current.harness] : [...ctx.detectedHarnesses],
|
|
318
|
+
);
|
|
319
|
+
if (lastFlag('harnesses')) override('harnesses', lastFlag('harnesses').value);
|
|
320
|
+
set('scope', undefined, () => (ctx.gitRepo ? 'project' : 'global'));
|
|
321
|
+
if (lastFlag('scope')) override('scope', lastFlag('scope').value);
|
|
322
|
+
set('method', undefined, () => 'symlink');
|
|
323
|
+
if (lastFlag('method')) override('method', lastFlag('method').value);
|
|
324
|
+
|
|
325
|
+
set('provider', ex.provider, () => 'anthropic');
|
|
326
|
+
if (lastFlag('provider')) override('provider', lastFlag('provider').value);
|
|
327
|
+
const providerDefaults = defaultsForProvider(v.provider);
|
|
328
|
+
set('levels', ex.provider === v.provider ? ex.levels : undefined, () => clone(providerDefaults.levels));
|
|
329
|
+
if (flagsFor('levels').length > 0) {
|
|
330
|
+
const levels = clone(v.levels);
|
|
331
|
+
for (const g of flagsFor('levels')) levels[g.value.level] = { ...g.value.spec };
|
|
332
|
+
override('levels', levels);
|
|
333
|
+
}
|
|
334
|
+
set('refresh_models', undefined, () => false);
|
|
335
|
+
if (lastFlag('refresh_models')) override('refresh_models', true);
|
|
336
|
+
|
|
337
|
+
set('multimodel', ex.review?.multimodel, () => false);
|
|
338
|
+
if (lastFlag('multimodel')) override('multimodel', lastFlag('multimodel').value);
|
|
339
|
+
set('second_provider', ex.review?.second_provider, () => null);
|
|
340
|
+
if (lastFlag('second_provider')) override('second_provider', lastFlag('second_provider').value);
|
|
341
|
+
|
|
342
|
+
const exJev = typeof ex.keys?.jev === 'string' ? { mode: 'ref', ref: ex.keys.jev } : existing ? { mode: 'none' } : undefined;
|
|
343
|
+
set('jev', exJev, () => ({ mode: 'auto' }));
|
|
344
|
+
const jevFlag = lastFlag('jev');
|
|
345
|
+
if (jevFlag) {
|
|
346
|
+
if (jevFlag.flag === '--no-jev') override('jev', { mode: 'none' });
|
|
347
|
+
else override('jev', { mode: 'ref', ref: jevFlag.flag === '--jev-env' ? `env:${jevFlag.value}` : jevFlag.value });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
set('engine', ex.engine, () => 'auto');
|
|
351
|
+
if (lastFlag('engine')) override('engine', lastFlag('engine').value);
|
|
352
|
+
set('solo_project', user?.solo?.project_id, () => null);
|
|
353
|
+
if (lastFlag('solo_project')) override('solo_project', lastFlag('solo_project').value);
|
|
354
|
+
|
|
355
|
+
const detectedGates = {
|
|
356
|
+
test: gateArgv(ctx.detected.test),
|
|
357
|
+
lint: gateArgv(ctx.detected.lint),
|
|
358
|
+
types: gateArgv(ctx.detected.types),
|
|
359
|
+
format: gateArgv(ctx.detected.format),
|
|
360
|
+
};
|
|
361
|
+
const exGates = ex.gates && typeof ex.gates === 'object' ? ex.gates : undefined;
|
|
362
|
+
set('gates', exGates ? { ...detectedGates, ...exGates } : undefined, () => detectedGates);
|
|
363
|
+
if (flagsFor('gates').length > 0) {
|
|
364
|
+
const gates = clone(v.gates);
|
|
365
|
+
for (const g of flagsFor('gates')) gates[g.value.name] = g.value.argv;
|
|
366
|
+
override('gates', gates);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const proposal = proposeProof(ctx.profile);
|
|
370
|
+
const exProof = ex.proof && typeof ex.proof === 'object'
|
|
371
|
+
? {
|
|
372
|
+
high: ex.proof.tiers?.high?.paths ?? proposal.high,
|
|
373
|
+
isolation: ex.proof.isolation ?? proposal.isolation,
|
|
374
|
+
link_dirs: ex.proof.export?.link_dirs ?? proposal.link_dirs,
|
|
375
|
+
copy_untracked: ex.proof.export?.copy_untracked ?? proposal.copy_untracked,
|
|
376
|
+
}
|
|
377
|
+
: undefined;
|
|
378
|
+
set('proof', exProof, () => proposal);
|
|
379
|
+
if (flagsFor('proof').length > 0) {
|
|
380
|
+
const proof = clone(v.proof);
|
|
381
|
+
for (const g of flagsFor('proof')) proof[g.value.key] = g.value.value;
|
|
382
|
+
override('proof', proof);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
set('doctor', undefined, () => true);
|
|
386
|
+
if (lastFlag('doctor')) override('doctor', false);
|
|
387
|
+
|
|
388
|
+
return { values: v, sources };
|
|
389
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `code-forge init` flags (plan §2 table, block B13a). Every answer flag overrides exactly one
|
|
3
|
+
* wizard answer (`ANSWER_FLAGS` maps flag -> answer key); `--no-interaction` is a mode switch and
|
|
4
|
+
* overrides no answer.
|
|
5
|
+
*
|
|
6
|
+
* **Q16 = cut (Ricardo, 2026-09-25):** step 8 asks no mutation-tool or nightly question, so the
|
|
7
|
+
* v1.2 `--no-nightly` flag is gone and `--proof` takes only the keys the schema still has
|
|
8
|
+
* (`isolation`, `high`, `link_dirs`, `copy_untracked`). With `-g` and `-p` counted as two flags
|
|
9
|
+
* (two tokens that each set the scope answer) there are 19 answer flags.
|
|
10
|
+
*
|
|
11
|
+
* Parsing is strict: an unknown flag, a missing value, a repeated single-value flag or a bad value
|
|
12
|
+
* is a usage error. Error messages name the flag, never the value given (a user can paste a key
|
|
13
|
+
* into any flag by mistake).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { isOpRef } from '../../keys/onepassword.mjs';
|
|
17
|
+
import { HARNESSES } from '../harnesses.mjs';
|
|
18
|
+
|
|
19
|
+
export const PROVIDERS = Object.freeze(['anthropic', 'openai', 'xai']);
|
|
20
|
+
/** The engines the wizard offers. `subprocess` is never proposed (R2): a power user types it. */
|
|
21
|
+
export const ENGINE_CHOICES = Object.freeze(['auto', 'solo', 'harness']);
|
|
22
|
+
export const GATE_NAMES = Object.freeze(['test', 'lint', 'types', 'format']);
|
|
23
|
+
export const PROOF_KEYS = Object.freeze(['isolation', 'high', 'link_dirs', 'copy_untracked']);
|
|
24
|
+
/** Tools step 1 knows how to detect (and, for some, install after a per-tool yes). */
|
|
25
|
+
export const TOOL_IDS = Object.freeze(['solo', 'codex', 'grok', 'gemini', 'op']);
|
|
26
|
+
|
|
27
|
+
/** flag (as typed) -> the one answer key it overrides. */
|
|
28
|
+
export const ANSWER_FLAGS = Object.freeze({
|
|
29
|
+
'--tools': 'tools',
|
|
30
|
+
'--yes-tool': 'yes_tools',
|
|
31
|
+
'--harness': 'harnesses',
|
|
32
|
+
'-g': 'scope',
|
|
33
|
+
'-p': 'scope',
|
|
34
|
+
'--copy': 'method',
|
|
35
|
+
'--provider': 'provider',
|
|
36
|
+
'--level': 'levels',
|
|
37
|
+
'--refresh-models': 'refresh_models',
|
|
38
|
+
'--multimodel': 'multimodel',
|
|
39
|
+
'--second-provider': 'second_provider',
|
|
40
|
+
'--jev-ref': 'jev',
|
|
41
|
+
'--jev-env': 'jev',
|
|
42
|
+
'--no-jev': 'jev',
|
|
43
|
+
'--engine': 'engine',
|
|
44
|
+
'--solo-project': 'solo_project',
|
|
45
|
+
'--gate': 'gates',
|
|
46
|
+
'--proof': 'proof',
|
|
47
|
+
'--skip-doctor': 'doctor',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const BOOLEAN_FLAGS = new Set(['-g', '-p', '--copy', '--refresh-models', '--no-jev', '--skip-doctor', '--no-interaction']);
|
|
51
|
+
const REPEATABLE = new Set(['--yes-tool', '--level', '--gate', '--proof']);
|
|
52
|
+
const LEVEL_NAMES = new Set(['L0', 'L1', 'L2', 'L3']);
|
|
53
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
54
|
+
|
|
55
|
+
export class UsageError extends Error {}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Split a gate command into argv the way a shell would split words (single/double quotes,
|
|
59
|
+
* backslash escapes) — without ever running a shell.
|
|
60
|
+
* @param {string} text
|
|
61
|
+
* @returns {string[]}
|
|
62
|
+
*/
|
|
63
|
+
export function shellWords(text) {
|
|
64
|
+
const words = [];
|
|
65
|
+
let current = '';
|
|
66
|
+
let has = false;
|
|
67
|
+
/** @type {string|null} */
|
|
68
|
+
let quote = null;
|
|
69
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
70
|
+
const ch = text[i];
|
|
71
|
+
if (quote === "'") {
|
|
72
|
+
if (ch === "'") quote = null;
|
|
73
|
+
else current += ch;
|
|
74
|
+
} else if (ch === '\\' && i + 1 < text.length && quote !== "'") {
|
|
75
|
+
i += 1;
|
|
76
|
+
current += text[i];
|
|
77
|
+
has = true;
|
|
78
|
+
} else if (quote === '"') {
|
|
79
|
+
if (ch === '"') quote = null;
|
|
80
|
+
else current += ch;
|
|
81
|
+
} else if (ch === '"' || ch === "'") {
|
|
82
|
+
quote = ch;
|
|
83
|
+
has = true;
|
|
84
|
+
} else if (/\s/.test(ch)) {
|
|
85
|
+
if (has) words.push(current);
|
|
86
|
+
current = '';
|
|
87
|
+
has = false;
|
|
88
|
+
} else {
|
|
89
|
+
current += ch;
|
|
90
|
+
has = true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (quote !== null) throw new UsageError('--gate: unterminated quote');
|
|
94
|
+
if (has) words.push(current);
|
|
95
|
+
return words;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** @param {string} flag @param {string} value @returns {[string, string]} */
|
|
99
|
+
function keyValue(flag, value) {
|
|
100
|
+
const eq = value.indexOf('=');
|
|
101
|
+
if (eq <= 0) throw new UsageError(`${flag} expects <key>=<value>`);
|
|
102
|
+
return [value.slice(0, eq), value.slice(eq + 1)];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** @param {string} value @returns {string[]} */
|
|
106
|
+
function csv(value) {
|
|
107
|
+
return value.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* `L1=model[:effort][@provider]`
|
|
112
|
+
* @param {string} value
|
|
113
|
+
* @returns {{level: string, spec: {model: string, effort?: string, provider?: string}}}
|
|
114
|
+
*/
|
|
115
|
+
export function parseLevel(value) {
|
|
116
|
+
const [level, rest] = keyValue('--level', value);
|
|
117
|
+
if (!LEVEL_NAMES.has(level)) throw new UsageError('--level: level must be L0, L1, L2 or L3');
|
|
118
|
+
let body = rest;
|
|
119
|
+
/** @type {{model: string, effort?: string, provider?: string}} */
|
|
120
|
+
const spec = { model: '' };
|
|
121
|
+
const at = body.lastIndexOf('@');
|
|
122
|
+
if (at >= 0) {
|
|
123
|
+
const provider = body.slice(at + 1);
|
|
124
|
+
if (!PROVIDERS.includes(provider)) throw new UsageError('--level: unknown provider');
|
|
125
|
+
spec.provider = provider;
|
|
126
|
+
body = body.slice(0, at);
|
|
127
|
+
}
|
|
128
|
+
const colon = body.lastIndexOf(':');
|
|
129
|
+
if (colon >= 0) {
|
|
130
|
+
const effort = body.slice(colon + 1);
|
|
131
|
+
if (effort.length === 0) throw new UsageError('--level: empty effort');
|
|
132
|
+
spec.effort = effort;
|
|
133
|
+
body = body.slice(0, colon);
|
|
134
|
+
}
|
|
135
|
+
if (body.length === 0) throw new UsageError('--level: empty model');
|
|
136
|
+
spec.model = body;
|
|
137
|
+
// key order model, effort, provider — the order the schema documents
|
|
138
|
+
return { level, spec: { model: spec.model, ...(spec.effort ? { effort: spec.effort } : {}), ...(spec.provider ? { provider: spec.provider } : {}) } };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {string} flag @param {string} value
|
|
143
|
+
* @returns {unknown} the typed value for that flag
|
|
144
|
+
*/
|
|
145
|
+
function convert(flag, value) {
|
|
146
|
+
switch (flag) {
|
|
147
|
+
case '--tools':
|
|
148
|
+
if (value !== 'recommended' && value !== 'current') throw new UsageError('--tools must be recommended or current');
|
|
149
|
+
return value;
|
|
150
|
+
case '--yes-tool':
|
|
151
|
+
if (!TOOL_IDS.includes(value)) throw new UsageError(`--yes-tool must be one of ${TOOL_IDS.join(', ')}`);
|
|
152
|
+
return value;
|
|
153
|
+
case '--harness': {
|
|
154
|
+
const ids = csv(value);
|
|
155
|
+
const known = HARNESSES.map((h) => h.id);
|
|
156
|
+
if (ids.length === 0 || ids.some((id) => !known.includes(id))) throw new UsageError(`--harness takes a comma list of ${known.join(', ')}`);
|
|
157
|
+
return [...new Set(ids)];
|
|
158
|
+
}
|
|
159
|
+
case '--provider':
|
|
160
|
+
case '--second-provider':
|
|
161
|
+
if (!PROVIDERS.includes(value)) throw new UsageError(`${flag} must be one of ${PROVIDERS.join(', ')}`);
|
|
162
|
+
return value;
|
|
163
|
+
case '--level':
|
|
164
|
+
return parseLevel(value);
|
|
165
|
+
case '--multimodel':
|
|
166
|
+
if (value !== 'on' && value !== 'off') throw new UsageError('--multimodel must be on or off');
|
|
167
|
+
return value === 'on';
|
|
168
|
+
case '--jev-ref':
|
|
169
|
+
if (!isOpRef(value)) throw new UsageError('--jev-ref must be an op://vault/item/field reference');
|
|
170
|
+
return value;
|
|
171
|
+
case '--jev-env':
|
|
172
|
+
if (!ENV_NAME.test(value)) throw new UsageError('--jev-env must be an environment variable name');
|
|
173
|
+
return value;
|
|
174
|
+
case '--engine':
|
|
175
|
+
if (value === 'subprocess') {
|
|
176
|
+
throw new UsageError('--engine: init never selects subprocess (R2); type "engine: subprocess" into .code-forge.yml yourself');
|
|
177
|
+
}
|
|
178
|
+
if (!ENGINE_CHOICES.includes(value)) throw new UsageError(`--engine must be one of ${ENGINE_CHOICES.join(', ')}`);
|
|
179
|
+
return value;
|
|
180
|
+
case '--solo-project':
|
|
181
|
+
if (!/^[1-9]\d{0,9}$/.test(value)) throw new UsageError('--solo-project must be a positive integer');
|
|
182
|
+
return Number(value);
|
|
183
|
+
case '--gate': {
|
|
184
|
+
const [name, cmd] = keyValue('--gate', value);
|
|
185
|
+
if (!GATE_NAMES.includes(name)) throw new UsageError(`--gate: gate must be one of ${GATE_NAMES.join(', ')}`);
|
|
186
|
+
if (cmd === 'none' || cmd === 'null') return { name, argv: null };
|
|
187
|
+
const argv = shellWords(cmd);
|
|
188
|
+
if (argv.length === 0) throw new UsageError('--gate: empty command');
|
|
189
|
+
return { name, argv };
|
|
190
|
+
}
|
|
191
|
+
case '--proof': {
|
|
192
|
+
const [key, raw] = keyValue('--proof', value);
|
|
193
|
+
if (!PROOF_KEYS.includes(key)) throw new UsageError(`--proof: key must be one of ${PROOF_KEYS.join(', ')}`);
|
|
194
|
+
if (key === 'isolation') {
|
|
195
|
+
if (raw !== 'export' && raw !== 'lock') throw new UsageError('--proof isolation must be export or lock');
|
|
196
|
+
return { key, value: raw };
|
|
197
|
+
}
|
|
198
|
+
return { key, value: csv(raw) };
|
|
199
|
+
}
|
|
200
|
+
default:
|
|
201
|
+
throw new UsageError(`unknown flag ${flag}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @typedef {object} ParsedFlags
|
|
207
|
+
* @property {boolean} noInteraction
|
|
208
|
+
* @property {Array<{flag: string, value: unknown}>} given - answer flags in the order typed.
|
|
209
|
+
*/
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @param {string[]} args
|
|
213
|
+
* @returns {ParsedFlags}
|
|
214
|
+
* @throws {UsageError}
|
|
215
|
+
*/
|
|
216
|
+
export function parseInitArgs(args) {
|
|
217
|
+
/** @type {ParsedFlags} */
|
|
218
|
+
const out = { noInteraction: false, given: [] };
|
|
219
|
+
const seen = new Set();
|
|
220
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
221
|
+
const arg = args[i];
|
|
222
|
+
const eq = arg.startsWith('--') ? arg.indexOf('=') : -1;
|
|
223
|
+
const flag = eq > 0 ? arg.slice(0, eq) : arg;
|
|
224
|
+
if (flag !== '--no-interaction' && !Object.hasOwn(ANSWER_FLAGS, flag)) {
|
|
225
|
+
throw new UsageError(arg.startsWith('-') ? `unknown flag ${flag}` : 'init takes no positional argument');
|
|
226
|
+
}
|
|
227
|
+
if (seen.has(flag) && !REPEATABLE.has(flag)) throw new UsageError(`${flag} given more than once`);
|
|
228
|
+
seen.add(flag);
|
|
229
|
+
if (BOOLEAN_FLAGS.has(flag)) {
|
|
230
|
+
if (eq > 0) throw new UsageError(`${flag} takes no value`);
|
|
231
|
+
if (flag === '--no-interaction') out.noInteraction = true;
|
|
232
|
+
else out.given.push({ flag, value: flag === '--copy' ? 'copy' : flag === '-g' ? 'global' : flag === '-p' ? 'project' : true });
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
let value;
|
|
236
|
+
if (eq > 0) value = arg.slice(eq + 1);
|
|
237
|
+
else {
|
|
238
|
+
value = args[i + 1];
|
|
239
|
+
i += 1;
|
|
240
|
+
}
|
|
241
|
+
if (typeof value !== 'string' || value.length === 0 || (eq < 0 && value.startsWith('-'))) throw new UsageError(`${flag} needs a value`);
|
|
242
|
+
out.given.push({ flag, value: convert(flag, value) });
|
|
243
|
+
}
|
|
244
|
+
if (seen.has('-g') && seen.has('-p')) throw new UsageError('-g and -p are exclusive');
|
|
245
|
+
const jev = ['--jev-ref', '--jev-env', '--no-jev'].filter((f) => seen.has(f));
|
|
246
|
+
if (jev.length > 1) throw new UsageError(`${jev.join(' and ')} are exclusive`);
|
|
247
|
+
return out;
|
|
248
|
+
}
|