@vodmal/vdx-cli 0.16.0 → 0.18.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/README.md +9 -3
- package/package.json +1 -1
- package/src/ai.ts +77 -4
- package/src/doctor.ts +35 -32
- package/src/index.ts +153 -51
package/README.md
CHANGED
|
@@ -49,7 +49,10 @@ vdx doctor [--format=ansi|markdown|json]
|
|
|
49
49
|
|
|
50
50
|
# Start your agent in a project per your profile (~/.vdx-environment.yaml or $VDX_ENVIRONMENT)
|
|
51
51
|
vdx ai[@host] [project-path] [--new] [--restart] [--detach] [--dry-run] # default: cwd; @host: over ssh, in tmux there
|
|
52
|
-
vdx ai --
|
|
52
|
+
vdx ai --check [project-path] # this session against the profile — what the plugin's SessionStart hook tells the agent
|
|
53
|
+
|
|
54
|
+
# What a command does and takes; nothing runs (ai: also where launch flags live)
|
|
55
|
+
vdx <command> --help
|
|
53
56
|
|
|
54
57
|
# Print the installed version
|
|
55
58
|
vdx --version
|
|
@@ -88,8 +91,11 @@ name. `vdx ai@<host>` runs the same command on `<host>` over ssh — that machin
|
|
|
88
91
|
profile and tmux, the path taken under its home directory; `--detach` only
|
|
89
92
|
starts it. The machine's own label (`$VDX_HOST`) runs here. Where git knows no
|
|
90
93
|
commit author, `vdx ai` proposes one (Enter writes it into `.git/config`).
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
|
|
95
|
+
Every command takes `--help` (`-h`) and runs nothing with it. An unknown option,
|
|
96
|
+
a stray argument or a value flag without its value is refused with exit 2
|
|
97
|
+
before anything runs. In 0.16.0 and earlier the commands other than `vdx ai`
|
|
98
|
+
dropped such arguments, and `vdx init --help` wrote `mise.toml`.
|
|
93
99
|
|
|
94
100
|
By default the bundled `rubric/vdx-rubric.yaml` is used (a mirror of canonical
|
|
95
101
|
[vdx-rubric-vodmal](https://github.com/VoDmAl/vdx-rubric-vodmal) at the time of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vodmal/vdx-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "vdx — unified lifecycle interface (up/down/build/test/check/fix) + versioned maturity audit with drift detection. Pairs with the vdx Claude Code plugin.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"vdx",
|
package/src/ai.ts
CHANGED
|
@@ -906,12 +906,21 @@ export function runAiRemote(opts: AiOptions, host: string, deps: AiDeps, version
|
|
|
906
906
|
return res.status ?? 1;
|
|
907
907
|
}
|
|
908
908
|
|
|
909
|
-
|
|
910
|
-
|
|
909
|
+
interface Prepared {
|
|
910
|
+
projectRoot: string;
|
|
911
|
+
environment: Environment;
|
|
912
|
+
profilePath: string | null;
|
|
913
|
+
profileDir: string;
|
|
914
|
+
plan: LaunchPlan;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/** The project, the profile and the launch plan for `pathArg`; an exit code when there is none. */
|
|
918
|
+
function prepare(pathArg: string, deps: AiDeps): Prepared | number {
|
|
919
|
+
const { log } = deps;
|
|
911
920
|
|
|
912
|
-
const projectRoot = resolveProjectRoot(
|
|
921
|
+
const projectRoot = resolveProjectRoot(pathArg);
|
|
913
922
|
if (!projectRoot) {
|
|
914
|
-
log(`vdx ai: ${path.resolve(
|
|
923
|
+
log(`vdx ai: ${path.resolve(pathArg)} is not a directory`);
|
|
915
924
|
return EXIT_USAGE;
|
|
916
925
|
}
|
|
917
926
|
|
|
@@ -959,6 +968,70 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
|
959
968
|
log(`vdx ai: ${profilePath ?? 'profile'}: ${e?.message ?? e}`);
|
|
960
969
|
return EXIT_USAGE;
|
|
961
970
|
}
|
|
971
|
+
return { projectRoot, environment, profilePath, profileDir, plan };
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/** The nearest ancestor of `pid` that is the agent: the session a hook or a shell tool runs in. */
|
|
975
|
+
export function findOwnAgent(procs: ProcInfo[], pid: number, command: string): ProcInfo | null {
|
|
976
|
+
const byPid = new Map(procs.map((p) => [p.pid, p]));
|
|
977
|
+
let cur = byPid.get(pid);
|
|
978
|
+
for (let hops = 0; cur && cur.ppid > 1 && hops < 64; hops++) {
|
|
979
|
+
cur = byPid.get(cur.ppid);
|
|
980
|
+
if (cur && isAgentProcess(cur, command)) return cur;
|
|
981
|
+
}
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* `vdx ai --check`: what an agent should know about how it is launched here,
|
|
987
|
+
* and whether the session it runs in carries the profile's flags. Prints
|
|
988
|
+
* nothing without a profile — on such a machine `vdx ai` decides nothing.
|
|
989
|
+
*/
|
|
990
|
+
export function runAiCheck(pathArg: string, deps: AiDeps, selfPid: number = process.pid): number {
|
|
991
|
+
if (!resolveEnvironmentPath(deps.env, deps.home)) return EXIT_OK;
|
|
992
|
+
const prepared = prepare(pathArg, deps);
|
|
993
|
+
if (typeof prepared === 'number') return prepared;
|
|
994
|
+
const { projectRoot, profilePath, plan } = prepared;
|
|
995
|
+
|
|
996
|
+
const lines = [
|
|
997
|
+
`vdx ai: the agent in this project is started with \`vdx ai\`, and its flags come from the profile ` +
|
|
998
|
+
`${profilePath} — agent.args for every project, agent.when[] for a class of projects. ` +
|
|
999
|
+
`Hand the user \`vdx ai\`, not \`${plan.command} --<flag>\`; a flag a class of projects needs is a ` +
|
|
1000
|
+
`condition in the profile, not an instruction to type. More: \`vdx ai --help\`.`,
|
|
1001
|
+
];
|
|
1002
|
+
const own = findOwnAgent(deps.processes(), selfPid, plan.command);
|
|
1003
|
+
if (!own) {
|
|
1004
|
+
lines.push(`· no ${plan.command} among the ancestors of this process — no session to compare with the profile.`);
|
|
1005
|
+
deps.out(lines.join('\n') + '\n');
|
|
1006
|
+
return EXIT_OK;
|
|
1007
|
+
}
|
|
1008
|
+
const missing = missingArgs(own.args, plan.args);
|
|
1009
|
+
const why = plan.matched.length ? ` (profile conditions met here: ${plan.matched.join(', ')})` : '';
|
|
1010
|
+
if (missing.length === 0) {
|
|
1011
|
+
lines.push(`✓ this session carries the profile's flags${why}.`);
|
|
1012
|
+
deps.out(lines.join('\n') + '\n');
|
|
1013
|
+
return EXIT_OK;
|
|
1014
|
+
}
|
|
1015
|
+
const root = shellQuote(projectRoot);
|
|
1016
|
+
lines.push(
|
|
1017
|
+
`✗ this session runs without ${missing.join(' ')}${why}. Hand the user the fix — ` +
|
|
1018
|
+
(deps.env['TMUX']
|
|
1019
|
+
? `\`vdx ai --restart ${root}\`: it stops this session and starts it again with the profile's flags` +
|
|
1020
|
+
(plan.resumeArgs.length ? `, resuming the conversation (${plan.resumeArgs.join(' ')})` : '') +
|
|
1021
|
+
`. Do not run it yourself: it ends this session.`
|
|
1022
|
+
: `this session runs outside tmux, so vdx cannot restart it: the user exits it and runs \`vdx ai ${root}\`.`),
|
|
1023
|
+
);
|
|
1024
|
+
deps.out(lines.join('\n') + '\n');
|
|
1025
|
+
return EXIT_DRIFT;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
1029
|
+
const { log, tmux } = deps;
|
|
1030
|
+
|
|
1031
|
+
const prepared = prepare(opts.path, deps);
|
|
1032
|
+
if (typeof prepared === 'number') return prepared;
|
|
1033
|
+
const { projectRoot, environment, profileDir } = prepared;
|
|
1034
|
+
let { plan } = prepared;
|
|
962
1035
|
// A start continues the last conversation unless asked for a new one.
|
|
963
1036
|
if (opts.fresh) plan = { ...plan, resumeArgs: [] };
|
|
964
1037
|
|
package/src/doctor.ts
CHANGED
|
@@ -339,42 +339,45 @@ function checkClaudeCode(): CheckResult {
|
|
|
339
339
|
};
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
message: 'vdx marketplace registered',
|
|
361
|
-
};
|
|
342
|
+
const PLUGIN_INSTALL_HINT =
|
|
343
|
+
'in Claude Code: /plugin marketplace add VoDmAl/ai-dev-plugins, then /plugin install vdx from it';
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* The vdx plugin as Claude Code records it: `vdx@<marketplace>` in
|
|
347
|
+
* plugins/installed_plugins.json, and not switched off in settings.json
|
|
348
|
+
* enabledPlugins. Whichever marketplace brought it — the plugin ships through
|
|
349
|
+
* the owner's marketplace (VoDmAl/ai-dev-plugins), not a vdx one of its own.
|
|
350
|
+
*/
|
|
351
|
+
export function checkClaudeCodePlugin(
|
|
352
|
+
claudeDir: string = process.env['CLAUDE_CONFIG_DIR'] || path.join(os.homedir(), '.claude'),
|
|
353
|
+
): CheckResult {
|
|
354
|
+
const base = { id: 'claude-plugin', label: 'Claude Code vdx plugin' };
|
|
355
|
+
const readJson = (file: string): any => {
|
|
356
|
+
try {
|
|
357
|
+
return JSON.parse(fs.readFileSync(path.join(claudeDir, file), 'utf8'));
|
|
358
|
+
} catch {
|
|
359
|
+
return null;
|
|
362
360
|
}
|
|
361
|
+
};
|
|
362
|
+
const installed = readJson(path.join('plugins', 'installed_plugins.json'));
|
|
363
|
+
if (!installed) {
|
|
363
364
|
return {
|
|
364
|
-
|
|
365
|
-
label: 'Claude Code vdx plugin',
|
|
366
|
-
status: 'warning',
|
|
367
|
-
message: 'vdx marketplace not registered',
|
|
368
|
-
remedy: 'Add github.com/VoDmAl/vdx/marketplace to extraKnownMarketplaces',
|
|
369
|
-
};
|
|
370
|
-
} catch (e: any) {
|
|
371
|
-
return {
|
|
372
|
-
id: 'claude-plugin',
|
|
373
|
-
label: 'Claude Code vdx plugin',
|
|
365
|
+
...base,
|
|
374
366
|
status: 'warning',
|
|
375
|
-
message: `
|
|
367
|
+
message: `no plugin records in ${claudeDir}/plugins/installed_plugins.json`,
|
|
368
|
+
remedy: PLUGIN_INSTALL_HINT,
|
|
376
369
|
};
|
|
377
370
|
}
|
|
371
|
+
const key = Object.keys(installed.plugins ?? {}).find((k) => k.startsWith('vdx@'));
|
|
372
|
+
if (!key) {
|
|
373
|
+
return { ...base, status: 'warning', message: 'not installed', remedy: PLUGIN_INSTALL_HINT };
|
|
374
|
+
}
|
|
375
|
+
const version = installed.plugins[key]?.[0]?.version;
|
|
376
|
+
const label = `${key}${typeof version === 'string' ? ` ${version}` : ''}`;
|
|
377
|
+
if (readJson('settings.json')?.enabledPlugins?.[key] === false) {
|
|
378
|
+
return { ...base, status: 'warning', message: `${label} installed but disabled`, remedy: `/plugin enable ${key}` };
|
|
379
|
+
}
|
|
380
|
+
return { ...base, status: 'ok', message: label };
|
|
378
381
|
}
|
|
379
382
|
|
|
380
383
|
/**
|
|
@@ -543,7 +546,7 @@ const CHECKS: Array<(ctx: DoctorCtx) => CheckResult | null> = [
|
|
|
543
546
|
checkVdxVersion,
|
|
544
547
|
checkVdx,
|
|
545
548
|
checkClaudeCode,
|
|
546
|
-
checkClaudeCodePlugin,
|
|
549
|
+
() => checkClaudeCodePlugin(),
|
|
547
550
|
checkNode,
|
|
548
551
|
checkGit,
|
|
549
552
|
checkMise,
|
package/src/index.ts
CHANGED
|
@@ -29,31 +29,40 @@ import {
|
|
|
29
29
|
type PublishOptions,
|
|
30
30
|
} from './publish.ts';
|
|
31
31
|
import { resolveDefaultRubric } from './defaults.ts';
|
|
32
|
-
import { runAi, runAiRemote, defaultDeps, hostLabel, REMOTE_HOST_RE } from './ai.ts';
|
|
32
|
+
import { runAi, runAiCheck, runAiRemote, defaultDeps, hostLabel, REMOTE_HOST_RE } from './ai.ts';
|
|
33
33
|
|
|
34
34
|
const DEFAULT_RUBRIC = resolveDefaultRubric();
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
process.stderr.write(
|
|
38
|
-
`Usage:
|
|
36
|
+
const USAGE = `Usage:
|
|
39
37
|
vdx <up|down|build|test|check|fix> run lifecycle verb (via mise run <verb>)
|
|
40
38
|
vdx audit [project_path] [--rubric <path>] [--stack <stack>] [--format=ansi|markdown|json] [--json] (default: cwd)
|
|
41
39
|
vdx init [project_path] [--stack <id>] [--baseline <ref>] [--dry-run] [--force] (default: cwd)
|
|
42
40
|
vdx publish <patch|minor|major> [--dry-run] [--force]
|
|
43
41
|
vdx doctor [--format=ansi|markdown|json] [--json]
|
|
44
42
|
vdx ai[@host] [project_path] [--new] [--restart] [--detach] [--dry-run] (default: cwd; @host: over ssh, in tmux there)
|
|
45
|
-
vdx
|
|
43
|
+
vdx <command> --help what the command does; an unknown option is refused (exit 2)
|
|
46
44
|
vdx --version
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
function usage(): never {
|
|
48
|
+
process.stderr.write(USAGE);
|
|
49
49
|
process.exit(1);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
/** What a command reads from its arguments; the rest is refused. */
|
|
53
|
+
interface CommandSpec {
|
|
54
|
+
usage: string;
|
|
55
|
+
/** Printed under the usage line by `--help`. */
|
|
56
|
+
help: string;
|
|
57
|
+
/** Flags that take no value. */
|
|
58
|
+
bools?: string[];
|
|
59
|
+
/** Flags that take one (`--stack node`, `--format=json`). */
|
|
60
|
+
values?: string[];
|
|
61
|
+
/** How many positional arguments the command takes. */
|
|
62
|
+
positionals: number;
|
|
63
|
+
}
|
|
55
64
|
|
|
56
|
-
Starts your agent in the project, or attaches to the one already running there.
|
|
65
|
+
const AI_HELP = `Starts your agent in the project, or attaches to the one already running there.
|
|
57
66
|
Which agent, its flags and whether it runs in tmux come from your profile:
|
|
58
67
|
$VDX_ENVIRONMENT, else ~/.vdx-environment.yaml, else a plain claude/codex.
|
|
59
68
|
|
|
@@ -67,6 +76,9 @@ the project's files). Hand a person \`vdx ai\`, not \`<agent> --<flag>\`.
|
|
|
67
76
|
--restart restart the running agent in its pane — applies a changed profile
|
|
68
77
|
--detach start without attaching; print the command that attaches
|
|
69
78
|
--dry-run print the plan and the project's running agents; start nothing
|
|
79
|
+
--check for the agent's own session: how the agent here is launched, and
|
|
80
|
+
whether this session carries the profile's flags (the vdx plugin's
|
|
81
|
+
SessionStart hook); prints nothing without a profile; exit 3 on drift
|
|
70
82
|
-h, --help this help
|
|
71
83
|
|
|
72
84
|
Exit codes: 0 the agent runs per the profile; 2 profile or usage error;
|
|
@@ -75,12 +87,65 @@ was not confirmed.
|
|
|
75
87
|
Profile format: https://github.com/VoDmAl/vdx/blob/main/docs/specs/environment-format.md
|
|
76
88
|
`;
|
|
77
89
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
const COMMANDS: Record<string, CommandSpec> = {
|
|
91
|
+
audit: {
|
|
92
|
+
usage: 'vdx audit [project_path] [--rubric <path>] [--stack <stack>] [--format=ansi|markdown|json] [--json]',
|
|
93
|
+
help: `Scores the project (default: cwd) against the rubric and prints the report.
|
|
94
|
+
Reads only. The rubric is the bundled one unless --rubric or $VDX_RUBRIC names another.
|
|
95
|
+
`,
|
|
96
|
+
bools: ['json'],
|
|
97
|
+
values: ['rubric', 'stack', 'format'],
|
|
98
|
+
positionals: 1,
|
|
99
|
+
},
|
|
100
|
+
init: {
|
|
101
|
+
usage: 'vdx init [project_path] [--stack <id>] [--baseline <ref>] [--dry-run] [--force]',
|
|
102
|
+
help: `Writes mise.toml for the project (default: cwd): the lifecycle verbs mapped to
|
|
103
|
+
the project's own scripts. --dry-run prints it and writes nothing; an existing
|
|
104
|
+
mise.toml is overwritten only with --force.
|
|
105
|
+
`,
|
|
106
|
+
bools: ['dry-run', 'force'],
|
|
107
|
+
values: ['stack', 'baseline'],
|
|
108
|
+
positionals: 1,
|
|
109
|
+
},
|
|
110
|
+
publish: {
|
|
111
|
+
usage: 'vdx publish <patch|minor|major> [--dry-run] [--force]',
|
|
112
|
+
help: `Releases the library in the current directory: pre-flight checks, version bump,
|
|
113
|
+
npm publish (npm asks for an OTP), git commit and tag. Does not push.
|
|
114
|
+
--dry-run runs the pre-flight and prints the plan; --force goes on past a failed pre-flight.
|
|
115
|
+
`,
|
|
116
|
+
bools: ['dry-run', 'force'],
|
|
117
|
+
positionals: 1,
|
|
118
|
+
},
|
|
119
|
+
doctor: {
|
|
120
|
+
usage: 'vdx doctor [--format=ansi|markdown|json] [--json]',
|
|
121
|
+
help: `Checks this machine for what vdx needs (Node, git, mise, npm auth, docker, the
|
|
122
|
+
Claude Code plugin) and, inside a project, its commit author. Reads only.
|
|
123
|
+
Exit 2 when something is missing.
|
|
124
|
+
`,
|
|
125
|
+
bools: ['json'],
|
|
126
|
+
values: ['format'],
|
|
127
|
+
positionals: 0,
|
|
128
|
+
},
|
|
129
|
+
ai: {
|
|
130
|
+
usage: 'vdx ai[@host] [project_path] [--new] [--restart] [--detach] [--dry-run]',
|
|
131
|
+
help: AI_HELP,
|
|
132
|
+
// `--resume` asked to continue in 0.12; continuing is the default now.
|
|
133
|
+
bools: ['new', 'restart', 'resume', 'detach', 'dry-run', 'check'],
|
|
134
|
+
positionals: 1,
|
|
135
|
+
},
|
|
136
|
+
...Object.fromEntries(
|
|
137
|
+
LIFECYCLE_VERBS.map((verb) => [
|
|
138
|
+
verb,
|
|
139
|
+
{
|
|
140
|
+
usage: `vdx ${verb}`,
|
|
141
|
+
help: `Runs \`mise run ${verb}\` in the current directory — the project's own ${verb}
|
|
142
|
+
command, as mise.toml maps it. Takes no arguments.
|
|
143
|
+
`,
|
|
144
|
+
positionals: 0,
|
|
145
|
+
},
|
|
146
|
+
]),
|
|
147
|
+
),
|
|
148
|
+
};
|
|
84
149
|
|
|
85
150
|
interface ParsedArgs {
|
|
86
151
|
cmd: string;
|
|
@@ -116,6 +181,48 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
116
181
|
return { cmd, positionals, flags };
|
|
117
182
|
}
|
|
118
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Keeps what the command reads and refuses the rest, before anything runs: a
|
|
186
|
+
* misread argument must not run the command. `vdx ai --help` started an agent,
|
|
187
|
+
* `vdx init --help` wrote mise.toml, `vdx down --help` ran `mise run down`.
|
|
188
|
+
*/
|
|
189
|
+
function checkArgs(name: string, opts: ParsedArgs): ParsedArgs {
|
|
190
|
+
const spec = COMMANDS[name]!;
|
|
191
|
+
const bools = [...(spec.bools ?? []), 'help'];
|
|
192
|
+
const values = spec.values ?? [];
|
|
193
|
+
// parseArgs hands the token after a bare flag to that flag as its value; a
|
|
194
|
+
// flag that takes none gives it back — it is the path (or `-h`).
|
|
195
|
+
const positionals = [...opts.positionals];
|
|
196
|
+
const flags: Record<string, string | boolean> = {};
|
|
197
|
+
for (const [flag, value] of Object.entries(opts.flags)) {
|
|
198
|
+
if (bools.includes(flag) && typeof value === 'string') {
|
|
199
|
+
positionals.push(value);
|
|
200
|
+
flags[flag] = true;
|
|
201
|
+
} else {
|
|
202
|
+
flags[flag] = value;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (flags.help || positionals.includes('-h')) {
|
|
206
|
+
process.stdout.write(`Usage: ${spec.usage}\n\n${spec.help}`);
|
|
207
|
+
process.exit(0);
|
|
208
|
+
}
|
|
209
|
+
const fail = (message: string): never => {
|
|
210
|
+
process.stderr.write(`vdx ${name}: ${message}\nUsage: ${spec.usage}\nMore: vdx ${name} --help\n`);
|
|
211
|
+
process.exit(2);
|
|
212
|
+
};
|
|
213
|
+
const unknown = [
|
|
214
|
+
...Object.keys(flags).filter((f) => !bools.includes(f) && !values.includes(f)).map((f) => `--${f}`),
|
|
215
|
+
...positionals.filter((p) => p.startsWith('-')),
|
|
216
|
+
];
|
|
217
|
+
if (unknown.length > 0) fail(`unknown option ${unknown.join(', ')}`);
|
|
218
|
+
const bare = values.filter((f) => flags[f] === true).map((f) => `--${f}`);
|
|
219
|
+
if (bare.length > 0) fail(`${bare.join(', ')} needs a value`);
|
|
220
|
+
if (positionals.length > spec.positionals) {
|
|
221
|
+
fail(`unexpected argument ${positionals.slice(spec.positionals).join(' ')}`);
|
|
222
|
+
}
|
|
223
|
+
return { cmd: opts.cmd, positionals, flags };
|
|
224
|
+
}
|
|
225
|
+
|
|
119
226
|
function cmdAudit(opts: ParsedArgs): void {
|
|
120
227
|
const projectArg = opts.positionals[0] ?? '.';
|
|
121
228
|
const projectRoot = path.resolve(projectArg);
|
|
@@ -297,34 +404,24 @@ function cmdDoctor(opts: ParsedArgs): void {
|
|
|
297
404
|
}
|
|
298
405
|
|
|
299
406
|
function cmdAi(opts: ParsedArgs): void {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
// `--resume` asked to continue in 0.12; continuing is the default now.
|
|
311
|
-
// Still read, so the path after it is not taken for its value.
|
|
312
|
-
bool('resume');
|
|
313
|
-
const detach = bool('detach');
|
|
314
|
-
const dryRun = bool('dry-run');
|
|
315
|
-
// `vdx ai` starts an agent, so a misread argument must not run it: an agent
|
|
316
|
-
// that tried `vdx ai --help` got a launch in the current directory.
|
|
317
|
-
if (bool('help') || positionals.includes('-h')) {
|
|
318
|
-
process.stdout.write(AI_HELP);
|
|
319
|
-
process.exit(0);
|
|
407
|
+
if (opts.flags.check === true) {
|
|
408
|
+
// About the session this runs in — there is nothing to start, and no other machine.
|
|
409
|
+
const others = Object.keys(opts.flags).filter((f) => f !== 'check');
|
|
410
|
+
if (others.length > 0 || opts.cmd !== 'ai') {
|
|
411
|
+
process.stderr.write(
|
|
412
|
+
`vdx ai: --check takes only the project path (not ${opts.cmd !== 'ai' ? opts.cmd : `--${others.join(', --')}`})\n`,
|
|
413
|
+
);
|
|
414
|
+
process.exit(2);
|
|
415
|
+
}
|
|
416
|
+
process.exit(runAiCheck(opts.positionals[0] ?? '.', defaultDeps()));
|
|
320
417
|
}
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
418
|
+
const options = {
|
|
419
|
+
path: opts.positionals[0] ?? '.',
|
|
420
|
+
restart: opts.flags.restart === true,
|
|
421
|
+
fresh: opts.flags.new === true,
|
|
422
|
+
detach: opts.flags.detach === true,
|
|
423
|
+
dryRun: opts.flags['dry-run'] === true,
|
|
424
|
+
};
|
|
328
425
|
// `ai@m3`: the same command on another machine. Its own label runs here.
|
|
329
426
|
const host = opts.cmd.startsWith('ai@') ? opts.cmd.slice(3) : null;
|
|
330
427
|
if (host !== null) {
|
|
@@ -341,11 +438,16 @@ function cmdAi(opts: ParsedArgs): void {
|
|
|
341
438
|
|
|
342
439
|
const parsed = parseArgs(process.argv);
|
|
343
440
|
if (parsed.cmd === '--version') process.stdout.write(readCliVersion() + '\n');
|
|
344
|
-
else if (
|
|
345
|
-
else if (parsed.cmd === '
|
|
346
|
-
else if (parsed.cmd === '
|
|
347
|
-
else if (parsed.cmd === '
|
|
348
|
-
else if (parsed.cmd === '
|
|
349
|
-
else if (
|
|
441
|
+
else if (['--help', '-h', 'help'].includes(parsed.cmd)) process.stdout.write(USAGE);
|
|
442
|
+
else if (parsed.cmd === 'audit') cmdAudit(checkArgs('audit', parsed));
|
|
443
|
+
else if (parsed.cmd === 'init') cmdInit(checkArgs('init', parsed));
|
|
444
|
+
else if (parsed.cmd === 'publish') cmdPublish(checkArgs('publish', parsed));
|
|
445
|
+
else if (parsed.cmd === 'doctor') cmdDoctor(checkArgs('doctor', parsed));
|
|
446
|
+
else if (parsed.cmd === 'ai' || parsed.cmd.startsWith('ai@')) cmdAi(checkArgs('ai', parsed));
|
|
447
|
+
else if ((LIFECYCLE_VERBS as readonly string[]).includes(parsed.cmd)) {
|
|
448
|
+
checkArgs(parsed.cmd, parsed);
|
|
350
449
|
cmdRun(parsed.cmd as LifecycleVerb);
|
|
351
|
-
else
|
|
450
|
+
} else {
|
|
451
|
+
if (parsed.cmd !== '') process.stderr.write(`vdx: unknown command "${parsed.cmd}"\n`);
|
|
452
|
+
usage();
|
|
453
|
+
}
|