@vodmal/vdx-cli 0.8.3 → 0.9.1
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 +113 -57
- 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.1",
|
|
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,124 @@ 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
|
-
|
|
280
|
+
id: 'vdx',
|
|
281
|
+
label: 'vdx',
|
|
282
|
+
status: 'ok',
|
|
283
|
+
level,
|
|
284
|
+
message: `${tag} — resolves in any shell (${found})`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (mode === 'npx-cache') {
|
|
288
|
+
return {
|
|
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`,
|
|
252
294
|
remedy:
|
|
253
|
-
'npm i -g @vodmal/vdx-cli OR alias vdx="npx -y -p @vodmal/vdx-cli vdx"',
|
|
295
|
+
'npm i -g @vodmal/vdx-cli OR add `alias vdx="npx -y -p @vodmal/vdx-cli vdx"` to your shell rc (~/.zshrc / ~/.bashrc)',
|
|
254
296
|
};
|
|
255
297
|
}
|
|
256
|
-
|
|
298
|
+
return {
|
|
299
|
+
id: 'vdx',
|
|
300
|
+
label: 'vdx',
|
|
301
|
+
status: 'ok',
|
|
302
|
+
level,
|
|
303
|
+
message: `${tag} — works inside this project only (${found})`,
|
|
304
|
+
remedy:
|
|
305
|
+
'npm i -g @vodmal/vdx-cli OR add `alias vdx="npx -y -p @vodmal/vdx-cli vdx"` to your shell rc',
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function checkClaudeCode(): CheckResult {
|
|
310
|
+
const found = findOnPath('claude');
|
|
311
|
+
if (!found) {
|
|
257
312
|
return {
|
|
258
|
-
id: '
|
|
259
|
-
label: '
|
|
313
|
+
id: 'claude-code',
|
|
314
|
+
label: 'Claude Code',
|
|
260
315
|
status: 'warning',
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
remedy: 'npm i -g @vodmal/vdx-cli for global shell access',
|
|
316
|
+
message: '`claude` CLI not on PATH',
|
|
317
|
+
remedy: 'https://docs.claude.com/en/docs/claude-code',
|
|
264
318
|
};
|
|
265
319
|
}
|
|
320
|
+
const v = probeVersion('claude', ['--version']);
|
|
266
321
|
return {
|
|
267
|
-
id: '
|
|
268
|
-
label: '
|
|
322
|
+
id: 'claude-code',
|
|
323
|
+
label: 'Claude Code',
|
|
269
324
|
status: 'ok',
|
|
270
325
|
level: 4,
|
|
271
|
-
message:
|
|
326
|
+
message: v ?? `installed (${found})`,
|
|
272
327
|
};
|
|
273
328
|
}
|
|
274
329
|
|
|
@@ -279,7 +334,7 @@ function checkClaudeCodePlugin(): CheckResult {
|
|
|
279
334
|
id: 'claude-plugin',
|
|
280
335
|
label: 'Claude Code vdx plugin',
|
|
281
336
|
status: 'warning',
|
|
282
|
-
message: '~/.claude/settings.json not found
|
|
337
|
+
message: '~/.claude/settings.json not found',
|
|
283
338
|
remedy: 'Install Claude Code, then add vdx marketplace (see README)',
|
|
284
339
|
};
|
|
285
340
|
}
|
|
@@ -290,14 +345,14 @@ function checkClaudeCodePlugin(): CheckResult {
|
|
|
290
345
|
id: 'claude-plugin',
|
|
291
346
|
label: 'Claude Code vdx plugin',
|
|
292
347
|
status: 'ok',
|
|
293
|
-
message: 'vdx marketplace
|
|
348
|
+
message: 'vdx marketplace registered',
|
|
294
349
|
};
|
|
295
350
|
}
|
|
296
351
|
return {
|
|
297
352
|
id: 'claude-plugin',
|
|
298
353
|
label: 'Claude Code vdx plugin',
|
|
299
354
|
status: 'warning',
|
|
300
|
-
message: '
|
|
355
|
+
message: 'vdx marketplace not registered',
|
|
301
356
|
remedy: 'Add github.com/VoDmAl/vdx/marketplace to extraKnownMarketplaces',
|
|
302
357
|
};
|
|
303
358
|
} catch (e: any) {
|
|
@@ -311,14 +366,15 @@ function checkClaudeCodePlugin(): CheckResult {
|
|
|
311
366
|
}
|
|
312
367
|
|
|
313
368
|
const CHECKS: Array<() => CheckResult> = [
|
|
369
|
+
checkVdxVersion,
|
|
370
|
+
checkVdx,
|
|
371
|
+
checkClaudeCode,
|
|
372
|
+
checkClaudeCodePlugin,
|
|
314
373
|
checkNode,
|
|
315
|
-
checkVdxOnPath,
|
|
316
|
-
checkVdxInShell,
|
|
317
374
|
checkGit,
|
|
318
375
|
checkMise,
|
|
319
376
|
checkNpmAuth,
|
|
320
377
|
checkContainerRuntime,
|
|
321
|
-
checkClaudeCodePlugin,
|
|
322
378
|
];
|
|
323
379
|
|
|
324
380
|
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
|
}
|