@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vodmal/vdx-cli",
3
- "version": "0.8.3",
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 checkVdxOnPath(): CheckResult {
186
- const found = findOnPath('vdx');
187
- if (!found) {
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-on-path',
190
- label: 'vdx install',
214
+ id: 'vdx-version',
215
+ label: 'vdx version',
191
216
  status: 'warning',
192
- 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',
217
+ message: 'cannot read local cli package.json version',
197
218
  };
198
219
  }
199
- const ephemeral = isEphemeralPath(found);
200
- if (ephemeral === 'npx-cache') {
220
+ if (!latest) {
201
221
  return {
202
- id: 'vdx-on-path',
203
- label: 'vdx install',
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
- if (ephemeral === 'local-bin') {
229
+ const cmp = compareSemver(current, latest);
230
+ if (cmp < 0) {
212
231
  return {
213
- id: 'vdx-on-path',
214
- label: 'vdx install',
215
- status: 'ok',
232
+ id: 'vdx-version',
233
+ label: 'vdx version',
234
+ status: 'warning',
216
235
  level: 2,
217
- message: `local node_modules: ${found} (works inside this project only)`,
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-on-path',
222
- label: 'vdx install',
250
+ id: 'vdx-version',
251
+ label: 'vdx version',
223
252
  status: 'ok',
224
253
  level: 4,
225
- message: `global: ${found}`,
254
+ message: `${current} (latest)`,
226
255
  };
227
256
  }
228
257
 
229
- function checkVdxInShell(): CheckResult {
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-in-shell',
234
- label: 'vdx in shell',
269
+ id: 'vdx',
270
+ label: 'vdx',
235
271
  status: 'warning',
236
272
  level: 1,
237
- message:
238
- 'no — `vdx` command not on persistent PATH. If you set a shell alias, it works but doctor cannot detect aliases.',
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 ephemeral = isEphemeralPath(found);
244
- if (ephemeral === 'npx-cache') {
277
+ const { mode, level, tag } = classifyVdxInstall(found);
278
+ if (mode === 'global') {
245
279
  return {
246
- id: 'vdx-in-shell',
247
- label: 'vdx in shell',
248
- status: 'warning',
249
- level: 1,
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 (ephemeral === 'local-bin') {
287
+ if (mode === 'npx-cache') {
257
288
  return {
258
- id: 'vdx-in-shell',
259
- label: 'vdx in shell',
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
- level: 2,
262
- message: `project-only — \`vdx\` works inside this project root (${found}) but not elsewhere`,
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: 'vdx-in-shell',
268
- label: 'vdx in shell',
320
+ id: 'claude-code',
321
+ label: 'Claude Code',
269
322
  status: 'ok',
270
323
  level: 4,
271
- message: `yes — \`vdx\` resolves globally (${found})`,
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 — Claude Code not installed or not configured',
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 present in extraKnownMarketplaces',
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: 'Claude Code configured but vdx marketplace not registered',
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 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
  }