@vodmal/vdx-cli 0.12.0 → 0.13.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 +3 -2
- package/package.json +1 -1
- package/src/ai.ts +39 -13
- package/src/index.ts +6 -3
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ vdx publish <patch|minor|major> [--dry-run] [--force]
|
|
|
48
48
|
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
|
-
vdx ai [project-path] [--
|
|
51
|
+
vdx ai [project-path] [--new] [--restart] [--detach] [--dry-run] # default: cwd
|
|
52
52
|
|
|
53
53
|
# MCP stdio server consumed by the Claude Code plugin
|
|
54
54
|
vdx-mcp --project <path>
|
|
@@ -67,7 +67,8 @@ vdx test # → mise run test (which calls `vitest run` or whatever wa
|
|
|
67
67
|
# 3. Ship a new minor release of a Node lib
|
|
68
68
|
vdx publish minor # bump → npm publish (OTP prompt) → git commit + tag (no push)
|
|
69
69
|
|
|
70
|
-
# 4. Start the agent here
|
|
70
|
+
# 4. Start the agent here, continuing its last conversation (--new: a new one);
|
|
71
|
+
# re-run to attach, --restart to apply a changed profile
|
|
71
72
|
vdx ai
|
|
72
73
|
```
|
|
73
74
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vodmal/vdx-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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
|
@@ -13,6 +13,10 @@ import type { Predicate } from './rubric.ts';
|
|
|
13
13
|
* and hands it to tmux, or runs it in this terminal when tmux is not wanted or
|
|
14
14
|
* not installed.
|
|
15
15
|
*
|
|
16
|
+
* A start continues the project's last conversation (the profile's
|
|
17
|
+
* `resume_args`) — after a reboot `vdx ai` brings the agent back where it was;
|
|
18
|
+
* `--new` starts a new conversation.
|
|
19
|
+
*
|
|
16
20
|
* The profile is personal (which agent, which permissions), so it is never
|
|
17
21
|
* bundled into the CLI: it is read from `$VDX_ENVIRONMENT` or
|
|
18
22
|
* `~/.vdx-environment.yaml`. Without one, vdx starts a plain `claude` or
|
|
@@ -420,8 +424,13 @@ export class Tmux {
|
|
|
420
424
|
}
|
|
421
425
|
}
|
|
422
426
|
|
|
427
|
+
/**
|
|
428
|
+
* `-u`: outside tmux and without a UTF-8 locale (`ssh host vdx ai`, launchd)
|
|
429
|
+
* tmux prints the tabs of PANE_FORMAT as `_`, and every pane would be
|
|
430
|
+
* dropped — the running agent unseen, a second one started.
|
|
431
|
+
*/
|
|
423
432
|
run(args: string[]): string {
|
|
424
|
-
return execFileSync('tmux', this.argv(args), {
|
|
433
|
+
return execFileSync('tmux', ['-u', ...this.argv(args)], {
|
|
425
434
|
encoding: 'utf8',
|
|
426
435
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
427
436
|
});
|
|
@@ -494,7 +503,8 @@ export class Tmux {
|
|
|
494
503
|
export interface AiOptions {
|
|
495
504
|
path: string;
|
|
496
505
|
restart: boolean;
|
|
497
|
-
resume
|
|
506
|
+
/** `--new`: start a new conversation — leave out the profile's resume args. */
|
|
507
|
+
fresh: boolean;
|
|
498
508
|
detach: boolean;
|
|
499
509
|
dryRun: boolean;
|
|
500
510
|
}
|
|
@@ -539,6 +549,13 @@ export const EXIT_USAGE = 2;
|
|
|
539
549
|
export const EXIT_DRIFT = 3;
|
|
540
550
|
export const EXIT_LAUNCH_FAILED = 4;
|
|
541
551
|
|
|
552
|
+
/**
|
|
553
|
+
* Without tmux the agent runs in the foreground. One that fails this soon after
|
|
554
|
+
* a start with the resume args had nothing to resume (`claude --continue` exits
|
|
555
|
+
* 1 at once); the window covers answering the channel prompt that comes first.
|
|
556
|
+
*/
|
|
557
|
+
export const NOTHING_TO_RESUME_MS = 30_000;
|
|
558
|
+
|
|
542
559
|
export function renderPlan(plan: LaunchPlan, multiplexer: Multiplexer, running: AgentPane[]): string {
|
|
543
560
|
const lines = [
|
|
544
561
|
`vdx ai — ${plan.projectRoot}`,
|
|
@@ -653,9 +670,9 @@ function settle(plan: LaunchPlan, paneId: string, deps: AiDeps): SettleResult {
|
|
|
653
670
|
}
|
|
654
671
|
|
|
655
672
|
/**
|
|
656
|
-
* Settle a pane
|
|
657
|
-
* resume exits at once; start it once more without them rather
|
|
658
|
-
* project without an agent.
|
|
673
|
+
* Settle a pane started with the plan's resume args, if it has any. An agent
|
|
674
|
+
* with nothing to resume exits at once; start it once more without them rather
|
|
675
|
+
* than leave the project without an agent.
|
|
659
676
|
*/
|
|
660
677
|
function settleResumed(plan: LaunchPlan, paneId: string, deps: AiDeps): boolean {
|
|
661
678
|
const result = settle(plan, paneId, deps);
|
|
@@ -719,6 +736,8 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
|
719
736
|
log(`vdx ai: ${profilePath ?? 'profile'}: ${e?.message ?? e}`);
|
|
720
737
|
return EXIT_USAGE;
|
|
721
738
|
}
|
|
739
|
+
// A start continues the last conversation unless asked for a new one.
|
|
740
|
+
if (opts.fresh) plan = { ...plan, resumeArgs: [] };
|
|
722
741
|
|
|
723
742
|
let multiplexer = plan.multiplexer;
|
|
724
743
|
if (multiplexer === 'tmux' && !tmux.available()) {
|
|
@@ -746,12 +765,17 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
|
746
765
|
return EXIT_USAGE;
|
|
747
766
|
}
|
|
748
767
|
for (const c of plan.confirm) log(`note: the agent will ask "${c.screen}" — answer it with ${c.keys.join(' ')}`);
|
|
749
|
-
const
|
|
750
|
-
|
|
768
|
+
const startedAt = Date.now();
|
|
769
|
+
let res = spawnSync(plan.command, [...plan.args, ...plan.resumeArgs], { cwd: projectRoot, stdio: 'inherit' });
|
|
751
770
|
if ((res.error as NodeJS.ErrnoException | undefined)?.code === 'ENOENT') {
|
|
752
771
|
log(`vdx ai: \`${plan.command}\` not found on PATH`);
|
|
753
772
|
return 127;
|
|
754
773
|
}
|
|
774
|
+
const failedAtOnce = res.status !== null && res.status !== 0 && Date.now() - startedAt < NOTHING_TO_RESUME_MS;
|
|
775
|
+
if (plan.resumeArgs.length > 0 && failedAtOnce) {
|
|
776
|
+
log(`↻ nothing to resume — starting without ${plan.resumeArgs.join(' ')}: ${commandLine(plan.command, plan.args)}`);
|
|
777
|
+
res = spawnSync(plan.command, plan.args, { cwd: projectRoot, stdio: 'inherit' });
|
|
778
|
+
}
|
|
755
779
|
return res.status ?? 1;
|
|
756
780
|
}
|
|
757
781
|
|
|
@@ -764,14 +788,17 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
|
764
788
|
}
|
|
765
789
|
|
|
766
790
|
if (current && !opts.restart) {
|
|
791
|
+
if (opts.fresh) log(`note: ${current.pane.session} already runs ${plan.command} — --new replaces it only with --restart`);
|
|
767
792
|
const missing = missingArgs(current.proc.args, plan.args);
|
|
768
793
|
if (missing.length > 0) {
|
|
769
794
|
log(`✗ ${current.pane.session}: ${plan.command} is running without ${missing.join(' ')}`);
|
|
770
795
|
log(
|
|
771
|
-
` to apply the profile: vdx ai --restart ${shellQuote(projectRoot)}` +
|
|
796
|
+
` to apply the profile: vdx ai --restart${opts.fresh ? ' --new' : ''} ${shellQuote(projectRoot)}` +
|
|
772
797
|
(plan.resumeArgs.length
|
|
773
798
|
? ` (stops the running agent, then resumes the conversation with ${plan.resumeArgs.join(' ')})`
|
|
774
|
-
:
|
|
799
|
+
: opts.fresh
|
|
800
|
+
? ' (stops the running agent and starts a new conversation)'
|
|
801
|
+
: ' (stops the running agent; the conversation is not resumed — the profile has no resume_args)'),
|
|
775
802
|
);
|
|
776
803
|
return EXIT_DRIFT;
|
|
777
804
|
}
|
|
@@ -786,14 +813,13 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
|
786
813
|
return finish(settleResumed(plan, current.pane.paneId, deps), current.pane.session, current.pane.paneId, opts, deps);
|
|
787
814
|
}
|
|
788
815
|
|
|
789
|
-
if (opts.restart) log(`note: no running ${plan.command} in this project — starting
|
|
790
|
-
const line = commandLine(plan.command, [...plan.args, ...
|
|
816
|
+
if (opts.restart) log(`note: no running ${plan.command} in this project — starting one`);
|
|
817
|
+
const line = commandLine(plan.command, [...plan.args, ...plan.resumeArgs]);
|
|
791
818
|
const cmd = tmuxShellCommand(line, deps.shell);
|
|
792
819
|
const reuse = tmux.hasSession(plan.sessionName);
|
|
793
820
|
const paneId = reuse
|
|
794
821
|
? tmux.newWindow(plan.sessionName, projectRoot, cmd)
|
|
795
822
|
: tmux.newSession(plan.sessionName, projectRoot, cmd);
|
|
796
823
|
log(`▶ ${plan.sessionName}${reuse ? ' (new window)' : ''}: ${line}`);
|
|
797
|
-
|
|
798
|
-
return finish(ok, plan.sessionName, paneId, opts, deps);
|
|
824
|
+
return finish(settleResumed(plan, paneId, deps), plan.sessionName, paneId, opts, deps);
|
|
799
825
|
}
|
package/src/index.ts
CHANGED
|
@@ -41,7 +41,7 @@ function usage(): never {
|
|
|
41
41
|
vdx init [project_path] [--stack <id>] [--baseline <ref>] [--dry-run] [--force] (default: cwd)
|
|
42
42
|
vdx publish <patch|minor|major> [--dry-run] [--force]
|
|
43
43
|
vdx doctor [--format=ansi|markdown|json] [--json]
|
|
44
|
-
vdx ai [project_path] [--
|
|
44
|
+
vdx ai [project_path] [--new] [--restart] [--detach] [--dry-run] (default: cwd)
|
|
45
45
|
`,
|
|
46
46
|
);
|
|
47
47
|
process.exit(1);
|
|
@@ -271,11 +271,14 @@ function cmdAi(opts: ParsedArgs): void {
|
|
|
271
271
|
return v !== undefined;
|
|
272
272
|
};
|
|
273
273
|
const restart = bool('restart');
|
|
274
|
-
const
|
|
274
|
+
const fresh = bool('new');
|
|
275
|
+
// `--resume` asked to continue in 0.12; continuing is the default now.
|
|
276
|
+
// Still read, so the path after it is not taken for its value.
|
|
277
|
+
bool('resume');
|
|
275
278
|
const detach = bool('detach');
|
|
276
279
|
const dryRun = bool('dry-run');
|
|
277
280
|
process.exit(
|
|
278
|
-
runAi({ path: positionals[0] ?? '.', restart,
|
|
281
|
+
runAi({ path: positionals[0] ?? '.', restart, fresh, detach, dryRun }, defaultDeps()),
|
|
279
282
|
);
|
|
280
283
|
}
|
|
281
284
|
|