@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 +45 -5
- package/package.json +4 -1
- package/src/index.ts +46 -3
- package/src/report.ts +17 -0
- package/src/run.ts +56 -0
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|