@vodmal/vdx-cli 0.4.0 → 0.6.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/README.md CHANGED
@@ -8,20 +8,53 @@ Published on npm as **[@vodmal/vdx-cli](https://www.npmjs.com/package/@vodmal/vd
8
8
 
9
9
  ## Install
10
10
 
11
+ Two equally supported paths:
12
+
11
13
  ```bash
12
- # global install
14
+ # A. Daily-use install — recommended when you run `vdx <verb>` many times per session
13
15
  npm install -g @vodmal/vdx-cli
16
+ vdx audit /path/to/project
14
17
 
15
- # or one-shot via npx (no install)
18
+ # B. Zero-install via npx — recommended for CI runners, one-shot trial, or fresh envs
16
19
  npx -y -p @vodmal/vdx-cli vdx audit /path/to/project
17
20
  ```
18
21
 
22
+ Same binary, same behavior. `npx` adds ~200–500 ms resolve overhead per
23
+ invocation; pick global when you'll run `vdx` repeatedly, npx when you
24
+ don't want anything in your global `node_modules` or you're in an
25
+ ephemeral environment.
26
+
19
27
  ## Use
20
28
 
21
29
  ```bash
30
+ # Lifecycle verbs (pass-through to `mise run <verb>`; requires mise + mise.toml)
31
+ vdx up | down | build | test | check | fix
32
+
33
+ # Maturity audit against the owner baseline rubric
22
34
  vdx audit <project-path> [--json] [--rubric <path>] [--stack <id>]
23
- vdx init <project-path> [--baseline github.com/org/repo@vX.Y] [--dry-run] [--force]
24
- vdx-mcp --project <path> # MCP stdio server (used by the plugin)
35
+
36
+ # Generate mise.toml + AGENTS.md for a project
37
+ vdx init <project-path> [--baseline github.com/org/repo@vX.Y] [--stack <id>] [--dry-run] [--force]
38
+
39
+ # Publish a library (Node MVP; PHP/Python coming in Y.3)
40
+ vdx publish <patch|minor|major> [--dry-run] [--force]
41
+
42
+ # MCP stdio server consumed by the Claude Code plugin
43
+ vdx-mcp --project <path>
44
+ ```
45
+
46
+ **Three quick examples**
47
+
48
+ ```bash
49
+ # 1. Score the project I'm standing in
50
+ vdx audit .
51
+
52
+ # 2. Wire native scripts into the 6 lifecycle verbs, then run one
53
+ vdx init . --stack node
54
+ vdx test # → mise run test (which calls `vitest run` or whatever was detected)
55
+
56
+ # 3. Ship a new minor release of a Node lib
57
+ vdx publish minor # bump → npm publish (OTP prompt) → git commit + tag (no push)
25
58
  ```
26
59
 
27
60
  By default the bundled `rubric/vdx-rubric.yaml` is used (a mirror of canonical
@@ -48,6 +81,9 @@ npm run typecheck
48
81
  - `src/evaluator.ts` — recursive evaluator + sugar notation
49
82
  - `src/scoring.ts` — delta-style levels, flags for orthogonal axes
50
83
  - `src/audit.ts` — orchestrator: overrides + applies_to filter + subpackage-ctx
84
+ - `src/init.ts` — `vdx init` planner (`selectVerbTask` + mise.toml/AGENTS.md renderers)
85
+ - `src/run.ts` — `resolveLifecycleVerb` + error renderer (pure logic for `vdx <verb>`)
86
+ - `src/publish.ts` — `planPublish` (pre-flight) + `executePublish` (bump → npm → git)
51
87
  - `src/report.ts` — markdown / JSON output
52
88
  - `src/index.ts` — CLI entry
53
89
  - `src/mcp-server.ts` — MCP stdio server (9 tools)
@@ -60,4 +96,8 @@ npm run typecheck
60
96
  - Baseline loading from a git ref is documented but evaluator still reads file
61
97
  paths only — `baseline:` in `mise.toml` is recorded but does not auto-fetch.
62
98
  - Watermark drift (phase 2 of `drift-algorithm.md`).
63
- - O25/O26/O27/O33/O34 — see [docs/decisions.md](../docs/decisions.md).
99
+ - `vdx publish` only ships for Node (MVP). PHP/Python in Y.3; Cargo/Ruby/Go/Java
100
+ in Y.4.
101
+ - Open items: O25 (mock-infra delta for Node), O26 (TOML round-trip), O27 (real
102
+ shared-infra precheck), O32 (multi-subpackage monorepo). See
103
+ [docs/decisions.md](../docs/decisions.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vodmal/vdx-cli",
3
- "version": "0.4.0",
3
+ "version": "0.6.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",
@@ -46,12 +46,15 @@
46
46
  "dependencies": {
47
47
  "@modelcontextprotocol/sdk": "^1.29.0",
48
48
  "js-yaml": "^4.1.0",
49
+ "marked": "^15.0.12",
50
+ "marked-terminal": "^7.3.0",
49
51
  "smol-toml": "^1.3.0",
50
52
  "tsx": "^4.19.0",
51
53
  "zod": "^3.23.0"
52
54
  },
53
55
  "devDependencies": {
54
56
  "@types/js-yaml": "^4.0.9",
57
+ "@types/marked-terminal": "^6.1.1",
55
58
  "@types/node": "^22.0.0",
56
59
  "@vitest/coverage-v8": "^4.1.7",
57
60
  "typescript": "^5.6.0",
package/src/index.ts CHANGED
@@ -1,10 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import * as path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
3
4
  import { loadRubric } from './rubric.ts';
4
5
  import { loadManifest, loadOverrides } from './manifest.ts';
5
6
  import { autoDetectStack, type Ctx } from './facts.ts';
7
+ import {
8
+ LIFECYCLE_VERBS,
9
+ type LifecycleVerb,
10
+ resolveLifecycleVerb,
11
+ renderResolveError,
12
+ } from './run.ts';
6
13
  import { audit } from './audit.ts';
7
- import { reportMarkdown, reportJson } from './report.ts';
14
+ import { reportMarkdown, reportJson, reportAnsi } from './report.ts';
8
15
  import { planInit, writeInit, renderPlanSummary } from './init.ts';
9
16
  import {
10
17
  planPublish,
@@ -20,7 +27,8 @@ const DEFAULT_RUBRIC = resolveDefaultRubric();
20
27
  function usage(): never {
21
28
  process.stderr.write(
22
29
  `Usage:
23
- vdx audit <project_path> [--rubric <path>] [--stack <stack>] [--json]
30
+ vdx <up|down|build|test|check|fix> run lifecycle verb (via mise run <verb>)
31
+ vdx audit <project_path> [--rubric <path>] [--stack <stack>] [--format=ansi|markdown|json] [--json]
24
32
  vdx init <project_path> [--stack <id>] [--baseline <ref>] [--dry-run] [--force]
25
33
  vdx publish <patch|minor|major> [--dry-run] [--force]
26
34
  `,
@@ -80,8 +88,19 @@ function cmdAudit(opts: ParsedArgs): void {
80
88
  const baselineRef = manifest?.baseline ?? `file://${rubricPath}`;
81
89
  const result = audit(rubric, ctx, overrides, baselineRef, manifest);
82
90
 
83
- if (opts.flags.json) {
91
+ const formatFlag =
92
+ typeof opts.flags.format === 'string' ? opts.flags.format : undefined;
93
+ const wantJson = opts.flags.json === true || formatFlag === 'json';
94
+ const wantMarkdown = formatFlag === 'markdown' || formatFlag === 'md';
95
+ const wantAnsi = formatFlag === 'ansi';
96
+ const isTty = process.stdout.isTTY === true;
97
+
98
+ if (wantJson) {
84
99
  process.stdout.write(reportJson(result) + '\n');
100
+ } else if (wantMarkdown) {
101
+ process.stdout.write(reportMarkdown(result));
102
+ } else if (wantAnsi || (isTty && !formatFlag)) {
103
+ process.stdout.write(reportAnsi(result));
85
104
  } else {
86
105
  process.stdout.write(reportMarkdown(result));
87
106
  }
@@ -176,8 +195,32 @@ function cmdPublish(opts: ParsedArgs): void {
176
195
  }
177
196
  }
178
197
 
198
+ function cmdRun(verb: LifecycleVerb): void {
199
+ const projectRoot = process.cwd();
200
+ const res = resolveLifecycleVerb(projectRoot, verb);
201
+ if (!res.ok) {
202
+ process.stderr.write(renderResolveError(res, projectRoot, verb));
203
+ process.exit(2);
204
+ }
205
+
206
+ try {
207
+ execFileSync('mise', ['run', verb], { cwd: projectRoot, stdio: 'inherit' });
208
+ } catch (e: any) {
209
+ if (e?.code === 'ENOENT') {
210
+ process.stderr.write(
211
+ `vdx: \`mise\` binary not found on PATH\n` +
212
+ `hint: install mise — https://mise.jdx.dev/getting-started.html\n`,
213
+ );
214
+ process.exit(127);
215
+ }
216
+ process.exit(typeof e?.status === 'number' ? e.status : 1);
217
+ }
218
+ }
219
+
179
220
  const parsed = parseArgs(process.argv);
180
221
  if (parsed.cmd === 'audit') cmdAudit(parsed);
181
222
  else if (parsed.cmd === 'init') cmdInit(parsed);
182
223
  else if (parsed.cmd === 'publish') cmdPublish(parsed);
224
+ else if ((LIFECYCLE_VERBS as readonly string[]).includes(parsed.cmd))
225
+ cmdRun(parsed.cmd as LifecycleVerb);
183
226
  else usage();
package/src/report.ts CHANGED
@@ -1,5 +1,16 @@
1
+ import { Marked } from 'marked';
2
+ import { markedTerminal } from 'marked-terminal';
1
3
  import type { AuditResult } from './audit.ts';
2
4
 
5
+ let terminalMarked: Marked | null = null;
6
+ function getTerminalMarked(): Marked {
7
+ if (terminalMarked) return terminalMarked;
8
+ const m = new Marked();
9
+ m.use(markedTerminal({ reflowText: false, tab: 2 }) as never);
10
+ terminalMarked = m;
11
+ return m;
12
+ }
13
+
3
14
  const SYMBOL: Record<string, string> = {
4
15
  aligned: '✅',
5
16
  gap: '⚠️ ',
@@ -47,3 +58,9 @@ export function reportMarkdown(r: AuditResult): string {
47
58
  export function reportJson(r: AuditResult): string {
48
59
  return JSON.stringify(r, null, 2);
49
60
  }
61
+
62
+ export function reportAnsi(r: AuditResult): string {
63
+ const md = reportMarkdown(r);
64
+ const out = getTerminalMarked().parse(md) as string;
65
+ return out.endsWith('\n') ? out : out + '\n';
66
+ }
package/src/run.ts ADDED
@@ -0,0 +1,56 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as TOML from 'smol-toml';
4
+
5
+ export const LIFECYCLE_VERBS = ['up', 'down', 'build', 'test', 'check', 'fix'] as const;
6
+ export type LifecycleVerb = (typeof LIFECYCLE_VERBS)[number];
7
+
8
+ export type VerbResolveResult =
9
+ | { ok: true; miseTomlPath: string }
10
+ | {
11
+ ok: false;
12
+ reason: 'no-mise-toml' | 'parse-error' | 'no-task';
13
+ detail?: string;
14
+ };
15
+
16
+ export function resolveLifecycleVerb(
17
+ projectRoot: string,
18
+ verb: string,
19
+ ): VerbResolveResult {
20
+ const miseTomlPath = path.join(projectRoot, 'mise.toml');
21
+ if (!fs.existsSync(miseTomlPath)) {
22
+ return { ok: false, reason: 'no-mise-toml' };
23
+ }
24
+ let parsed: unknown;
25
+ try {
26
+ parsed = TOML.parse(fs.readFileSync(miseTomlPath, 'utf8'));
27
+ } catch (e: any) {
28
+ return { ok: false, reason: 'parse-error', detail: e?.message ?? String(e) };
29
+ }
30
+ const tasks = (parsed as { tasks?: Record<string, unknown> })?.tasks;
31
+ if (!tasks || typeof tasks !== 'object' || !(verb in tasks)) {
32
+ return { ok: false, reason: 'no-task' };
33
+ }
34
+ return { ok: true, miseTomlPath };
35
+ }
36
+
37
+ export function renderResolveError(
38
+ res: Exclude<VerbResolveResult, { ok: true }>,
39
+ projectRoot: string,
40
+ verb: string,
41
+ ): string {
42
+ switch (res.reason) {
43
+ case 'no-mise-toml':
44
+ return (
45
+ `vdx: no mise.toml in ${projectRoot}\n` +
46
+ `hint: run \`vdx init .\` to generate one, or define it manually.\n`
47
+ );
48
+ case 'parse-error':
49
+ return `vdx: failed to parse mise.toml: ${res.detail ?? 'unknown error'}\n`;
50
+ case 'no-task':
51
+ return (
52
+ `vdx: no \`[tasks.${verb}]\` in mise.toml\n` +
53
+ `hint: run \`vdx init . --force\` to re-detect, or add the task manually.\n`
54
+ );
55
+ }
56
+ }