@vodmal/vdx-cli 0.15.0 → 0.17.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 +8 -0
- package/package.json +1 -1
- package/src/index.ts +161 -27
package/README.md
CHANGED
|
@@ -50,6 +50,9 @@ vdx doctor [--format=ansi|markdown|json]
|
|
|
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
52
|
|
|
53
|
+
# What a command does and takes; nothing runs (ai: also where launch flags live)
|
|
54
|
+
vdx <command> --help
|
|
55
|
+
|
|
53
56
|
# Print the installed version
|
|
54
57
|
vdx --version
|
|
55
58
|
|
|
@@ -88,6 +91,11 @@ profile and tmux, the path taken under its home directory; `--detach` only
|
|
|
88
91
|
starts it. The machine's own label (`$VDX_HOST`) runs here. Where git knows no
|
|
89
92
|
commit author, `vdx ai` proposes one (Enter writes it into `.git/config`).
|
|
90
93
|
|
|
94
|
+
Every command takes `--help` (`-h`) and runs nothing with it. An unknown option,
|
|
95
|
+
a stray argument or a value flag without its value is refused with exit 2
|
|
96
|
+
before anything runs. In 0.16.0 and earlier the commands other than `vdx ai`
|
|
97
|
+
dropped such arguments, and `vdx init --help` wrote `mise.toml`.
|
|
98
|
+
|
|
91
99
|
By default the bundled `rubric/vdx-rubric.yaml` is used (a mirror of canonical
|
|
92
100
|
[vdx-rubric-vodmal](https://github.com/VoDmAl/vdx-rubric-vodmal) at the time of
|
|
93
101
|
each CLI release). Override via `--rubric <path>` or the env var `VDX_RUBRIC`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vodmal/vdx-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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/index.ts
CHANGED
|
@@ -33,21 +33,117 @@ import { runAi, runAiRemote, defaultDeps, hostLabel, REMOTE_HOST_RE } from './ai
|
|
|
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)
|
|
43
|
+
vdx <command> --help what the command does; an unknown option is refused (exit 2)
|
|
45
44
|
vdx --version
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
function usage(): never {
|
|
48
|
+
process.stderr.write(USAGE);
|
|
48
49
|
process.exit(1);
|
|
49
50
|
}
|
|
50
51
|
|
|
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
|
+
}
|
|
64
|
+
|
|
65
|
+
const AI_HELP = `Starts your agent in the project, or attaches to the one already running there.
|
|
66
|
+
Which agent, its flags and whether it runs in tmux come from your profile:
|
|
67
|
+
$VDX_ENVIRONMENT, else ~/.vdx-environment.yaml, else a plain claude/codex.
|
|
68
|
+
|
|
69
|
+
Launch flags live in the profile, not in a command to type: agent.args for
|
|
70
|
+
every project, agent.when[] for a class of projects (a rubric predicate over
|
|
71
|
+
the project's files). Hand a person \`vdx ai\`, not \`<agent> --<flag>\`.
|
|
72
|
+
|
|
73
|
+
project_path the project (default: cwd); its git toplevel is used
|
|
74
|
+
@host the same command on <host> over ssh: its vdx, profile and tmux
|
|
75
|
+
--new a new conversation instead of continuing the last one
|
|
76
|
+
--restart restart the running agent in its pane — applies a changed profile
|
|
77
|
+
--detach start without attaching; print the command that attaches
|
|
78
|
+
--dry-run print the plan and the project's running agents; start nothing
|
|
79
|
+
-h, --help this help
|
|
80
|
+
|
|
81
|
+
Exit codes: 0 the agent runs per the profile; 2 profile or usage error;
|
|
82
|
+
3 the running agent lacks profile flags (rerun with --restart); 4 the start
|
|
83
|
+
was not confirmed.
|
|
84
|
+
Profile format: https://github.com/VoDmAl/vdx/blob/main/docs/specs/environment-format.md
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
const COMMANDS: Record<string, CommandSpec> = {
|
|
88
|
+
audit: {
|
|
89
|
+
usage: 'vdx audit [project_path] [--rubric <path>] [--stack <stack>] [--format=ansi|markdown|json] [--json]',
|
|
90
|
+
help: `Scores the project (default: cwd) against the rubric and prints the report.
|
|
91
|
+
Reads only. The rubric is the bundled one unless --rubric or $VDX_RUBRIC names another.
|
|
92
|
+
`,
|
|
93
|
+
bools: ['json'],
|
|
94
|
+
values: ['rubric', 'stack', 'format'],
|
|
95
|
+
positionals: 1,
|
|
96
|
+
},
|
|
97
|
+
init: {
|
|
98
|
+
usage: 'vdx init [project_path] [--stack <id>] [--baseline <ref>] [--dry-run] [--force]',
|
|
99
|
+
help: `Writes mise.toml for the project (default: cwd): the lifecycle verbs mapped to
|
|
100
|
+
the project's own scripts. --dry-run prints it and writes nothing; an existing
|
|
101
|
+
mise.toml is overwritten only with --force.
|
|
102
|
+
`,
|
|
103
|
+
bools: ['dry-run', 'force'],
|
|
104
|
+
values: ['stack', 'baseline'],
|
|
105
|
+
positionals: 1,
|
|
106
|
+
},
|
|
107
|
+
publish: {
|
|
108
|
+
usage: 'vdx publish <patch|minor|major> [--dry-run] [--force]',
|
|
109
|
+
help: `Releases the library in the current directory: pre-flight checks, version bump,
|
|
110
|
+
npm publish (npm asks for an OTP), git commit and tag. Does not push.
|
|
111
|
+
--dry-run runs the pre-flight and prints the plan; --force goes on past a failed pre-flight.
|
|
112
|
+
`,
|
|
113
|
+
bools: ['dry-run', 'force'],
|
|
114
|
+
positionals: 1,
|
|
115
|
+
},
|
|
116
|
+
doctor: {
|
|
117
|
+
usage: 'vdx doctor [--format=ansi|markdown|json] [--json]',
|
|
118
|
+
help: `Checks this machine for what vdx needs (Node, git, mise, npm auth, docker, the
|
|
119
|
+
Claude Code plugin) and, inside a project, its commit author. Reads only.
|
|
120
|
+
Exit 2 when something is missing.
|
|
121
|
+
`,
|
|
122
|
+
bools: ['json'],
|
|
123
|
+
values: ['format'],
|
|
124
|
+
positionals: 0,
|
|
125
|
+
},
|
|
126
|
+
ai: {
|
|
127
|
+
usage: 'vdx ai[@host] [project_path] [--new] [--restart] [--detach] [--dry-run]',
|
|
128
|
+
help: AI_HELP,
|
|
129
|
+
// `--resume` asked to continue in 0.12; continuing is the default now.
|
|
130
|
+
bools: ['new', 'restart', 'resume', 'detach', 'dry-run'],
|
|
131
|
+
positionals: 1,
|
|
132
|
+
},
|
|
133
|
+
...Object.fromEntries(
|
|
134
|
+
LIFECYCLE_VERBS.map((verb) => [
|
|
135
|
+
verb,
|
|
136
|
+
{
|
|
137
|
+
usage: `vdx ${verb}`,
|
|
138
|
+
help: `Runs \`mise run ${verb}\` in the current directory — the project's own ${verb}
|
|
139
|
+
command, as mise.toml maps it. Takes no arguments.
|
|
140
|
+
`,
|
|
141
|
+
positionals: 0,
|
|
142
|
+
},
|
|
143
|
+
]),
|
|
144
|
+
),
|
|
145
|
+
};
|
|
146
|
+
|
|
51
147
|
interface ParsedArgs {
|
|
52
148
|
cmd: string;
|
|
53
149
|
positionals: string[];
|
|
@@ -82,6 +178,48 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
82
178
|
return { cmd, positionals, flags };
|
|
83
179
|
}
|
|
84
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Keeps what the command reads and refuses the rest, before anything runs: a
|
|
183
|
+
* misread argument must not run the command. `vdx ai --help` started an agent,
|
|
184
|
+
* `vdx init --help` wrote mise.toml, `vdx down --help` ran `mise run down`.
|
|
185
|
+
*/
|
|
186
|
+
function checkArgs(name: string, opts: ParsedArgs): ParsedArgs {
|
|
187
|
+
const spec = COMMANDS[name]!;
|
|
188
|
+
const bools = [...(spec.bools ?? []), 'help'];
|
|
189
|
+
const values = spec.values ?? [];
|
|
190
|
+
// parseArgs hands the token after a bare flag to that flag as its value; a
|
|
191
|
+
// flag that takes none gives it back — it is the path (or `-h`).
|
|
192
|
+
const positionals = [...opts.positionals];
|
|
193
|
+
const flags: Record<string, string | boolean> = {};
|
|
194
|
+
for (const [flag, value] of Object.entries(opts.flags)) {
|
|
195
|
+
if (bools.includes(flag) && typeof value === 'string') {
|
|
196
|
+
positionals.push(value);
|
|
197
|
+
flags[flag] = true;
|
|
198
|
+
} else {
|
|
199
|
+
flags[flag] = value;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (flags.help || positionals.includes('-h')) {
|
|
203
|
+
process.stdout.write(`Usage: ${spec.usage}\n\n${spec.help}`);
|
|
204
|
+
process.exit(0);
|
|
205
|
+
}
|
|
206
|
+
const fail = (message: string): never => {
|
|
207
|
+
process.stderr.write(`vdx ${name}: ${message}\nUsage: ${spec.usage}\nMore: vdx ${name} --help\n`);
|
|
208
|
+
process.exit(2);
|
|
209
|
+
};
|
|
210
|
+
const unknown = [
|
|
211
|
+
...Object.keys(flags).filter((f) => !bools.includes(f) && !values.includes(f)).map((f) => `--${f}`),
|
|
212
|
+
...positionals.filter((p) => p.startsWith('-')),
|
|
213
|
+
];
|
|
214
|
+
if (unknown.length > 0) fail(`unknown option ${unknown.join(', ')}`);
|
|
215
|
+
const bare = values.filter((f) => flags[f] === true).map((f) => `--${f}`);
|
|
216
|
+
if (bare.length > 0) fail(`${bare.join(', ')} needs a value`);
|
|
217
|
+
if (positionals.length > spec.positionals) {
|
|
218
|
+
fail(`unexpected argument ${positionals.slice(spec.positionals).join(' ')}`);
|
|
219
|
+
}
|
|
220
|
+
return { cmd: opts.cmd, positionals, flags };
|
|
221
|
+
}
|
|
222
|
+
|
|
85
223
|
function cmdAudit(opts: ParsedArgs): void {
|
|
86
224
|
const projectArg = opts.positionals[0] ?? '.';
|
|
87
225
|
const projectRoot = path.resolve(projectArg);
|
|
@@ -263,22 +401,13 @@ function cmdDoctor(opts: ParsedArgs): void {
|
|
|
263
401
|
}
|
|
264
402
|
|
|
265
403
|
function cmdAi(opts: ParsedArgs): void {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return v !== undefined;
|
|
404
|
+
const options = {
|
|
405
|
+
path: opts.positionals[0] ?? '.',
|
|
406
|
+
restart: opts.flags.restart === true,
|
|
407
|
+
fresh: opts.flags.new === true,
|
|
408
|
+
detach: opts.flags.detach === true,
|
|
409
|
+
dryRun: opts.flags['dry-run'] === true,
|
|
273
410
|
};
|
|
274
|
-
const restart = bool('restart');
|
|
275
|
-
const fresh = bool('new');
|
|
276
|
-
// `--resume` asked to continue in 0.12; continuing is the default now.
|
|
277
|
-
// Still read, so the path after it is not taken for its value.
|
|
278
|
-
bool('resume');
|
|
279
|
-
const detach = bool('detach');
|
|
280
|
-
const dryRun = bool('dry-run');
|
|
281
|
-
const options = { path: positionals[0] ?? '.', restart, fresh, detach, dryRun };
|
|
282
411
|
// `ai@m3`: the same command on another machine. Its own label runs here.
|
|
283
412
|
const host = opts.cmd.startsWith('ai@') ? opts.cmd.slice(3) : null;
|
|
284
413
|
if (host !== null) {
|
|
@@ -295,11 +424,16 @@ function cmdAi(opts: ParsedArgs): void {
|
|
|
295
424
|
|
|
296
425
|
const parsed = parseArgs(process.argv);
|
|
297
426
|
if (parsed.cmd === '--version') process.stdout.write(readCliVersion() + '\n');
|
|
298
|
-
else if (
|
|
299
|
-
else if (parsed.cmd === '
|
|
300
|
-
else if (parsed.cmd === '
|
|
301
|
-
else if (parsed.cmd === '
|
|
302
|
-
else if (parsed.cmd === '
|
|
303
|
-
else if (
|
|
427
|
+
else if (['--help', '-h', 'help'].includes(parsed.cmd)) process.stdout.write(USAGE);
|
|
428
|
+
else if (parsed.cmd === 'audit') cmdAudit(checkArgs('audit', parsed));
|
|
429
|
+
else if (parsed.cmd === 'init') cmdInit(checkArgs('init', parsed));
|
|
430
|
+
else if (parsed.cmd === 'publish') cmdPublish(checkArgs('publish', parsed));
|
|
431
|
+
else if (parsed.cmd === 'doctor') cmdDoctor(checkArgs('doctor', parsed));
|
|
432
|
+
else if (parsed.cmd === 'ai' || parsed.cmd.startsWith('ai@')) cmdAi(checkArgs('ai', parsed));
|
|
433
|
+
else if ((LIFECYCLE_VERBS as readonly string[]).includes(parsed.cmd)) {
|
|
434
|
+
checkArgs(parsed.cmd, parsed);
|
|
304
435
|
cmdRun(parsed.cmd as LifecycleVerb);
|
|
305
|
-
else
|
|
436
|
+
} else {
|
|
437
|
+
if (parsed.cmd !== '') process.stderr.write(`vdx: unknown command "${parsed.cmd}"\n`);
|
|
438
|
+
usage();
|
|
439
|
+
}
|