@vodmal/vdx-cli 0.8.3 → 0.9.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 +2 -1
- package/src/doctor.ts +112 -58
- package/src/index.ts +11 -6
- package/src/report.ts +53 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vodmal/vdx-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
48
|
+
"cli-table3": "^0.6.5",
|
|
48
49
|
"js-yaml": "^4.1.0",
|
|
49
50
|
"marked": "^15.0.12",
|
|
50
51
|
"marked-terminal": "^7.3.0",
|
package/src/doctor.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface DoctorReport {
|
|
|
23
23
|
missing: number;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function readCliVersion(): string {
|
|
26
|
+
export function readCliVersion(): string {
|
|
27
27
|
try {
|
|
28
28
|
const pkgPath = path.resolve(
|
|
29
29
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
@@ -37,6 +37,30 @@ function readCliVersion(): string {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function probeNpmLatestVersion(pkg: string, timeoutMs = 2000): string | null {
|
|
41
|
+
try {
|
|
42
|
+
const out = execFileSync('npm', ['view', pkg, 'version'], {
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
45
|
+
timeout: timeoutMs,
|
|
46
|
+
});
|
|
47
|
+
return out.trim() || null;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function compareSemver(a: string, b: string): number {
|
|
54
|
+
const pa = a.split('.').map((n) => parseInt(n, 10));
|
|
55
|
+
const pb = b.split('.').map((n) => parseInt(n, 10));
|
|
56
|
+
for (let i = 0; i < 3; i++) {
|
|
57
|
+
const ai = pa[i] ?? 0;
|
|
58
|
+
const bi = pb[i] ?? 0;
|
|
59
|
+
if (ai !== bi) return ai - bi;
|
|
60
|
+
}
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
40
64
|
function probeVersion(binary: string, args: string[] = ['--version']): string | null {
|
|
41
65
|
try {
|
|
42
66
|
const out = execFileSync(binary, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
@@ -182,93 +206,122 @@ function isEphemeralPath(p: string): 'npx-cache' | 'local-bin' | null {
|
|
|
182
206
|
return null;
|
|
183
207
|
}
|
|
184
208
|
|
|
185
|
-
function
|
|
186
|
-
const
|
|
187
|
-
|
|
209
|
+
function checkVdxVersion(): CheckResult {
|
|
210
|
+
const current = readCliVersion();
|
|
211
|
+
const latest = probeNpmLatestVersion('@vodmal/vdx-cli');
|
|
212
|
+
if (current === 'unknown') {
|
|
188
213
|
return {
|
|
189
|
-
id: 'vdx-
|
|
190
|
-
label: 'vdx
|
|
214
|
+
id: 'vdx-version',
|
|
215
|
+
label: 'vdx version',
|
|
191
216
|
status: 'warning',
|
|
192
|
-
|
|
193
|
-
message:
|
|
194
|
-
'`vdx` not on PATH at check time. If you use a shell alias it is also fine — doctor cannot detect aliases.',
|
|
195
|
-
remedy:
|
|
196
|
-
'npm i -g @vodmal/vdx-cli OR alias vdx="npx -y -p @vodmal/vdx-cli vdx" OR keep using `npx -y -p @vodmal/vdx-cli vdx …` ad-hoc',
|
|
217
|
+
message: 'cannot read local cli package.json version',
|
|
197
218
|
};
|
|
198
219
|
}
|
|
199
|
-
|
|
200
|
-
if (ephemeral === 'npx-cache') {
|
|
220
|
+
if (!latest) {
|
|
201
221
|
return {
|
|
202
|
-
id: 'vdx-
|
|
203
|
-
label: 'vdx
|
|
222
|
+
id: 'vdx-version',
|
|
223
|
+
label: 'vdx version',
|
|
204
224
|
status: 'ok',
|
|
205
225
|
level: 3,
|
|
206
|
-
message:
|
|
207
|
-
`via npx cache: ${found} — invoked-only (shell command \`vdx\` will not resolve outside npx). ` +
|
|
208
|
-
`For shell-global command run: npm i -g @vodmal/vdx-cli OR alias vdx="npx -y -p @vodmal/vdx-cli vdx".`,
|
|
226
|
+
message: `${current} (latest check skipped — offline or npm timeout)`,
|
|
209
227
|
};
|
|
210
228
|
}
|
|
211
|
-
|
|
229
|
+
const cmp = compareSemver(current, latest);
|
|
230
|
+
if (cmp < 0) {
|
|
212
231
|
return {
|
|
213
|
-
id: 'vdx-
|
|
214
|
-
label: 'vdx
|
|
215
|
-
status: '
|
|
232
|
+
id: 'vdx-version',
|
|
233
|
+
label: 'vdx version',
|
|
234
|
+
status: 'warning',
|
|
216
235
|
level: 2,
|
|
217
|
-
message:
|
|
236
|
+
message: `${current} → latest ${latest}`,
|
|
237
|
+
remedy: 'npm i -g @vodmal/vdx-cli@latest (or clear npx cache)',
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (cmp > 0) {
|
|
241
|
+
return {
|
|
242
|
+
id: 'vdx-version',
|
|
243
|
+
label: 'vdx version',
|
|
244
|
+
status: 'ok',
|
|
245
|
+
level: 4,
|
|
246
|
+
message: `${current} (ahead of npm latest ${latest} — local dev build)`,
|
|
218
247
|
};
|
|
219
248
|
}
|
|
220
249
|
return {
|
|
221
|
-
id: 'vdx-
|
|
222
|
-
label: 'vdx
|
|
250
|
+
id: 'vdx-version',
|
|
251
|
+
label: 'vdx version',
|
|
223
252
|
status: 'ok',
|
|
224
253
|
level: 4,
|
|
225
|
-
message:
|
|
254
|
+
message: `${current} (latest)`,
|
|
226
255
|
};
|
|
227
256
|
}
|
|
228
257
|
|
|
229
|
-
function
|
|
258
|
+
function classifyVdxInstall(found: string): { mode: 'global' | 'npx-cache' | 'local-bin'; level: number; tag: string } {
|
|
259
|
+
const ephemeral = isEphemeralPath(found);
|
|
260
|
+
if (ephemeral === 'npx-cache') return { mode: 'npx-cache', level: 3, tag: 'npx cache (ephemeral)' };
|
|
261
|
+
if (ephemeral === 'local-bin') return { mode: 'local-bin', level: 2, tag: 'project node_modules' };
|
|
262
|
+
return { mode: 'global', level: 4, tag: 'global' };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function checkVdx(): CheckResult {
|
|
230
266
|
const found = findOnPath('vdx');
|
|
231
267
|
if (!found) {
|
|
232
268
|
return {
|
|
233
|
-
id: 'vdx
|
|
234
|
-
label: 'vdx
|
|
269
|
+
id: 'vdx',
|
|
270
|
+
label: 'vdx',
|
|
235
271
|
status: 'warning',
|
|
236
272
|
level: 1,
|
|
237
|
-
message:
|
|
238
|
-
|
|
239
|
-
remedy:
|
|
240
|
-
'npm i -g @vodmal/vdx-cli OR alias vdx="npx -y -p @vodmal/vdx-cli vdx"',
|
|
273
|
+
message: 'not on PATH (alias may still work — doctor cannot detect aliases)',
|
|
274
|
+
remedy: 'npm i -g @vodmal/vdx-cli OR alias vdx="npx -y -p @vodmal/vdx-cli vdx"',
|
|
241
275
|
};
|
|
242
276
|
}
|
|
243
|
-
const
|
|
244
|
-
if (
|
|
277
|
+
const { mode, level, tag } = classifyVdxInstall(found);
|
|
278
|
+
if (mode === 'global') {
|
|
245
279
|
return {
|
|
246
|
-
id: 'vdx
|
|
247
|
-
label: 'vdx
|
|
248
|
-
status: '
|
|
249
|
-
level
|
|
250
|
-
message:
|
|
251
|
-
'no — only the ephemeral npx-cache binary is on PATH; bare `vdx <verb>` in a fresh shell will not resolve',
|
|
252
|
-
remedy:
|
|
253
|
-
'npm i -g @vodmal/vdx-cli OR alias vdx="npx -y -p @vodmal/vdx-cli vdx"',
|
|
280
|
+
id: 'vdx',
|
|
281
|
+
label: 'vdx',
|
|
282
|
+
status: 'ok',
|
|
283
|
+
level,
|
|
284
|
+
message: `${tag} — resolves in any shell (${found})`,
|
|
254
285
|
};
|
|
255
286
|
}
|
|
256
|
-
if (
|
|
287
|
+
if (mode === 'npx-cache') {
|
|
257
288
|
return {
|
|
258
|
-
id: 'vdx
|
|
259
|
-
label: 'vdx
|
|
289
|
+
id: 'vdx',
|
|
290
|
+
label: 'vdx',
|
|
291
|
+
status: 'ok',
|
|
292
|
+
level,
|
|
293
|
+
message: `${tag} — \`npx -y -p @vodmal/vdx-cli vdx …\` works; bare \`vdx\` in a fresh shell does not`,
|
|
294
|
+
remedy: 'npm i -g @vodmal/vdx-cli for shell-global command',
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
id: 'vdx',
|
|
299
|
+
label: 'vdx',
|
|
300
|
+
status: 'ok',
|
|
301
|
+
level,
|
|
302
|
+
message: `${tag} — works inside this project only (${found})`,
|
|
303
|
+
remedy: 'npm i -g @vodmal/vdx-cli for global shell access',
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function checkClaudeCode(): CheckResult {
|
|
308
|
+
const found = findOnPath('claude');
|
|
309
|
+
if (!found) {
|
|
310
|
+
return {
|
|
311
|
+
id: 'claude-code',
|
|
312
|
+
label: 'Claude Code',
|
|
260
313
|
status: 'warning',
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
remedy: 'npm i -g @vodmal/vdx-cli for global shell access',
|
|
314
|
+
message: '`claude` CLI not on PATH',
|
|
315
|
+
remedy: 'https://docs.claude.com/en/docs/claude-code',
|
|
264
316
|
};
|
|
265
317
|
}
|
|
318
|
+
const v = probeVersion('claude', ['--version']);
|
|
266
319
|
return {
|
|
267
|
-
id: '
|
|
268
|
-
label: '
|
|
320
|
+
id: 'claude-code',
|
|
321
|
+
label: 'Claude Code',
|
|
269
322
|
status: 'ok',
|
|
270
323
|
level: 4,
|
|
271
|
-
message:
|
|
324
|
+
message: v ?? `installed (${found})`,
|
|
272
325
|
};
|
|
273
326
|
}
|
|
274
327
|
|
|
@@ -279,7 +332,7 @@ function checkClaudeCodePlugin(): CheckResult {
|
|
|
279
332
|
id: 'claude-plugin',
|
|
280
333
|
label: 'Claude Code vdx plugin',
|
|
281
334
|
status: 'warning',
|
|
282
|
-
message: '~/.claude/settings.json not found
|
|
335
|
+
message: '~/.claude/settings.json not found',
|
|
283
336
|
remedy: 'Install Claude Code, then add vdx marketplace (see README)',
|
|
284
337
|
};
|
|
285
338
|
}
|
|
@@ -290,14 +343,14 @@ function checkClaudeCodePlugin(): CheckResult {
|
|
|
290
343
|
id: 'claude-plugin',
|
|
291
344
|
label: 'Claude Code vdx plugin',
|
|
292
345
|
status: 'ok',
|
|
293
|
-
message: 'vdx marketplace
|
|
346
|
+
message: 'vdx marketplace registered',
|
|
294
347
|
};
|
|
295
348
|
}
|
|
296
349
|
return {
|
|
297
350
|
id: 'claude-plugin',
|
|
298
351
|
label: 'Claude Code vdx plugin',
|
|
299
352
|
status: 'warning',
|
|
300
|
-
message: '
|
|
353
|
+
message: 'vdx marketplace not registered',
|
|
301
354
|
remedy: 'Add github.com/VoDmAl/vdx/marketplace to extraKnownMarketplaces',
|
|
302
355
|
};
|
|
303
356
|
} catch (e: any) {
|
|
@@ -311,14 +364,15 @@ function checkClaudeCodePlugin(): CheckResult {
|
|
|
311
364
|
}
|
|
312
365
|
|
|
313
366
|
const CHECKS: Array<() => CheckResult> = [
|
|
367
|
+
checkVdxVersion,
|
|
368
|
+
checkVdx,
|
|
369
|
+
checkClaudeCode,
|
|
370
|
+
checkClaudeCodePlugin,
|
|
314
371
|
checkNode,
|
|
315
|
-
checkVdxOnPath,
|
|
316
|
-
checkVdxInShell,
|
|
317
372
|
checkGit,
|
|
318
373
|
checkMise,
|
|
319
374
|
checkNpmAuth,
|
|
320
375
|
checkContainerRuntime,
|
|
321
|
-
checkClaudeCodePlugin,
|
|
322
376
|
];
|
|
323
377
|
|
|
324
378
|
export function runDoctor(): DoctorReport {
|
package/src/index.ts
CHANGED
|
@@ -59,13 +59,18 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
59
59
|
for (let i = 0; i < rest.length; i++) {
|
|
60
60
|
const a = rest[i]!;
|
|
61
61
|
if (a.startsWith('--')) {
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
flags[
|
|
66
|
-
i++;
|
|
62
|
+
const body = a.slice(2);
|
|
63
|
+
const eqIdx = body.indexOf('=');
|
|
64
|
+
if (eqIdx >= 0) {
|
|
65
|
+
flags[body.slice(0, eqIdx)] = body.slice(eqIdx + 1);
|
|
67
66
|
} else {
|
|
68
|
-
|
|
67
|
+
const next = rest[i + 1];
|
|
68
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
69
|
+
flags[body] = next;
|
|
70
|
+
i++;
|
|
71
|
+
} else {
|
|
72
|
+
flags[body] = true;
|
|
73
|
+
}
|
|
69
74
|
}
|
|
70
75
|
} else {
|
|
71
76
|
positionals.push(a);
|
package/src/report.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { Marked } from 'marked';
|
|
2
2
|
import { markedTerminal } from 'marked-terminal';
|
|
3
|
+
import Table from 'cli-table3';
|
|
3
4
|
import type { AuditResult } from './audit.ts';
|
|
4
5
|
import type { DoctorReport, CheckStatus } from './doctor.ts';
|
|
5
6
|
|
|
7
|
+
const ANSI = {
|
|
8
|
+
bold: (s: string): string => `\x1b[1m${s}\x1b[22m`,
|
|
9
|
+
dim: (s: string): string => `\x1b[2m${s}\x1b[22m`,
|
|
10
|
+
green: (s: string): string => `\x1b[32m${s}\x1b[39m`,
|
|
11
|
+
yellow: (s: string): string => `\x1b[33m${s}\x1b[39m`,
|
|
12
|
+
red: (s: string): string => `\x1b[31m${s}\x1b[39m`,
|
|
13
|
+
};
|
|
14
|
+
|
|
6
15
|
let terminalMarked: Marked | null = null;
|
|
7
16
|
function getTerminalMarked(): Marked {
|
|
8
17
|
if (terminalMarked) return terminalMarked;
|
|
@@ -76,9 +85,8 @@ export function reportDoctorMarkdown(r: DoctorReport): string {
|
|
|
76
85
|
const lines: string[] = [];
|
|
77
86
|
lines.push('# vdx doctor report');
|
|
78
87
|
lines.push('');
|
|
79
|
-
lines.push(`- **@vodmal/vdx-cli version**: \`${r.cliVersion}\``);
|
|
80
88
|
lines.push(
|
|
81
|
-
|
|
89
|
+
`**OK**: ${r.ok} **Warning**: ${r.warning} **Missing**: ${r.missing}`,
|
|
82
90
|
);
|
|
83
91
|
lines.push('');
|
|
84
92
|
lines.push('| Check | Status | Detail | Remedy |');
|
|
@@ -96,8 +104,48 @@ export function reportDoctorJson(r: DoctorReport): string {
|
|
|
96
104
|
return JSON.stringify(r, null, 2);
|
|
97
105
|
}
|
|
98
106
|
|
|
107
|
+
const STATUS_RENDER: Record<CheckStatus, (s: string) => string> = {
|
|
108
|
+
ok: ANSI.green,
|
|
109
|
+
warning: ANSI.yellow,
|
|
110
|
+
missing: ANSI.red,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function termWidth(fallback = 100): number {
|
|
114
|
+
const w = process.stdout.columns;
|
|
115
|
+
if (typeof w === 'number' && w >= 60) return w;
|
|
116
|
+
return fallback;
|
|
117
|
+
}
|
|
118
|
+
|
|
99
119
|
export function reportDoctorAnsi(r: DoctorReport): string {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
120
|
+
const lines: string[] = [];
|
|
121
|
+
lines.push(ANSI.bold('vdx doctor'));
|
|
122
|
+
const summary =
|
|
123
|
+
`${STATUS_RENDER.ok(`✅ ${r.ok} ok`)} ` +
|
|
124
|
+
`${STATUS_RENDER.warning(`⚠ ${r.warning} warning`)} ` +
|
|
125
|
+
`${STATUS_RENDER.missing(`✗ ${r.missing} missing`)}`;
|
|
126
|
+
lines.push(summary);
|
|
127
|
+
lines.push('');
|
|
128
|
+
|
|
129
|
+
const total = termWidth();
|
|
130
|
+
const checkW = 18;
|
|
131
|
+
const statusW = 14;
|
|
132
|
+
const remedyW = Math.max(20, Math.min(36, Math.floor((total - checkW - statusW - 6) * 0.4)));
|
|
133
|
+
const detailW = Math.max(24, total - checkW - statusW - remedyW - 6);
|
|
134
|
+
|
|
135
|
+
const table = new Table({
|
|
136
|
+
head: [ANSI.bold('Check'), ANSI.bold('Status'), ANSI.bold('Detail'), ANSI.bold('Remedy')],
|
|
137
|
+
colWidths: [checkW, statusW, detailW, remedyW],
|
|
138
|
+
wordWrap: true,
|
|
139
|
+
wrapOnWordBoundary: true,
|
|
140
|
+
style: { head: [], border: [] },
|
|
141
|
+
});
|
|
142
|
+
for (const c of r.checks) {
|
|
143
|
+
const sym = CHECK_SYMBOL[c.status];
|
|
144
|
+
const level = c.level !== undefined ? ` L${c.level}` : '';
|
|
145
|
+
const statusCell = STATUS_RENDER[c.status](`${sym} ${c.status}${level}`);
|
|
146
|
+
table.push([ANSI.bold(c.label), statusCell, c.message, c.remedy ?? ANSI.dim('—')]);
|
|
147
|
+
}
|
|
148
|
+
lines.push(table.toString());
|
|
149
|
+
lines.push('');
|
|
150
|
+
return lines.join('\n');
|
|
103
151
|
}
|