@vodmal/vdx-cli 0.8.2 → 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 +130 -29
- 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,47 +206,122 @@ function isEphemeralPath(p: string): 'npx-cache' | 'local-bin' | null {
|
|
|
182
206
|
return null;
|
|
183
207
|
}
|
|
184
208
|
|
|
185
|
-
function
|
|
209
|
+
function checkVdxVersion(): CheckResult {
|
|
210
|
+
const current = readCliVersion();
|
|
211
|
+
const latest = probeNpmLatestVersion('@vodmal/vdx-cli');
|
|
212
|
+
if (current === 'unknown') {
|
|
213
|
+
return {
|
|
214
|
+
id: 'vdx-version',
|
|
215
|
+
label: 'vdx version',
|
|
216
|
+
status: 'warning',
|
|
217
|
+
message: 'cannot read local cli package.json version',
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (!latest) {
|
|
221
|
+
return {
|
|
222
|
+
id: 'vdx-version',
|
|
223
|
+
label: 'vdx version',
|
|
224
|
+
status: 'ok',
|
|
225
|
+
level: 3,
|
|
226
|
+
message: `${current} (latest check skipped — offline or npm timeout)`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
const cmp = compareSemver(current, latest);
|
|
230
|
+
if (cmp < 0) {
|
|
231
|
+
return {
|
|
232
|
+
id: 'vdx-version',
|
|
233
|
+
label: 'vdx version',
|
|
234
|
+
status: 'warning',
|
|
235
|
+
level: 2,
|
|
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)`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
id: 'vdx-version',
|
|
251
|
+
label: 'vdx version',
|
|
252
|
+
status: 'ok',
|
|
253
|
+
level: 4,
|
|
254
|
+
message: `${current} (latest)`,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
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 {
|
|
186
266
|
const found = findOnPath('vdx');
|
|
187
267
|
if (!found) {
|
|
188
268
|
return {
|
|
189
|
-
id: 'vdx
|
|
190
|
-
label: 'vdx
|
|
269
|
+
id: 'vdx',
|
|
270
|
+
label: 'vdx',
|
|
191
271
|
status: 'warning',
|
|
192
272
|
level: 1,
|
|
193
|
-
message:
|
|
194
|
-
|
|
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',
|
|
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"',
|
|
197
275
|
};
|
|
198
276
|
}
|
|
199
|
-
const
|
|
200
|
-
if (
|
|
277
|
+
const { mode, level, tag } = classifyVdxInstall(found);
|
|
278
|
+
if (mode === 'global') {
|
|
201
279
|
return {
|
|
202
|
-
id: 'vdx
|
|
203
|
-
label: 'vdx
|
|
280
|
+
id: 'vdx',
|
|
281
|
+
label: 'vdx',
|
|
204
282
|
status: 'ok',
|
|
205
|
-
level
|
|
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".`,
|
|
283
|
+
level,
|
|
284
|
+
message: `${tag} — resolves in any shell (${found})`,
|
|
209
285
|
};
|
|
210
286
|
}
|
|
211
|
-
if (
|
|
287
|
+
if (mode === 'npx-cache') {
|
|
212
288
|
return {
|
|
213
|
-
id: 'vdx
|
|
214
|
-
label: 'vdx
|
|
289
|
+
id: 'vdx',
|
|
290
|
+
label: 'vdx',
|
|
215
291
|
status: 'ok',
|
|
216
|
-
level
|
|
217
|
-
message:
|
|
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',
|
|
218
295
|
};
|
|
219
296
|
}
|
|
220
297
|
return {
|
|
221
|
-
id: 'vdx
|
|
222
|
-
label: 'vdx
|
|
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',
|
|
313
|
+
status: 'warning',
|
|
314
|
+
message: '`claude` CLI not on PATH',
|
|
315
|
+
remedy: 'https://docs.claude.com/en/docs/claude-code',
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const v = probeVersion('claude', ['--version']);
|
|
319
|
+
return {
|
|
320
|
+
id: 'claude-code',
|
|
321
|
+
label: 'Claude Code',
|
|
223
322
|
status: 'ok',
|
|
224
323
|
level: 4,
|
|
225
|
-
message: `
|
|
324
|
+
message: v ?? `installed (${found})`,
|
|
226
325
|
};
|
|
227
326
|
}
|
|
228
327
|
|
|
@@ -233,7 +332,7 @@ function checkClaudeCodePlugin(): CheckResult {
|
|
|
233
332
|
id: 'claude-plugin',
|
|
234
333
|
label: 'Claude Code vdx plugin',
|
|
235
334
|
status: 'warning',
|
|
236
|
-
message: '~/.claude/settings.json not found
|
|
335
|
+
message: '~/.claude/settings.json not found',
|
|
237
336
|
remedy: 'Install Claude Code, then add vdx marketplace (see README)',
|
|
238
337
|
};
|
|
239
338
|
}
|
|
@@ -244,14 +343,14 @@ function checkClaudeCodePlugin(): CheckResult {
|
|
|
244
343
|
id: 'claude-plugin',
|
|
245
344
|
label: 'Claude Code vdx plugin',
|
|
246
345
|
status: 'ok',
|
|
247
|
-
message: 'vdx marketplace
|
|
346
|
+
message: 'vdx marketplace registered',
|
|
248
347
|
};
|
|
249
348
|
}
|
|
250
349
|
return {
|
|
251
350
|
id: 'claude-plugin',
|
|
252
351
|
label: 'Claude Code vdx plugin',
|
|
253
352
|
status: 'warning',
|
|
254
|
-
message: '
|
|
353
|
+
message: 'vdx marketplace not registered',
|
|
255
354
|
remedy: 'Add github.com/VoDmAl/vdx/marketplace to extraKnownMarketplaces',
|
|
256
355
|
};
|
|
257
356
|
} catch (e: any) {
|
|
@@ -265,13 +364,15 @@ function checkClaudeCodePlugin(): CheckResult {
|
|
|
265
364
|
}
|
|
266
365
|
|
|
267
366
|
const CHECKS: Array<() => CheckResult> = [
|
|
367
|
+
checkVdxVersion,
|
|
368
|
+
checkVdx,
|
|
369
|
+
checkClaudeCode,
|
|
370
|
+
checkClaudeCodePlugin,
|
|
268
371
|
checkNode,
|
|
269
|
-
checkVdxOnPath,
|
|
270
372
|
checkGit,
|
|
271
373
|
checkMise,
|
|
272
374
|
checkNpmAuth,
|
|
273
375
|
checkContainerRuntime,
|
|
274
|
-
checkClaudeCodePlugin,
|
|
275
376
|
];
|
|
276
377
|
|
|
277
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
|
}
|