@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vodmal/vdx-cli",
3
- "version": "0.8.2",
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 checkVdxOnPath(): CheckResult {
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-on-path',
190
- label: 'vdx install',
269
+ id: 'vdx',
270
+ label: 'vdx',
191
271
  status: 'warning',
192
272
  level: 1,
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',
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 ephemeral = isEphemeralPath(found);
200
- if (ephemeral === 'npx-cache') {
277
+ const { mode, level, tag } = classifyVdxInstall(found);
278
+ if (mode === 'global') {
201
279
  return {
202
- id: 'vdx-on-path',
203
- label: 'vdx install',
280
+ id: 'vdx',
281
+ label: 'vdx',
204
282
  status: 'ok',
205
- 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".`,
283
+ level,
284
+ message: `${tag} — resolves in any shell (${found})`,
209
285
  };
210
286
  }
211
- if (ephemeral === 'local-bin') {
287
+ if (mode === 'npx-cache') {
212
288
  return {
213
- id: 'vdx-on-path',
214
- label: 'vdx install',
289
+ id: 'vdx',
290
+ label: 'vdx',
215
291
  status: 'ok',
216
- level: 2,
217
- message: `local node_modules: ${found} (works inside this project only)`,
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-on-path',
222
- label: 'vdx install',
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: `global: ${found}`,
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 — Claude Code not installed or not configured',
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 present in extraKnownMarketplaces',
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: 'Claude Code configured but vdx marketplace not registered',
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 name = a.slice(2);
63
- const next = rest[i + 1];
64
- if (next !== undefined && !next.startsWith('--')) {
65
- flags[name] = next;
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
- flags[name] = true;
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
- `- **OK**: ${r.ok} **Warning**: ${r.warning} **Missing**: ${r.missing}`,
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 md = reportDoctorMarkdown(r);
101
- const out = getTerminalMarked().parse(md) as string;
102
- return out.endsWith('\n') ? out : out + '\n';
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
  }