@vodmal/vdx-cli 0.22.0 → 0.23.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 +1 -1
- package/package.json +1 -1
- package/rubric/vdx-rubric.yaml +1 -1
- package/src/ai.ts +165 -36
- package/src/conversations.ts +6 -3
- package/src/index.ts +10 -3
- package/src/init.ts +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ vdx doctor --check [project-path] # one line per hook declared but not running
|
|
|
51
51
|
vdx doctor --fix # write the profile's personal hooks (git.hooks) into ~/.gitconfig
|
|
52
52
|
|
|
53
53
|
# Start your agent in a project per your profile (~/.vdx-environment.yaml or $VDX_ENVIRONMENT)
|
|
54
|
-
vdx ai[@host] [project-path] [--new | --conversation <id>] [--restart] [--detach] [--dry-run] # default: cwd; @host: over ssh, in tmux there
|
|
54
|
+
vdx ai[@host] [project-path] [--new | --conversation <id>] [--focused] [--restart] [--detach] [--dry-run] # default: cwd; @host: over ssh, in tmux there
|
|
55
55
|
vdx ai --check [project-path] # this session against the profile — what the plugin's SessionStart hook tells the agent
|
|
56
56
|
|
|
57
57
|
# What a command does and takes; nothing runs (ai: also where launch flags live)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vodmal/vdx-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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/rubric/vdx-rubric.yaml
CHANGED
package/src/ai.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface WhenRule {
|
|
|
57
57
|
if: Predicate;
|
|
58
58
|
args?: string[];
|
|
59
59
|
confirm?: ConfirmRule[];
|
|
60
|
+
/** The rule is there to wake the agent: a focused session (`vdx ai --focused`) leaves it out. */
|
|
61
|
+
wakes?: boolean;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
export interface AgentProfile {
|
|
@@ -116,6 +118,13 @@ export interface Environment {
|
|
|
116
118
|
|
|
117
119
|
export const ENVIRONMENT_ENV_VAR = 'VDX_ENVIRONMENT';
|
|
118
120
|
export const HOST_ENV_VAR = 'VDX_HOST';
|
|
121
|
+
/** Set by `vdx ai@<host>` for the vdx it starts there: the machine the person sits at. */
|
|
122
|
+
export const SEAT_ENV_VAR = 'VDX_AI_FROM';
|
|
123
|
+
/**
|
|
124
|
+
* `1` in the environment of an agent started with `vdx ai --focused`, and so of
|
|
125
|
+
* its hooks and MCP servers: nothing wakes this session or reads its inbox for it.
|
|
126
|
+
*/
|
|
127
|
+
export const FOCUSED_ENV_VAR = 'VDX_FOCUSED';
|
|
119
128
|
export const USER_ENVIRONMENT_FILE = '.vdx-environment.yaml';
|
|
120
129
|
/** Tried in order when no profile names an agent. Started plain, with no flags. */
|
|
121
130
|
export const FALLBACK_AGENTS = ['claude', 'codex'] as const;
|
|
@@ -168,6 +177,7 @@ export function parseEnvironment(text: string, source: string): Environment {
|
|
|
168
177
|
if (rule.args !== undefined && !isStringList(rule.args)) {
|
|
169
178
|
fail(`${at}.args`, 'must be a list of strings');
|
|
170
179
|
}
|
|
180
|
+
if (rule.wakes !== undefined && typeof rule.wakes !== 'boolean') fail(`${at}.wakes`, 'must be true or false');
|
|
171
181
|
if (rule.confirm !== undefined) {
|
|
172
182
|
if (!Array.isArray(rule.confirm)) fail(`${at}.confirm`, 'must be a list');
|
|
173
183
|
rule.confirm.forEach((c, j) => {
|
|
@@ -279,12 +289,18 @@ export interface LaunchPlan {
|
|
|
279
289
|
/** Where `project` came from: mise.toml, a profile source, or the repository name. */
|
|
280
290
|
projectSource: string;
|
|
281
291
|
host: string;
|
|
292
|
+
/** The machine the person sits at: `host`, or the one that ran `vdx ai@<host>`. */
|
|
293
|
+
seat: string;
|
|
282
294
|
profilePath: string | null;
|
|
283
295
|
command: string;
|
|
284
296
|
/** agent.args plus the args of every matched `when` rule — what a running agent must carry. */
|
|
285
297
|
args: string[];
|
|
286
298
|
resumeArgs: string[];
|
|
287
299
|
matched: string[];
|
|
300
|
+
/** `vdx ai --focused`: the agent gets VDX_FOCUSED=1, and the rules that wake it are left out. */
|
|
301
|
+
focused: boolean;
|
|
302
|
+
/** Matched rules a focused session leaves out (`wakes: true`). */
|
|
303
|
+
skipped: string[];
|
|
288
304
|
confirm: ConfirmRule[];
|
|
289
305
|
multiplexer: Multiplexer;
|
|
290
306
|
sessionName: string;
|
|
@@ -300,14 +316,22 @@ export function planLaunch(input: {
|
|
|
300
316
|
project: string;
|
|
301
317
|
projectSource?: string;
|
|
302
318
|
host: string;
|
|
319
|
+
seat?: string;
|
|
320
|
+
focused?: boolean;
|
|
303
321
|
}): LaunchPlan {
|
|
304
322
|
const { environment, agent, projectRoot } = input;
|
|
323
|
+
const focused = input.focused === true;
|
|
305
324
|
const ctx: Ctx = { projectRoot, stack: autoDetectStack(projectRoot), cache: new Map() };
|
|
306
325
|
const args = [...(agent.args ?? [])];
|
|
307
326
|
const matched: string[] = [];
|
|
327
|
+
const skipped: string[] = [];
|
|
308
328
|
const confirm: ConfirmRule[] = [];
|
|
309
329
|
for (const rule of agent.when ?? []) {
|
|
310
330
|
if (!evalPredicate(rule.if, ctx)) continue;
|
|
331
|
+
if (focused && rule.wakes) {
|
|
332
|
+
skipped.push(rule.id);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
311
335
|
matched.push(rule.id);
|
|
312
336
|
args.push(...(rule.args ?? []));
|
|
313
337
|
confirm.push(...(rule.confirm ?? []));
|
|
@@ -317,11 +341,14 @@ export function planLaunch(input: {
|
|
|
317
341
|
project: input.project,
|
|
318
342
|
projectSource: input.projectSource ?? 'repository name',
|
|
319
343
|
host: input.host,
|
|
344
|
+
seat: input.seat ?? input.host,
|
|
320
345
|
profilePath: input.profilePath,
|
|
321
346
|
command: agent.command,
|
|
322
347
|
args,
|
|
323
348
|
resumeArgs: [...(agent.resume_args ?? [])],
|
|
324
349
|
matched,
|
|
350
|
+
focused,
|
|
351
|
+
skipped,
|
|
325
352
|
confirm,
|
|
326
353
|
multiplexer: environment.session?.multiplexer ?? 'none',
|
|
327
354
|
sessionName: renderSessionName(environment.session?.name ?? DEFAULT_SESSION_NAME, {
|
|
@@ -535,6 +562,16 @@ export function hostLabel(env: NodeJS.ProcessEnv = process.env): string {
|
|
|
535
562
|
return label || os.hostname().split('.')[0]!;
|
|
536
563
|
}
|
|
537
564
|
|
|
565
|
+
/** The machine the person sits at: the one whose `vdx ai@<host>` started this vdx, else this one. */
|
|
566
|
+
export function seatLabel(env: NodeJS.ProcessEnv = process.env): string {
|
|
567
|
+
return env[SEAT_ENV_VAR]?.trim() || hostLabel(env);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** Machine labels compare without case: `M3` in the profile is `m3` in a transcript. */
|
|
571
|
+
export function sameMachine(a: string | null, b: string | null): boolean {
|
|
572
|
+
return (a ?? '').toLowerCase() === (b ?? '').toLowerCase();
|
|
573
|
+
}
|
|
574
|
+
|
|
538
575
|
export function onPath(bin: string, env: NodeJS.ProcessEnv = process.env): boolean {
|
|
539
576
|
for (const dir of (env['PATH'] ?? '').split(path.delimiter)) {
|
|
540
577
|
if (!dir) continue;
|
|
@@ -654,6 +691,8 @@ export interface AiOptions {
|
|
|
654
691
|
conversation?: string;
|
|
655
692
|
detach: boolean;
|
|
656
693
|
dryRun: boolean;
|
|
694
|
+
/** `--focused`: a session nothing wakes — VDX_FOCUSED=1, the profile's `wakes` rules left out. */
|
|
695
|
+
focused?: boolean;
|
|
657
696
|
}
|
|
658
697
|
|
|
659
698
|
export interface AiDeps {
|
|
@@ -667,6 +706,8 @@ export interface AiDeps {
|
|
|
667
706
|
out: (text: string) => void;
|
|
668
707
|
onPath: (bin: string) => boolean;
|
|
669
708
|
processes: () => ProcInfo[];
|
|
709
|
+
/** Was this agent process started focused; default: its environment (`processFocused`). */
|
|
710
|
+
agentFocused?: (pid: number) => boolean;
|
|
670
711
|
sleep: (ms: number) => void;
|
|
671
712
|
confirmTimeoutMs: number;
|
|
672
713
|
/** One line from the person at the terminal, or null when there is none. */
|
|
@@ -742,15 +783,21 @@ export const EXIT_LAUNCH_FAILED = 4;
|
|
|
742
783
|
*/
|
|
743
784
|
export const NOTHING_TO_RESUME_MS = 30_000;
|
|
744
785
|
|
|
745
|
-
export function renderPlan(
|
|
786
|
+
export function renderPlan(
|
|
787
|
+
plan: LaunchPlan,
|
|
788
|
+
multiplexer: Multiplexer,
|
|
789
|
+
running: AgentPane[],
|
|
790
|
+
focusedOf: (pid: number) => boolean = () => false,
|
|
791
|
+
): string {
|
|
746
792
|
const lines = [
|
|
747
793
|
`vdx ai — ${plan.projectRoot}`,
|
|
748
794
|
` name: ${plan.project} (${plan.projectSource})`,
|
|
749
795
|
` profile: ${plan.profilePath ?? `none — built-in default (${FALLBACK_AGENTS.join(' or ')}, no flags, no tmux)`}`,
|
|
750
|
-
` agent: ${
|
|
796
|
+
` agent: ${agentLine(plan, plan.args)}`,
|
|
751
797
|
` resume: ${plan.resumeArgs.length ? plan.resumeArgs.join(' ') : '—'}`,
|
|
752
798
|
` matched: ${plan.matched.length ? plan.matched.join(', ') : '—'}`,
|
|
753
799
|
];
|
|
800
|
+
if (plan.focused) lines.push(` focused: nothing wakes it; left out: ${plan.skipped.length ? plan.skipped.join(', ') : '—'}`);
|
|
754
801
|
for (const c of plan.confirm) lines.push(` confirm: "${c.screen}" → ${c.keys.join(' ')}`);
|
|
755
802
|
lines.push(
|
|
756
803
|
multiplexer === 'tmux'
|
|
@@ -761,10 +808,11 @@ export function renderPlan(plan: LaunchPlan, multiplexer: Multiplexer, running:
|
|
|
761
808
|
if (running.length === 0) lines.push(' running: —');
|
|
762
809
|
for (const r of running) {
|
|
763
810
|
const missing = missingArgs(r.proc.args, plan.args);
|
|
811
|
+
const focused = focusedOf(r.proc.pid);
|
|
764
812
|
lines.push(
|
|
765
813
|
` running: ${r.pane.session} ${r.pane.paneId} — ${
|
|
766
814
|
missing.length ? `missing ${missing.join(' ')}` : 'matches the profile'
|
|
767
|
-
}`,
|
|
815
|
+
}${focused ? ', focused' : plan.focused ? ', not focused' : ''}`,
|
|
768
816
|
);
|
|
769
817
|
}
|
|
770
818
|
}
|
|
@@ -864,7 +912,7 @@ function settle(plan: LaunchPlan, paneId: string, deps: AiDeps): SettleResult {
|
|
|
864
912
|
function settleResumed(plan: LaunchPlan, paneId: string, deps: AiDeps): boolean {
|
|
865
913
|
const result = settle(plan, paneId, deps);
|
|
866
914
|
if (result !== 'not-running' || plan.resumeArgs.length === 0) return result === 'ok';
|
|
867
|
-
const line =
|
|
915
|
+
const line = agentLine(plan, plan.args);
|
|
868
916
|
deps.log(`↻ nothing to resume — starting without ${plan.resumeArgs.join(' ')}: ${line}`);
|
|
869
917
|
deps.tmux.respawn(paneId, plan.projectRoot, tmuxShellCommand(line, deps.shell));
|
|
870
918
|
return settle(plan, paneId, deps) === 'ok';
|
|
@@ -895,9 +943,12 @@ export function remotePathWord(abs: string, home: string): string {
|
|
|
895
943
|
* The ssh argv for `vdx ai@<host>`: the same `vdx ai` on the host, in its own
|
|
896
944
|
* tmux there. The path goes first: an older vdx hands the token after a flag it
|
|
897
945
|
* does not know to that flag. A different vdx version there is named, not fatal.
|
|
946
|
+
* `from` — the machine the person sits at, so the vdx there speaks from it; an
|
|
947
|
+
* older vdx leaves the variable alone.
|
|
898
948
|
*/
|
|
899
949
|
export function remoteAiArgs(input: {
|
|
900
950
|
host: string;
|
|
951
|
+
from: string;
|
|
901
952
|
projectPath: string;
|
|
902
953
|
home: string;
|
|
903
954
|
version: string;
|
|
@@ -907,6 +958,7 @@ export function remoteAiArgs(input: {
|
|
|
907
958
|
const { host, opts, version } = input;
|
|
908
959
|
const flags = [
|
|
909
960
|
opts.fresh && '--new',
|
|
961
|
+
opts.focused && '--focused',
|
|
910
962
|
opts.restart && '--restart',
|
|
911
963
|
opts.detach && '--detach',
|
|
912
964
|
opts.dryRun && '--dry-run',
|
|
@@ -916,6 +968,7 @@ export function remoteAiArgs(input: {
|
|
|
916
968
|
const script =
|
|
917
969
|
`v=$(vdx --version 2>/dev/null) || v='older than 0.13.1'; ` +
|
|
918
970
|
`[ "$v" = ${v} ] || echo "note: vdx on ${host} is $v, here "${v} >&2; ` +
|
|
971
|
+
`export ${SEAT_ENV_VAR}=${shellQuote(input.from)}; ` +
|
|
919
972
|
`exec vdx ai ${[remotePathWord(input.projectPath, input.home), ...flags].join(' ')}`;
|
|
920
973
|
return [input.tty ? '-t' : '-T', host, script];
|
|
921
974
|
}
|
|
@@ -975,10 +1028,11 @@ export function ensureAuthor(projectRoot: string, poolDirs: string[], deps: AiDe
|
|
|
975
1028
|
}
|
|
976
1029
|
}
|
|
977
1030
|
|
|
978
|
-
|
|
1031
|
+
/** `from` — where the person sits; a vdx that `vdx ai@<host>` started passes its own on. */
|
|
1032
|
+
export function runAiRemote(opts: AiOptions, host: string, deps: AiDeps, from: string = seatLabel(deps.env)): number {
|
|
979
1033
|
const projectPath = resolveProjectRoot(opts.path) ?? path.resolve(opts.path);
|
|
980
1034
|
const tty = deps.interactive && !opts.detach && !opts.dryRun;
|
|
981
|
-
const args = remoteAiArgs({ host, projectPath, home: deps.home, version, opts, tty });
|
|
1035
|
+
const args = remoteAiArgs({ host, from, projectPath, home: deps.home, version: deps.version ?? '', opts, tty });
|
|
982
1036
|
deps.log(`→ ${host}: vdx ai ${shellQuote(projectPath)}${opts.conversation ? ` --conversation ${opts.conversation}` : ''}`);
|
|
983
1037
|
const res = (deps.ssh ?? runSsh)(args, false);
|
|
984
1038
|
if (res === null) {
|
|
@@ -1051,9 +1105,12 @@ const resumeId = (id: string) => ['--resume', id];
|
|
|
1051
1105
|
export function chooseConversation(plan: LaunchPlan, deps: AiDeps, canAsk: boolean): ConversationChoice {
|
|
1052
1106
|
if (!isClaude(plan.command)) return { args: plan.resumeArgs, lines: [] };
|
|
1053
1107
|
const host = plan.host;
|
|
1054
|
-
const same =
|
|
1108
|
+
const same = sameMachine;
|
|
1055
1109
|
const all = plan.resumeArgs.length === 0 ? [] : listConversations({ env: deps.env, home: deps.home, projectRoot: plan.projectRoot });
|
|
1056
1110
|
const root = shellQuote(plan.projectRoot);
|
|
1111
|
+
// Said from where the person sits: under `vdx ai@<host>` that is not this machine.
|
|
1112
|
+
const atSeat = same(plan.seat, host);
|
|
1113
|
+
const on = (m: string) => (same(m, plan.seat) ? (atSeat ? 'here' : `on ${m}, where you are,`) : `on ${m}`);
|
|
1057
1114
|
|
|
1058
1115
|
// Who runs what right now: this machine from its session files, the others over ssh.
|
|
1059
1116
|
const reach = new Map<string, MachineReport | null>();
|
|
@@ -1064,9 +1121,18 @@ export function chooseConversation(plan: LaunchPlan, deps: AiDeps, canAsk: boole
|
|
|
1064
1121
|
const elsewhere = [...reach].flatMap(([m, r]) =>
|
|
1065
1122
|
(r?.sessions ?? []).filter((l) => runsInProject(l, r!.home, plan.projectRoot, deps.home)).map((l) => ({ m, l })),
|
|
1066
1123
|
);
|
|
1124
|
+
// The project's path on the machine `m`, by its home: the person types it there.
|
|
1125
|
+
const rootOn = (m: string) => {
|
|
1126
|
+
const theirs = [...reach].find(([k]) => same(k, m))?.[1]?.home;
|
|
1127
|
+
const rel = path.relative(deps.home, plan.projectRoot);
|
|
1128
|
+
return theirs && !rel.startsWith('..') && !path.isAbsolute(rel) ? shellQuote(path.join(theirs, rel)) : root;
|
|
1129
|
+
};
|
|
1130
|
+
// `vdx ai` attaches to an agent in tmux; one in a terminal of its own has no command to reach it.
|
|
1131
|
+
const reachAgent = (m: string, l: LiveSession) =>
|
|
1132
|
+
!l.tmux ? '' : same(m, plan.seat) ? ` — attach: vdx ai ${rootOn(m)}` : ` — there: vdx ai@${m} ${root}`;
|
|
1067
1133
|
const fresh = (lines: string[]): ConversationChoice => {
|
|
1068
1134
|
if (elsewhere.length === 0) return { args: [], lines };
|
|
1069
|
-
const agents = elsewhere.map(({ m, l }) => `an agent of this project is ${describeLive(m, l
|
|
1135
|
+
const agents = elsewhere.map(({ m, l }) => `an agent of this project is ${describeLive(on(m), l)}${reachAgent(m, l)}`);
|
|
1070
1136
|
return { args: [], elsewhere: [...new Set(elsewhere.map(({ m }) => m))], lines: [...lines, ...agents] };
|
|
1071
1137
|
};
|
|
1072
1138
|
if (plan.resumeArgs.length === 0) return fresh(silent);
|
|
@@ -1086,7 +1152,7 @@ export function chooseConversation(plan: LaunchPlan, deps: AiDeps, canAsk: boole
|
|
|
1086
1152
|
(a, b) => b.updated.getTime() - a.updated.getTime(),
|
|
1087
1153
|
);
|
|
1088
1154
|
const show = (c: Conversation) => describeConversation(c, machine(c));
|
|
1089
|
-
const running = (c: Conversation) => (live.get(c.id) ?? []).map((l) => describeLive(l.machine, l.session
|
|
1155
|
+
const running = (c: Conversation) => (live.get(c.id) ?? []).map((l) => describeLive(on(l.machine), l.session));
|
|
1090
1156
|
|
|
1091
1157
|
const take = (c: Conversation): ConversationChoice => {
|
|
1092
1158
|
const m = machine(c);
|
|
@@ -1094,7 +1160,7 @@ export function chooseConversation(plan: LaunchPlan, deps: AiDeps, canAsk: boole
|
|
|
1094
1160
|
if (here) {
|
|
1095
1161
|
return {
|
|
1096
1162
|
args: [],
|
|
1097
|
-
askHere: { id: c.id, why: `It runs
|
|
1163
|
+
askHere: { id: c.id, why: `It runs ${on(host)} right now (${here.session.tmux ?? `pid ${here.session.pid}`}); two sessions would write to it` },
|
|
1098
1164
|
lines: [`chosen: ${show(c)}`],
|
|
1099
1165
|
};
|
|
1100
1166
|
}
|
|
@@ -1102,7 +1168,7 @@ export function chooseConversation(plan: LaunchPlan, deps: AiDeps, canAsk: boole
|
|
|
1102
1168
|
if (m) return { args: [], remote: { host: m, id: c.id, reachable: reach.get(m) != null }, lines: [`continuing on ${m}: ${show(c)}`] };
|
|
1103
1169
|
return {
|
|
1104
1170
|
args: [],
|
|
1105
|
-
askHere: { id: c.id, why:
|
|
1171
|
+
askHere: { id: c.id, why: `Its machine is unknown (never typed in ${on(host)}); if it runs there, both machines will write to it` },
|
|
1106
1172
|
lines: [`chosen: ${show(c)}`],
|
|
1107
1173
|
};
|
|
1108
1174
|
};
|
|
@@ -1115,7 +1181,7 @@ export function chooseConversation(plan: LaunchPlan, deps: AiDeps, canAsk: boole
|
|
|
1115
1181
|
if (canAsk && deps.interactive) {
|
|
1116
1182
|
deps.log('conversations of this project:');
|
|
1117
1183
|
found.forEach((c, i) => {
|
|
1118
|
-
const marks = [i === 0 && 'newest', c === firstIdle && 'this machine', !machine(c) && 'machine unknown', ...running(c)];
|
|
1184
|
+
const marks = [i === 0 && 'newest', c === firstIdle && (atSeat ? 'this machine' : `idle on ${host}`), !machine(c) && 'machine unknown', ...running(c)];
|
|
1119
1185
|
const m = marks.filter(Boolean);
|
|
1120
1186
|
deps.log(` ${i + 1}) ${show(c)}${m.length ? ` ← ${m.join(', ')}` : ''}`);
|
|
1121
1187
|
});
|
|
@@ -1142,16 +1208,17 @@ export function chooseConversation(plan: LaunchPlan, deps: AiDeps, canAsk: boole
|
|
|
1142
1208
|
`${c.updated > (firstIdle?.updated ?? new Date(0)) ? 'a newer' : 'another'} conversation ${c.id.slice(0, 8)}` +
|
|
1143
1209
|
(now.length ? `, ${now.join(', ')}` : '');
|
|
1144
1210
|
if (runsHere(c)) return `note: ${which} — not taken: it would have two sessions`;
|
|
1211
|
+
if (m && same(m, plan.seat)) return `note: ${m} has ${which} — continue it where you are: vdx ai ${rootOn(m)} --conversation ${c.id}`;
|
|
1145
1212
|
return m
|
|
1146
1213
|
? `note: ${m} has ${which} — continue it there: vdx ai@${m} ${root} --conversation ${c.id}`
|
|
1147
|
-
: `note: ${which} is of an unknown machine (never typed in
|
|
1214
|
+
: `note: ${which} is of an unknown machine (never typed in ${on(host)}) — on its machine: vdx ai@<machine> ${root} --conversation ${c.id}`;
|
|
1148
1215
|
});
|
|
1149
1216
|
notes.push(...silent);
|
|
1150
1217
|
if (firstIdle) {
|
|
1151
1218
|
const chosen = take(firstIdle);
|
|
1152
1219
|
return { ...chosen, lines: [...chosen.lines, ...notes] };
|
|
1153
1220
|
}
|
|
1154
|
-
return fresh([
|
|
1221
|
+
return fresh([`no idle conversation ${atSeat ? 'of this machine' : `on ${host}`} — starting a new one`, ...notes]);
|
|
1155
1222
|
}
|
|
1156
1223
|
|
|
1157
1224
|
interface Prepared {
|
|
@@ -1160,10 +1227,12 @@ interface Prepared {
|
|
|
1160
1227
|
profilePath: string | null;
|
|
1161
1228
|
profileDir: string;
|
|
1162
1229
|
plan: LaunchPlan;
|
|
1230
|
+
/** The same plan for a focused session or a plain one. */
|
|
1231
|
+
replan: (focused: boolean) => LaunchPlan;
|
|
1163
1232
|
}
|
|
1164
1233
|
|
|
1165
1234
|
/** The project, the profile and the launch plan for `pathArg`; an exit code when there is none. */
|
|
1166
|
-
function prepare(pathArg: string, deps: AiDeps): Prepared | number {
|
|
1235
|
+
function prepare(pathArg: string, deps: AiDeps, focused = false): Prepared | number {
|
|
1167
1236
|
const { log } = deps;
|
|
1168
1237
|
|
|
1169
1238
|
const projectRoot = resolveProjectRoot(pathArg);
|
|
@@ -1201,9 +1270,8 @@ function prepare(pathArg: string, deps: AiDeps): Prepared | number {
|
|
|
1201
1270
|
profileDir,
|
|
1202
1271
|
});
|
|
1203
1272
|
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
plan = planLaunch({
|
|
1273
|
+
const replan = (asFocused: boolean) =>
|
|
1274
|
+
planLaunch({
|
|
1207
1275
|
environment,
|
|
1208
1276
|
profilePath,
|
|
1209
1277
|
agent,
|
|
@@ -1211,12 +1279,38 @@ function prepare(pathArg: string, deps: AiDeps): Prepared | number {
|
|
|
1211
1279
|
project: named.name,
|
|
1212
1280
|
projectSource: named.source,
|
|
1213
1281
|
host: hostLabel(deps.env),
|
|
1282
|
+
seat: seatLabel(deps.env),
|
|
1283
|
+
focused: asFocused,
|
|
1214
1284
|
});
|
|
1285
|
+
let plan: LaunchPlan;
|
|
1286
|
+
try {
|
|
1287
|
+
plan = replan(focused);
|
|
1215
1288
|
} catch (e: any) {
|
|
1216
1289
|
log(`vdx ai: ${profilePath ?? 'profile'}: ${e?.message ?? e}`);
|
|
1217
1290
|
return EXIT_USAGE;
|
|
1218
1291
|
}
|
|
1219
|
-
return { projectRoot, environment, profilePath, profileDir, plan };
|
|
1292
|
+
return { projectRoot, environment, profilePath, profileDir, plan, replan };
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/** Was the process started focused — VDX_FOCUSED=1 in its environment (`ps -E` on macOS, /proc on Linux)? */
|
|
1296
|
+
export function processFocused(pid: number): boolean {
|
|
1297
|
+
const mark = `${FOCUSED_ENV_VAR}=1`;
|
|
1298
|
+
try {
|
|
1299
|
+
const environ = `/proc/${pid}/environ`;
|
|
1300
|
+
if (fs.existsSync(environ)) return fs.readFileSync(environ, 'utf8').split('\0').includes(mark);
|
|
1301
|
+
const out = execFileSync('ps', ['-E', '-ww', '-o', 'command=', '-p', String(pid)], {
|
|
1302
|
+
encoding: 'utf8',
|
|
1303
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1304
|
+
});
|
|
1305
|
+
return out.split(/\s+/).includes(mark);
|
|
1306
|
+
} catch {
|
|
1307
|
+
return false;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/** The agent's command line; a focused one carries VDX_FOCUSED=1 in front, for the shell in its tmux pane. */
|
|
1312
|
+
function agentLine(plan: LaunchPlan, args: string[]): string {
|
|
1313
|
+
return `${plan.focused ? `${FOCUSED_ENV_VAR}=1 ` : ''}${commandLine(plan.command, args)}`;
|
|
1220
1314
|
}
|
|
1221
1315
|
|
|
1222
1316
|
/** The nearest ancestor of `pid` that is the agent: the session a hook or a shell tool runs in. */
|
|
@@ -1237,9 +1331,12 @@ export function findOwnAgent(procs: ProcInfo[], pid: number, command: string): P
|
|
|
1237
1331
|
*/
|
|
1238
1332
|
export function runAiCheck(pathArg: string, deps: AiDeps, selfPid: number = process.pid): number {
|
|
1239
1333
|
if (!resolveEnvironmentPath(deps.env, deps.home)) return EXIT_OK;
|
|
1240
|
-
|
|
1334
|
+
// The hook runs in the agent's environment: a focused session's carries VDX_FOCUSED=1.
|
|
1335
|
+
const focused = deps.env[FOCUSED_ENV_VAR] === '1';
|
|
1336
|
+
const prepared = prepare(pathArg, deps, focused);
|
|
1241
1337
|
if (typeof prepared === 'number') return prepared;
|
|
1242
1338
|
const { projectRoot, profilePath, plan } = prepared;
|
|
1339
|
+
const vdxAi = `vdx ai${focused ? ' --focused' : ''}`;
|
|
1243
1340
|
|
|
1244
1341
|
const lines = [
|
|
1245
1342
|
`${machineLine(plan.host)}; the agent in this project is started with \`vdx ai\`, and its flags come from the profile ` +
|
|
@@ -1248,6 +1345,14 @@ export function runAiCheck(pathArg: string, deps: AiDeps, selfPid: number = proc
|
|
|
1248
1345
|
`condition in the profile, not an instruction to type. A project's agent on another machine: ` +
|
|
1249
1346
|
`\`vdx ai@<machine>\`. More: \`vdx ai --help\`.`,
|
|
1250
1347
|
];
|
|
1348
|
+
if (focused) {
|
|
1349
|
+
lines.push(
|
|
1350
|
+
`· this session is focused (\`vdx ai --focused\`): it works on what the user gave it — nothing should wake it, ` +
|
|
1351
|
+
`and it does not read its inbox unless the user asks` +
|
|
1352
|
+
(plan.skipped.length ? `; the profile's ${plan.skipped.join(', ')} is left out` : '') +
|
|
1353
|
+
'.',
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1251
1356
|
const own = findOwnAgent(deps.processes(), selfPid, plan.command);
|
|
1252
1357
|
if (!own) {
|
|
1253
1358
|
lines.push(`· no ${plan.command} among the ancestors of this process — no session to compare with the profile.`);
|
|
@@ -1265,10 +1370,10 @@ export function runAiCheck(pathArg: string, deps: AiDeps, selfPid: number = proc
|
|
|
1265
1370
|
lines.push(
|
|
1266
1371
|
`✗ this session runs without ${missing.join(' ')}${why}. Hand the user the fix — ` +
|
|
1267
1372
|
(deps.env['TMUX']
|
|
1268
|
-
?
|
|
1373
|
+
? `\`${vdxAi} --restart ${root}\`: it stops this session and starts it again with the profile's flags` +
|
|
1269
1374
|
(plan.resumeArgs.length ? `, resuming the conversation (${plan.resumeArgs.join(' ')})` : '') +
|
|
1270
1375
|
`. Do not run it yourself: it ends this session.`
|
|
1271
|
-
: `this session runs outside tmux, so vdx cannot restart it: the user exits it and runs
|
|
1376
|
+
: `this session runs outside tmux, so vdx cannot restart it: the user exits it and runs \`${vdxAi} ${root}\`.`),
|
|
1272
1377
|
);
|
|
1273
1378
|
deps.out(lines.join('\n') + '\n');
|
|
1274
1379
|
return EXIT_DRIFT;
|
|
@@ -1277,10 +1382,18 @@ export function runAiCheck(pathArg: string, deps: AiDeps, selfPid: number = proc
|
|
|
1277
1382
|
export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
1278
1383
|
const { log, tmux } = deps;
|
|
1279
1384
|
|
|
1280
|
-
const prepared = prepare(opts.path, deps);
|
|
1385
|
+
const prepared = prepare(opts.path, deps, opts.focused === true);
|
|
1281
1386
|
if (typeof prepared === 'number') return prepared;
|
|
1282
1387
|
const { projectRoot, environment, profileDir } = prepared;
|
|
1283
1388
|
let { plan } = prepared;
|
|
1389
|
+
// The agent started here runs on this machine: it does not inherit where the person sat,
|
|
1390
|
+
// and it is focused by this start's flag, not by the shell vdx runs in.
|
|
1391
|
+
delete deps.env[SEAT_ENV_VAR];
|
|
1392
|
+
delete deps.env[FOCUSED_ENV_VAR];
|
|
1393
|
+
// Under `vdx ai@<host>` the person is not here: this machine goes by its name.
|
|
1394
|
+
const atSeat = sameMachine(plan.seat, plan.host);
|
|
1395
|
+
const here = atSeat ? `here, on ${plan.host}` : `on ${plan.host}`;
|
|
1396
|
+
const vdxAiHere = atSeat ? 'vdx ai' : `vdx ai@${plan.host}`;
|
|
1284
1397
|
if (opts.conversation && !isClaude(plan.command)) {
|
|
1285
1398
|
log(`vdx ai: --conversation names a Claude Code conversation; the agent here is ${plan.command}`);
|
|
1286
1399
|
return EXIT_USAGE;
|
|
@@ -1299,6 +1412,13 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
|
1299
1412
|
? findAgentPanes(tmux.panes(), deps.processes(), projectRoot, plan.command)
|
|
1300
1413
|
: [];
|
|
1301
1414
|
const current = running[0];
|
|
1415
|
+
// A running agent keeps its mode: plain `vdx ai` takes a focused one as it is,
|
|
1416
|
+
// and the rules it leaves out are not missing from it.
|
|
1417
|
+
const focusedOf = deps.agentFocused ?? processFocused;
|
|
1418
|
+
const runningFocused = current ? focusedOf(current.proc.pid) : false;
|
|
1419
|
+
if (current && !opts.restart && runningFocused && !plan.focused) {
|
|
1420
|
+
plan = { ...prepared.replan(true), resumeArgs: plan.resumeArgs };
|
|
1421
|
+
}
|
|
1302
1422
|
|
|
1303
1423
|
// What a start continues. A restart keeps the conversation the agent is in.
|
|
1304
1424
|
let choice: ConversationChoice | null = null;
|
|
@@ -1317,7 +1437,7 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
|
1317
1437
|
|
|
1318
1438
|
const poolDirs = authorPoolDirs(environment, projectRoot, deps.home, profileDir);
|
|
1319
1439
|
if (opts.dryRun) {
|
|
1320
|
-
deps.out(renderPlan(plan, multiplexer, running));
|
|
1440
|
+
deps.out(renderPlan(plan, multiplexer, running, focusedOf));
|
|
1321
1441
|
for (const l of choice?.lines ?? []) deps.out(` conversation: ${l}\n`);
|
|
1322
1442
|
ensureAuthor(projectRoot, poolDirs, deps, true);
|
|
1323
1443
|
return EXIT_OK;
|
|
@@ -1325,7 +1445,7 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
|
1325
1445
|
for (const l of choice?.lines ?? []) log(l);
|
|
1326
1446
|
if (choice?.abort) return EXIT_LAUNCH_FAILED;
|
|
1327
1447
|
if (choice?.elsewhere && !current) {
|
|
1328
|
-
const answer = deps.interactive ? deps.ask(`Start a new conversation here,
|
|
1448
|
+
const answer = deps.interactive ? deps.ask(`Start a new conversation ${here}${atSeat ? ',' : ''} anyway? [y/N]: `) : null;
|
|
1329
1449
|
if (!/^y(es)?$/i.test(answer?.trim() ?? '')) {
|
|
1330
1450
|
log('nothing started');
|
|
1331
1451
|
return EXIT_LAUNCH_FAILED;
|
|
@@ -1335,17 +1455,17 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
|
1335
1455
|
let askHere = choice?.askHere;
|
|
1336
1456
|
if (choice?.remote) {
|
|
1337
1457
|
const { host, id, reachable } = choice.remote;
|
|
1338
|
-
if (reachable) return runAiRemote({ ...opts, path: projectRoot, conversation: id }, host, deps);
|
|
1458
|
+
if (reachable) return runAiRemote({ ...opts, path: projectRoot, conversation: id }, host, deps, plan.seat);
|
|
1339
1459
|
log(`✗ ${host} does not answer over ssh — its conversation ${id.slice(0, 8)} cannot be continued there`);
|
|
1340
1460
|
askHere = { id, why: `If it still runs on ${host}, both machines will write to it` };
|
|
1341
1461
|
}
|
|
1342
1462
|
if (askHere) {
|
|
1343
1463
|
const { id, why } = askHere;
|
|
1344
|
-
const answer = deps.interactive ? deps.ask(`Continue ${id.slice(0, 8)}
|
|
1464
|
+
const answer = deps.interactive ? deps.ask(`Continue ${id.slice(0, 8)} ${here}? ${why}. [y/N]: `) : null;
|
|
1345
1465
|
if (!/^y(es)?$/i.test(answer?.trim() ?? '')) {
|
|
1346
1466
|
log(
|
|
1347
|
-
`nothing started. Here:
|
|
1348
|
-
` · a new conversation:
|
|
1467
|
+
`nothing started. ${atSeat ? 'Here' : `On ${plan.host}`}: ${vdxAiHere} ${shellQuote(projectRoot)} --conversation ${id}` +
|
|
1468
|
+
` · a new conversation: ${vdxAiHere} --new ${shellQuote(projectRoot)}`,
|
|
1349
1469
|
);
|
|
1350
1470
|
return EXIT_LAUNCH_FAILED;
|
|
1351
1471
|
}
|
|
@@ -1363,16 +1483,17 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
|
1363
1483
|
return EXIT_USAGE;
|
|
1364
1484
|
}
|
|
1365
1485
|
for (const c of plan.confirm) log(`note: the agent will ask "${c.screen}" — answer it with ${c.keys.join(' ')}`);
|
|
1486
|
+
const env = plan.focused ? { ...process.env, [FOCUSED_ENV_VAR]: '1' } : undefined;
|
|
1366
1487
|
const startedAt = Date.now();
|
|
1367
|
-
let res = spawnSync(plan.command, [...plan.args, ...plan.resumeArgs], { cwd: projectRoot, stdio: 'inherit' });
|
|
1488
|
+
let res = spawnSync(plan.command, [...plan.args, ...plan.resumeArgs], { cwd: projectRoot, stdio: 'inherit', env });
|
|
1368
1489
|
if ((res.error as NodeJS.ErrnoException | undefined)?.code === 'ENOENT') {
|
|
1369
1490
|
log(`vdx ai: \`${plan.command}\` not found on PATH`);
|
|
1370
1491
|
return 127;
|
|
1371
1492
|
}
|
|
1372
1493
|
const failedAtOnce = res.status !== null && res.status !== 0 && Date.now() - startedAt < NOTHING_TO_RESUME_MS;
|
|
1373
1494
|
if (plan.resumeArgs.length > 0 && failedAtOnce) {
|
|
1374
|
-
log(`↻ nothing to resume — starting without ${plan.resumeArgs.join(' ')}: ${
|
|
1375
|
-
res = spawnSync(plan.command, plan.args, { cwd: projectRoot, stdio: 'inherit' });
|
|
1495
|
+
log(`↻ nothing to resume — starting without ${plan.resumeArgs.join(' ')}: ${agentLine(plan, plan.args)}`);
|
|
1496
|
+
res = spawnSync(plan.command, plan.args, { cwd: projectRoot, stdio: 'inherit', env });
|
|
1376
1497
|
}
|
|
1377
1498
|
return res.status ?? 1;
|
|
1378
1499
|
}
|
|
@@ -1395,11 +1516,19 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
|
1395
1516
|
);
|
|
1396
1517
|
}
|
|
1397
1518
|
}
|
|
1519
|
+
if (plan.focused && !runningFocused) {
|
|
1520
|
+
log(`✗ ${current.pane.session}: ${plan.command} is running, not focused`);
|
|
1521
|
+
log(
|
|
1522
|
+
` to focus it: vdx ai --restart --focused ${shellQuote(projectRoot)}` +
|
|
1523
|
+
(plan.resumeArgs.length ? ' (stops the running agent, then resumes its conversation)' : ' (stops the running agent)'),
|
|
1524
|
+
);
|
|
1525
|
+
return EXIT_DRIFT;
|
|
1526
|
+
}
|
|
1398
1527
|
const missing = missingArgs(current.proc.args, plan.args);
|
|
1399
1528
|
if (missing.length > 0) {
|
|
1400
1529
|
log(`✗ ${current.pane.session}: ${plan.command} is running without ${missing.join(' ')}`);
|
|
1401
1530
|
log(
|
|
1402
|
-
` to apply the profile: vdx ai --restart${opts.fresh ? ' --new' : ''} ${shellQuote(projectRoot)}` +
|
|
1531
|
+
` to apply the profile: vdx ai --restart${plan.focused ? ' --focused' : ''}${opts.fresh ? ' --new' : ''} ${shellQuote(projectRoot)}` +
|
|
1403
1532
|
(plan.resumeArgs.length
|
|
1404
1533
|
? ` (stops the running agent, then resumes its conversation)`
|
|
1405
1534
|
: opts.fresh
|
|
@@ -1408,19 +1537,19 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
|
1408
1537
|
);
|
|
1409
1538
|
return EXIT_DRIFT;
|
|
1410
1539
|
}
|
|
1411
|
-
log(`✓ ${current.pane.session}: already running with the profile — ${current.proc.args}`);
|
|
1540
|
+
log(`✓ ${current.pane.session}: already running with the profile${runningFocused ? ', focused' : ''} — ${current.proc.args}`);
|
|
1412
1541
|
return finish(true, current.pane.session, current.pane.paneId, opts, deps);
|
|
1413
1542
|
}
|
|
1414
1543
|
|
|
1415
1544
|
if (current) {
|
|
1416
|
-
const line =
|
|
1545
|
+
const line = agentLine(plan, [...plan.args, ...plan.resumeArgs]);
|
|
1417
1546
|
log(`↻ ${current.pane.session} ${current.pane.paneId}: restarting — ${line}`);
|
|
1418
1547
|
tmux.respawn(current.pane.paneId, projectRoot, tmuxShellCommand(line, deps.shell));
|
|
1419
1548
|
return finish(settleResumed(plan, current.pane.paneId, deps), current.pane.session, current.pane.paneId, opts, deps);
|
|
1420
1549
|
}
|
|
1421
1550
|
|
|
1422
1551
|
if (opts.restart) log(`note: no running ${plan.command} in this project — starting one`);
|
|
1423
|
-
const line =
|
|
1552
|
+
const line = agentLine(plan, [...plan.args, ...plan.resumeArgs]);
|
|
1424
1553
|
const cmd = tmuxShellCommand(line, deps.shell);
|
|
1425
1554
|
const reuse = tmux.hasSession(plan.sessionName);
|
|
1426
1555
|
const paneId = reuse
|
package/src/conversations.ts
CHANGED
|
@@ -314,10 +314,13 @@ export function parseMachineReport(text: string): MachineReport {
|
|
|
314
314
|
return { home: text.match(/^home=(.+)$/m)?.[1] ?? null, sessions: parseLiveSessions(text) };
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
/**
|
|
318
|
-
|
|
317
|
+
/**
|
|
318
|
+
* "running on m3 now (t23b-program@m3, waiting)" — where a live session is, for
|
|
319
|
+
* a person choosing; `place` is the machine as they see it: "here", "on m3".
|
|
320
|
+
*/
|
|
321
|
+
export function describeLive(place: string, live: LiveSession): string {
|
|
319
322
|
const where = live.tmux ? live.tmux.replace(/:.*$/, '') : `pid ${live.pid}`;
|
|
320
|
-
return `running ${
|
|
323
|
+
return `running ${place} now (${where}${live.status ? `, ${live.status}` : ''})`;
|
|
321
324
|
}
|
|
322
325
|
|
|
323
326
|
/** "3 min ago", "yesterday 14:05", "2026-09-30 14:05" — for a person choosing. */
|
package/src/index.ts
CHANGED
|
@@ -76,6 +76,11 @@ the project's files). Hand a person \`vdx ai\`, not \`<agent> --<flag>\`.
|
|
|
76
76
|
project_path the project (default: cwd); its git toplevel is used
|
|
77
77
|
@host the same command on <host> over ssh: its vdx, profile and tmux
|
|
78
78
|
--new a new conversation instead of continuing the last one
|
|
79
|
+
--focused a session nothing wakes: the agent, its hooks and MCP servers get
|
|
80
|
+
VDX_FOCUSED=1 — for the plugins that wake sessions to leave it
|
|
81
|
+
alone — and the profile's rules marked \`wakes: true\` (a channel
|
|
82
|
+
flag) are left out. A running agent keeps its mode; --restart
|
|
83
|
+
applies this one
|
|
79
84
|
--conversation <id>
|
|
80
85
|
continue this Claude Code conversation
|
|
81
86
|
--restart restart the running agent in its pane — applies a changed profile
|
|
@@ -97,7 +102,8 @@ machine's. A new conversation (--new, or none to continue) while an agent of
|
|
|
97
102
|
the project runs on another machine starts only after a yes.
|
|
98
103
|
|
|
99
104
|
Exit codes: 0 the agent runs per the profile; 2 profile or usage error;
|
|
100
|
-
3 the running agent lacks profile flags
|
|
105
|
+
3 the running agent lacks profile flags, or is not focused under --focused
|
|
106
|
+
(rerun with --restart); 4 the start
|
|
101
107
|
was not confirmed.
|
|
102
108
|
Profile format: https://github.com/VoDmAl/vdx/blob/main/docs/specs/environment-format.md
|
|
103
109
|
`;
|
|
@@ -156,10 +162,10 @@ Exit 2 when something is missing.
|
|
|
156
162
|
positionals: 1,
|
|
157
163
|
},
|
|
158
164
|
ai: {
|
|
159
|
-
usage: 'vdx ai[@host] [project_path] [--new | --conversation <id>] [--restart] [--detach] [--dry-run]',
|
|
165
|
+
usage: 'vdx ai[@host] [project_path] [--new | --conversation <id>] [--focused] [--restart] [--detach] [--dry-run]',
|
|
160
166
|
help: AI_HELP,
|
|
161
167
|
// `--resume` asked to continue in 0.12; continuing is the default now.
|
|
162
|
-
bools: ['new', 'restart', 'resume', 'detach', 'dry-run', 'check'],
|
|
168
|
+
bools: ['new', 'focused', 'restart', 'resume', 'detach', 'dry-run', 'check'],
|
|
163
169
|
values: ['conversation'],
|
|
164
170
|
positionals: 1,
|
|
165
171
|
},
|
|
@@ -500,6 +506,7 @@ function cmdAi(opts: ParsedArgs): void {
|
|
|
500
506
|
conversation,
|
|
501
507
|
detach: opts.flags.detach === true,
|
|
502
508
|
dryRun: opts.flags['dry-run'] === true,
|
|
509
|
+
focused: opts.flags.focused === true,
|
|
503
510
|
};
|
|
504
511
|
const deps = { ...defaultDeps(), version: readCliVersion() };
|
|
505
512
|
// `ai@m3`: the same command on another machine. Its own label runs here.
|
package/src/init.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
export const STANDARD_VERBS = ['up', 'down', 'build', 'test', 'check', 'fix'] as const;
|
|
13
13
|
export type Verb = (typeof STANDARD_VERBS)[number];
|
|
14
14
|
|
|
15
|
-
export const DEFAULT_BASELINE = 'github.com/VoDmAl/vdx-rubric-vodmal@v1.
|
|
15
|
+
export const DEFAULT_BASELINE = 'github.com/VoDmAl/vdx-rubric-vodmal@v1.2.0';
|
|
16
16
|
|
|
17
17
|
const VERB_ALIASES: Record<Verb, string[]> = {
|
|
18
18
|
up: ['docker:up', 'docker-up', 'docker:up:detached', 'start', 'dev', 'serve'],
|