@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vodmal/vdx-cli",
3
- "version": "0.8.3",
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 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',
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
- if (ephemeral === 'local-bin') {
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: 'vdx-in-shell',
259
- label: 'vdx in shell',
313
+ id: 'claude-code',
314
+ label: 'Claude Code',
260
315
  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',
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: 'vdx-in-shell',
268
- label: 'vdx in shell',
322
+ id: 'claude-code',
323
+ label: 'Claude Code',
269
324
  status: 'ok',
270
325
  level: 4,
271
- message: `yes — \`vdx\` resolves globally (${found})`,
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 — Claude Code not installed or not configured',
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 present in extraKnownMarketplaces',
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: 'Claude Code configured but vdx marketplace not registered',
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 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
  }