@vodmal/vdx-cli 0.20.0 → 0.21.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/package.json +1 -1
- package/rubric/vdx-rubric.yaml +1 -1
- package/src/ai.ts +64 -21
- package/src/conversations.ts +42 -0
- package/src/index.ts +7 -5
- package/src/init.ts +1 -1
- package/src/publish.ts +30 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vodmal/vdx-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type Conversation,
|
|
13
13
|
describeConversation,
|
|
14
14
|
describeLive,
|
|
15
|
+
isMachineLabel,
|
|
15
16
|
LIVE_SESSIONS_SCRIPT,
|
|
16
17
|
type LiveSession,
|
|
17
18
|
latestPerMachine,
|
|
@@ -19,8 +20,10 @@ import {
|
|
|
19
20
|
liveConversationId,
|
|
20
21
|
liveSessionsHere,
|
|
21
22
|
machineLine,
|
|
23
|
+
type MachineReport,
|
|
22
24
|
machineOf,
|
|
23
|
-
|
|
25
|
+
parseMachineReport,
|
|
26
|
+
runsInProject,
|
|
24
27
|
} from './conversations.ts';
|
|
25
28
|
|
|
26
29
|
/**
|
|
@@ -76,6 +79,8 @@ export interface SessionProfile {
|
|
|
76
79
|
name?: string;
|
|
77
80
|
/** Tried after `[vdx] name` in the project's mise.toml; the first non-empty string wins. */
|
|
78
81
|
project_names?: NameSource[];
|
|
82
|
+
/** The machines a project's agent may run on, as ssh destinations; vdx asks the others what runs there. */
|
|
83
|
+
machines?: string[];
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
export interface GitProfile {
|
|
@@ -183,6 +188,12 @@ export function parseEnvironment(text: string, source: string): Environment {
|
|
|
183
188
|
}
|
|
184
189
|
});
|
|
185
190
|
}
|
|
191
|
+
if (session.machines !== undefined) {
|
|
192
|
+
if (!isStringList(session.machines)) fail('session.machines', 'must be a list of machine names');
|
|
193
|
+
session.machines.forEach((m, i) => {
|
|
194
|
+
if (!isMachineLabel(m)) fail(`session.machines[${i}]`, 'must be an ssh destination: letters, digits, . _ - and no leading -');
|
|
195
|
+
});
|
|
196
|
+
}
|
|
186
197
|
}
|
|
187
198
|
|
|
188
199
|
const gitDoc = envDoc.git;
|
|
@@ -238,6 +249,8 @@ export interface LaunchPlan {
|
|
|
238
249
|
confirm: ConfirmRule[];
|
|
239
250
|
multiplexer: Multiplexer;
|
|
240
251
|
sessionName: string;
|
|
252
|
+
/** The profile's other machines — asked over ssh what runs there before a start. */
|
|
253
|
+
machines: string[];
|
|
241
254
|
}
|
|
242
255
|
|
|
243
256
|
export function planLaunch(input: {
|
|
@@ -276,6 +289,7 @@ export function planLaunch(input: {
|
|
|
276
289
|
project: input.project,
|
|
277
290
|
host: input.host,
|
|
278
291
|
}),
|
|
292
|
+
machines: (environment.session?.machines ?? []).filter((m) => m.toLowerCase() !== input.host.toLowerCase()),
|
|
279
293
|
};
|
|
280
294
|
}
|
|
281
295
|
|
|
@@ -942,12 +956,12 @@ export function runAiRemote(opts: AiOptions, host: string, deps: AiDeps, version
|
|
|
942
956
|
* The Claude Code sessions alive on `host` right now; null when it does not
|
|
943
957
|
* answer over ssh (BatchMode: a password prompt counts as no answer).
|
|
944
958
|
*/
|
|
945
|
-
export function askMachine(host: string, deps: AiDeps):
|
|
959
|
+
export function askMachine(host: string, deps: AiDeps): MachineReport | null {
|
|
946
960
|
const res = (deps.ssh ?? runSsh)(
|
|
947
961
|
['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=5', host, `sh -c ${shellQuote(LIVE_SESSIONS_SCRIPT)}`],
|
|
948
962
|
true,
|
|
949
963
|
);
|
|
950
|
-
return res && res.status === 0 ?
|
|
964
|
+
return res && res.status === 0 ? parseMachineReport(res.stdout) : null;
|
|
951
965
|
}
|
|
952
966
|
|
|
953
967
|
/** What a start continues: resume args here, or a conversation on another machine. */
|
|
@@ -957,6 +971,10 @@ export interface ConversationChoice {
|
|
|
957
971
|
remote?: { host: string; id: string; reachable: boolean };
|
|
958
972
|
/** Continue it here only after a yes: another machine's that cannot be asked, or one running here already. */
|
|
959
973
|
askHere?: { id: string; why: string };
|
|
974
|
+
/** The person ended the question (Ctrl-D): start nothing. */
|
|
975
|
+
abort?: boolean;
|
|
976
|
+
/** A new conversation nobody picked, while an agent of the project runs on these machines: only after a yes. */
|
|
977
|
+
elsewhere?: string[];
|
|
960
978
|
/** For the person: what was found and what is taken. */
|
|
961
979
|
lines: string[];
|
|
962
980
|
}
|
|
@@ -974,10 +992,15 @@ const resumeId = (id: string) => ['--resume', id];
|
|
|
974
992
|
* newest one in the folder, and with the folder synced between machines that
|
|
975
993
|
* can be a conversation another machine is still writing to.
|
|
976
994
|
*
|
|
977
|
-
*
|
|
978
|
-
*
|
|
979
|
-
* its own session files. A running
|
|
980
|
-
* its machine's newest.
|
|
995
|
+
* The profile's other machines, and every other machine this project's
|
|
996
|
+
* conversations name, are asked over ssh what runs there right now; this
|
|
997
|
+
* machine's running ones come from its own session files. A running
|
|
998
|
+
* conversation is listed even when it is not its machine's newest.
|
|
999
|
+
*
|
|
1000
|
+
* A new conversation the person did not pick from the list — `--new`, a
|
|
1001
|
+
* project with none yet, none of this machine's idle — while an agent of this
|
|
1002
|
+
* project runs on another machine (by the directory it runs in) is started
|
|
1003
|
+
* only after a yes: two agents would work in one repository from two machines.
|
|
981
1004
|
*
|
|
982
1005
|
* Another machine's conversation is continued on that machine, so two
|
|
983
1006
|
* machines never write to one conversation; one whose machine is unknown, or
|
|
@@ -987,24 +1010,34 @@ const resumeId = (id: string) => ['--resume', id];
|
|
|
987
1010
|
* idle one, never another's.
|
|
988
1011
|
*/
|
|
989
1012
|
export function chooseConversation(plan: LaunchPlan, deps: AiDeps, canAsk: boolean): ConversationChoice {
|
|
990
|
-
if (
|
|
1013
|
+
if (!isClaude(plan.command)) return { args: plan.resumeArgs, lines: [] };
|
|
991
1014
|
const host = plan.host;
|
|
992
1015
|
const same = (a: string | null, b: string | null) => (a ?? '').toLowerCase() === (b ?? '').toLowerCase();
|
|
993
|
-
const all = listConversations({ env: deps.env, home: deps.home, projectRoot: plan.projectRoot });
|
|
994
|
-
|
|
1016
|
+
const all = plan.resumeArgs.length === 0 ? [] : listConversations({ env: deps.env, home: deps.home, projectRoot: plan.projectRoot });
|
|
1017
|
+
const root = shellQuote(plan.projectRoot);
|
|
995
1018
|
|
|
996
1019
|
// Who runs what right now: this machine from its session files, the others over ssh.
|
|
997
|
-
const reach = new Map<string,
|
|
998
|
-
for (const
|
|
999
|
-
if (
|
|
1000
|
-
reach.set(c.machine, askMachine(c.machine, deps));
|
|
1001
|
-
}
|
|
1020
|
+
const reach = new Map<string, MachineReport | null>();
|
|
1021
|
+
for (const m of [...plan.machines, ...all.map((c) => c.machine)]) {
|
|
1022
|
+
if (m && !same(m, host) && ![...reach.keys()].some((k) => same(k, m))) reach.set(m, askMachine(m, deps));
|
|
1002
1023
|
}
|
|
1024
|
+
const silent = [...reach].filter(([, r]) => r === null).map(([m]) => `note: ${m} does not answer over ssh — what runs there is not known`);
|
|
1025
|
+
const elsewhere = [...reach].flatMap(([m, r]) =>
|
|
1026
|
+
(r?.sessions ?? []).filter((l) => runsInProject(l, r!.home, plan.projectRoot, deps.home)).map((l) => ({ m, l })),
|
|
1027
|
+
);
|
|
1028
|
+
const fresh = (lines: string[]): ConversationChoice => {
|
|
1029
|
+
if (elsewhere.length === 0) return { args: [], lines };
|
|
1030
|
+
const agents = elsewhere.map(({ m, l }) => `an agent of this project is ${describeLive(m, l, false)} — there: vdx ai@${m} ${root}`);
|
|
1031
|
+
return { args: [], elsewhere: [...new Set(elsewhere.map(({ m }) => m))], lines: [...lines, ...agents] };
|
|
1032
|
+
};
|
|
1033
|
+
if (plan.resumeArgs.length === 0) return fresh(silent);
|
|
1034
|
+
if (all.length === 0) return fresh(['no conversation of this project yet — starting a new one', ...silent]);
|
|
1035
|
+
|
|
1003
1036
|
const live = new Map<string, { machine: string; session: LiveSession; here: boolean }[]>();
|
|
1004
1037
|
const addLive = (machine: string, session: LiveSession, here: boolean) =>
|
|
1005
1038
|
live.set(session.conversation, [...(live.get(session.conversation) ?? []), { machine, session, here }]);
|
|
1006
1039
|
for (const l of (deps.liveHere ?? (() => liveSessionsHere(deps.env, deps.home)))()) addLive(host, l, true);
|
|
1007
|
-
for (const [m,
|
|
1040
|
+
for (const [m, r] of reach) for (const l of r?.sessions ?? []) addLive(m, l, false);
|
|
1008
1041
|
|
|
1009
1042
|
const machine = (c: Conversation) => live.get(c.id)?.find((l) => !l.here)?.machine ?? machineOf(c, host);
|
|
1010
1043
|
const runsHere = (c: Conversation) => live.get(c.id)?.find((l) => l.here);
|
|
@@ -1047,7 +1080,8 @@ export function chooseConversation(plan: LaunchPlan, deps: AiDeps, canAsk: boole
|
|
|
1047
1080
|
deps.log(' n) a new conversation');
|
|
1048
1081
|
for (let tries = 0; tries < 2; tries++) {
|
|
1049
1082
|
const answer = deps.ask(`Which one to continue? [1]: `);
|
|
1050
|
-
|
|
1083
|
+
// Ctrl-D at the question is a cancel, not "no terminal": start nothing.
|
|
1084
|
+
if (answer === null) return { args: [], abort: true, lines: ['no answer — nothing started'] };
|
|
1051
1085
|
const a = answer.trim();
|
|
1052
1086
|
if (/^n(ew)?$/i.test(a)) return { args: [], lines: ['a new conversation'] };
|
|
1053
1087
|
const c = a === '' ? found[0] : /^\d+$/.test(a) ? found[Number(a) - 1] : undefined;
|
|
@@ -1055,7 +1089,6 @@ export function chooseConversation(plan: LaunchPlan, deps: AiDeps, canAsk: boole
|
|
|
1055
1089
|
deps.log(` 1–${found.length}, or n`);
|
|
1056
1090
|
}
|
|
1057
1091
|
}
|
|
1058
|
-
const root = shellQuote(plan.projectRoot);
|
|
1059
1092
|
const notes = found
|
|
1060
1093
|
.filter((c) => !idleHere(c))
|
|
1061
1094
|
.map((c) => {
|
|
@@ -1069,12 +1102,12 @@ export function chooseConversation(plan: LaunchPlan, deps: AiDeps, canAsk: boole
|
|
|
1069
1102
|
? `note: ${m} has ${which} — continue it there: vdx ai@${m} ${root} --conversation ${c.id}`
|
|
1070
1103
|
: `note: ${which} is of an unknown machine (never typed in here) — on its machine: vdx ai@<machine> ${root} --conversation ${c.id}`;
|
|
1071
1104
|
});
|
|
1072
|
-
|
|
1105
|
+
notes.push(...silent);
|
|
1073
1106
|
if (firstIdle) {
|
|
1074
1107
|
const chosen = take(firstIdle);
|
|
1075
1108
|
return { ...chosen, lines: [...chosen.lines, ...notes] };
|
|
1076
1109
|
}
|
|
1077
|
-
return
|
|
1110
|
+
return fresh(['no idle conversation of this machine — starting a new one', ...notes]);
|
|
1078
1111
|
}
|
|
1079
1112
|
|
|
1080
1113
|
interface Prepared {
|
|
@@ -1227,7 +1260,9 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
|
1227
1260
|
let choice: ConversationChoice | null = null;
|
|
1228
1261
|
if (!current || opts.restart) {
|
|
1229
1262
|
if (opts.conversation) choice = { args: resumeId(opts.conversation), lines: [] };
|
|
1230
|
-
|
|
1263
|
+
// A restart into a new conversation replaces the agent here: no second agent, nothing to ask.
|
|
1264
|
+
else if (current && plan.resumeArgs.length === 0) choice = { args: [], lines: [] };
|
|
1265
|
+
else if (current && isClaude(plan.command)) {
|
|
1231
1266
|
const live = liveConversationId(deps.env, deps.home, current.proc.pid);
|
|
1232
1267
|
choice = live
|
|
1233
1268
|
? { args: resumeId(live), lines: [`the agent is in conversation ${live.slice(0, 8)} — resuming it`] }
|
|
@@ -1244,6 +1279,14 @@ export function runAi(opts: AiOptions, deps: AiDeps): number {
|
|
|
1244
1279
|
return EXIT_OK;
|
|
1245
1280
|
}
|
|
1246
1281
|
for (const l of choice?.lines ?? []) log(l);
|
|
1282
|
+
if (choice?.abort) return EXIT_LAUNCH_FAILED;
|
|
1283
|
+
if (choice?.elsewhere && !current) {
|
|
1284
|
+
const answer = deps.interactive ? deps.ask(`Start a new conversation here, on ${plan.host}, anyway? [y/N]: `) : null;
|
|
1285
|
+
if (!/^y(es)?$/i.test(answer?.trim() ?? '')) {
|
|
1286
|
+
log('nothing started');
|
|
1287
|
+
return EXIT_LAUNCH_FAILED;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1247
1290
|
|
|
1248
1291
|
let askHere = choice?.askHere;
|
|
1249
1292
|
if (choice?.remote) {
|
package/src/conversations.ts
CHANGED
|
@@ -38,6 +38,11 @@ export function machineLine(label: string): string {
|
|
|
38
38
|
return `${MACHINE_LINE_PREFIX} \`${label}\``;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/** A machine label vdx may hand to ssh as the destination. */
|
|
42
|
+
export function isMachineLabel(s: string): boolean {
|
|
43
|
+
return LABEL_RE.test(s);
|
|
44
|
+
}
|
|
45
|
+
|
|
41
46
|
export function isConversationId(s: string): boolean {
|
|
42
47
|
return ID_RE.test(s);
|
|
43
48
|
}
|
|
@@ -206,6 +211,8 @@ export interface LiveSession {
|
|
|
206
211
|
tmux: string | null;
|
|
207
212
|
/** Claude Code's own word for it: `busy`, `waiting`, … */
|
|
208
213
|
status: string | null;
|
|
214
|
+
/** The directory it was started in — on its own machine. */
|
|
215
|
+
cwd: string | null;
|
|
209
216
|
}
|
|
210
217
|
|
|
211
218
|
/** Session files, one JSON object per line — a file read here, or the output of LIVE_SESSIONS_SCRIPT. */
|
|
@@ -225,11 +232,32 @@ export function parseLiveSessions(text: string): LiveSession[] {
|
|
|
225
232
|
conversation: data.sessionId,
|
|
226
233
|
tmux: typeof data.tmux === 'string' ? data.tmux : null,
|
|
227
234
|
status: typeof data.status === 'string' ? data.status : null,
|
|
235
|
+
cwd: typeof data.cwd === 'string' ? data.cwd : null,
|
|
228
236
|
});
|
|
229
237
|
}
|
|
230
238
|
return out;
|
|
231
239
|
}
|
|
232
240
|
|
|
241
|
+
/** `~/AI Projects/x` for a path under `home`; any other path as is. */
|
|
242
|
+
function fromHome(p: string, home: string): string {
|
|
243
|
+
const rel = path.relative(home, p);
|
|
244
|
+
if (rel === '') return '~';
|
|
245
|
+
return rel.startsWith('..') || path.isAbsolute(rel) ? p : `~/${rel}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The session runs in the project: its directory is the project's root or
|
|
250
|
+
* below it. Paths of two machines are compared from their homes — `vdx
|
|
251
|
+
* ai@<host>` puts a project under the same place in the other home; with that
|
|
252
|
+
* home unknown, as they are.
|
|
253
|
+
*/
|
|
254
|
+
export function runsInProject(live: LiveSession, liveHome: string | null, projectRoot: string, home: string): boolean {
|
|
255
|
+
if (!live.cwd) return false;
|
|
256
|
+
const cwd = liveHome ? fromHome(live.cwd, liveHome) : live.cwd;
|
|
257
|
+
const root = liveHome ? fromHome(projectRoot, home) : projectRoot;
|
|
258
|
+
return cwd === root || cwd.startsWith(root.endsWith('/') ? root : `${root}/`);
|
|
259
|
+
}
|
|
260
|
+
|
|
233
261
|
function pidAlive(pid: number): boolean {
|
|
234
262
|
try {
|
|
235
263
|
process.kill(pid, 0);
|
|
@@ -244,8 +272,11 @@ function pidAlive(pid: number): boolean {
|
|
|
244
272
|
* runs on, one per line — what `vdx ai` asks another machine over ssh. Claude
|
|
245
273
|
* removes the file of a process that ended, but not after a crash or a reboot:
|
|
246
274
|
* the pid is checked. `<pid>.sync-conflict-….json` is not a session file.
|
|
275
|
+
* The first line, `home=<dir>`, places the sessions' directories in that
|
|
276
|
+
* machine's home (see runsInProject).
|
|
247
277
|
*/
|
|
248
278
|
export const LIVE_SESSIONS_SCRIPT =
|
|
279
|
+
'printf "home=%s\\n" "$HOME"; ' +
|
|
249
280
|
'd="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/sessions"; for f in "$d"/*.json; do ' +
|
|
250
281
|
'p=${f##*/}; p=${p%.json}; case $p in ""|*[!0-9]*) continue;; esac; ' +
|
|
251
282
|
'kill -0 "$p" 2>/dev/null && tr -d "\\n" < "$f" && echo; done; exit 0';
|
|
@@ -272,6 +303,17 @@ export function liveSessionsHere(env: NodeJS.ProcessEnv, home: string, alive: (p
|
|
|
272
303
|
return parseLiveSessions(lines.join('\n'));
|
|
273
304
|
}
|
|
274
305
|
|
|
306
|
+
/** What another machine runs right now: its home and the Claude Code sessions alive there. */
|
|
307
|
+
export interface MachineReport {
|
|
308
|
+
home: string | null;
|
|
309
|
+
sessions: LiveSession[];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** The output of LIVE_SESSIONS_SCRIPT. */
|
|
313
|
+
export function parseMachineReport(text: string): MachineReport {
|
|
314
|
+
return { home: text.match(/^home=(.+)$/m)?.[1] ?? null, sessions: parseLiveSessions(text) };
|
|
315
|
+
}
|
|
316
|
+
|
|
275
317
|
/** "running on m3 now (t23b-program@m3, waiting)" — where a live session is, for a person choosing. */
|
|
276
318
|
export function describeLive(machine: string, live: LiveSession, here: boolean): string {
|
|
277
319
|
const where = live.tmux ? live.tmux.replace(/:.*$/, '') : `pid ${live.pid}`;
|
package/src/index.ts
CHANGED
|
@@ -88,11 +88,13 @@ the project's files). Hand a person \`vdx ai\`, not \`<agent> --<flag>\`.
|
|
|
88
88
|
|
|
89
89
|
Which conversation a start continues (Claude Code): each one is marked with the
|
|
90
90
|
machine it was last started on (the vdx plugin's hook writes it in), and the
|
|
91
|
-
other machines
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
other machines (session.machines in the profile, and those the conversations
|
|
92
|
+
name) are asked over ssh what runs there now. Another machine's conversation
|
|
93
|
+
is continued on that machine; if it does not answer, vdx asks before
|
|
94
|
+
continuing it here. When the choice is not plain, vdx lists them, the running
|
|
95
|
+
ones marked — Enter takes the newest; without a terminal it takes this
|
|
96
|
+
machine's. A new conversation (--new, or none to continue) while an agent of
|
|
97
|
+
the project runs on another machine starts only after a yes.
|
|
96
98
|
|
|
97
99
|
Exit codes: 0 the agent runs per the profile; 2 profile or usage error;
|
|
98
100
|
3 the running agent lacks profile flags (rerun with --restart); 4 the start
|
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@v0.
|
|
15
|
+
export const DEFAULT_BASELINE = 'github.com/VoDmAl/vdx-rubric-vodmal@v0.7.0';
|
|
16
16
|
|
|
17
17
|
const VERB_ALIASES: Record<Verb, string[]> = {
|
|
18
18
|
up: ['docker:up', 'docker-up', 'docker:up:detached', 'start', 'dev', 'serve'],
|
package/src/publish.ts
CHANGED
|
@@ -392,6 +392,8 @@ export interface PublishDeps {
|
|
|
392
392
|
sleep: (ms: number) => void;
|
|
393
393
|
/** How long to wait for the registry to show the new version. */
|
|
394
394
|
registryWaitMs: number;
|
|
395
|
+
/** Rewrites one status line in place (a terminal); without it the wait logs a line now and then. */
|
|
396
|
+
progress?: (text: string) => void;
|
|
395
397
|
}
|
|
396
398
|
|
|
397
399
|
function sleepSync(ms: number): void {
|
|
@@ -407,6 +409,7 @@ export function defaultPublishDeps(): PublishDeps {
|
|
|
407
409
|
log: (line) => process.stderr.write(line + '\n'),
|
|
408
410
|
sleep: sleepSync,
|
|
409
411
|
registryWaitMs: 180_000,
|
|
412
|
+
progress: process.stderr.isTTY ? (text) => process.stderr.write(`\r${text}\x1b[K`) : undefined,
|
|
410
413
|
};
|
|
411
414
|
}
|
|
412
415
|
|
|
@@ -427,19 +430,38 @@ export function bumpLockText(text: string, version: string): string {
|
|
|
427
430
|
export function waitForRegistry(name: string, version: string, deps: PublishDeps): boolean {
|
|
428
431
|
const started = Date.now();
|
|
429
432
|
const deadline = started + deps.registryWaitMs;
|
|
430
|
-
|
|
431
|
-
|
|
433
|
+
const limit = Math.round(deps.registryWaitMs / 1000);
|
|
434
|
+
const seconds = () => Math.round((Date.now() - started) / 1000);
|
|
435
|
+
deps.log(
|
|
436
|
+
`→ waiting for the registry to show ${name}@${version} — npm has taken it; ` +
|
|
437
|
+
`the registry usually shows it within ~2 min (giving it ${limit} s)`,
|
|
438
|
+
);
|
|
439
|
+
const shown = () => {
|
|
432
440
|
try {
|
|
433
|
-
|
|
434
|
-
deps.log(`✓ ${name}@${version} is on the registry (${Math.round((Date.now() - started) / 1000)} s after publish)`);
|
|
435
|
-
return true;
|
|
436
|
-
}
|
|
441
|
+
return deps.npm(['view', `${name}@${version}`, 'version', '--prefer-online']).trim() === version;
|
|
437
442
|
} catch {
|
|
438
|
-
|
|
443
|
+
return false; // E404 until it is processed
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
// Ask npm every 10 s; between the asks, count the seconds on one line.
|
|
447
|
+
let lastLogged = 0;
|
|
448
|
+
for (let found = shown(); ; found = shown()) {
|
|
449
|
+
if (found) {
|
|
450
|
+
deps.progress?.('');
|
|
451
|
+
deps.log(`✓ ${name}@${version} is on the registry (${seconds()} s after publish)`);
|
|
452
|
+
return true;
|
|
439
453
|
}
|
|
440
454
|
if (Date.now() >= deadline) break;
|
|
441
|
-
|
|
455
|
+
for (let i = 0; i < 10 && Date.now() < deadline; i++) {
|
|
456
|
+
if (deps.progress) deps.progress(` … ${seconds()} s of ${limit} s`);
|
|
457
|
+
else if (seconds() - lastLogged >= 30) {
|
|
458
|
+
lastLogged = seconds();
|
|
459
|
+
deps.log(` … ${lastLogged} s`);
|
|
460
|
+
}
|
|
461
|
+
deps.sleep(1_000);
|
|
462
|
+
}
|
|
442
463
|
}
|
|
464
|
+
deps.progress?.('');
|
|
443
465
|
deps.log(
|
|
444
466
|
`! ${name}@${version} not visible on the registry after ${Math.round(deps.registryWaitMs / 60_000)} min — ` +
|
|
445
467
|
`npm accepted it; check later: npm view ${name}@${version} version`,
|