@vodmal/vdx-cli 0.24.0 → 0.26.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 +5 -4
- package/package.json +10 -1
- package/rubric/vdx-rubric.yaml +76 -32
- package/src/ai.ts +221 -76
- package/src/audit.ts +19 -9
- package/src/author.ts +27 -8
- package/src/clone-checks.ts +14 -9
- package/src/conversations.ts +48 -22
- package/src/doctor.ts +14 -15
- package/src/evaluator.ts +21 -20
- package/src/facts.ts +71 -56
- package/src/gha.ts +38 -27
- package/src/hooks.ts +78 -43
- package/src/index.ts +56 -41
- package/src/init.ts +22 -61
- package/src/manifest.ts +25 -4
- package/src/mcp-server.ts +40 -39
- package/src/narrow.ts +56 -0
- package/src/personal-hooks.ts +16 -5
- package/src/predicates.ts +12 -16
- package/src/publish.ts +38 -31
- package/src/report.ts +1 -3
- package/src/rubric.ts +2 -2
- package/src/run.ts +5 -8
- package/src/scoring.ts +2 -8
- package/src/vocabulary.ts +13 -12
package/src/index.ts
CHANGED
|
@@ -2,14 +2,10 @@
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
4
|
import { loadRubric } from './rubric.ts';
|
|
5
|
-
import { loadManifest, loadOverrides } from './manifest.ts';
|
|
5
|
+
import { loadManifest, loadManifestAt, loadOverrides } from './manifest.ts';
|
|
6
6
|
import { autoDetectStack, type Ctx } from './facts.ts';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
type LifecycleVerb,
|
|
10
|
-
resolveLifecycleVerb,
|
|
11
|
-
renderResolveError,
|
|
12
|
-
} from './run.ts';
|
|
7
|
+
import { at, errorMessage } from './narrow.ts';
|
|
8
|
+
import { LIFECYCLE_VERBS, type LifecycleVerb, resolveLifecycleVerb, renderResolveError } from './run.ts';
|
|
13
9
|
import { audit } from './audit.ts';
|
|
14
10
|
import {
|
|
15
11
|
reportMarkdown,
|
|
@@ -19,7 +15,14 @@ import {
|
|
|
19
15
|
reportDoctorJson,
|
|
20
16
|
reportDoctorAnsi,
|
|
21
17
|
} from './report.ts';
|
|
22
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
runDoctor,
|
|
20
|
+
runDoctorCheck,
|
|
21
|
+
runDoctorFix,
|
|
22
|
+
resolveDoctorCtx,
|
|
23
|
+
looksLikeProject,
|
|
24
|
+
readCliVersion,
|
|
25
|
+
} from './doctor.ts';
|
|
23
26
|
import { planInit, writeInit, renderPlanSummary } from './init.ts';
|
|
24
27
|
import {
|
|
25
28
|
planPublish,
|
|
@@ -66,6 +69,7 @@ interface CommandSpec {
|
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
const AI_HELP = `Starts your agent in the project, or attaches to the one already running there.
|
|
72
|
+
Several running there: a list to pick from — Enter takes the newest.
|
|
69
73
|
Which agent, its flags and whether it runs in tmux come from your profile:
|
|
70
74
|
$VDX_ENVIRONMENT, else ~/.vdx-environment.yaml, else a plain claude/codex.
|
|
71
75
|
|
|
@@ -75,15 +79,20 @@ the project's files). Hand a person \`vdx ai\`, not \`<agent> --<flag>\`.
|
|
|
75
79
|
|
|
76
80
|
project_path the project (default: cwd); its git toplevel is used
|
|
77
81
|
@host the same command on <host> over ssh: its vdx, profile and tmux
|
|
78
|
-
--new a new conversation instead of continuing the last one
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
--new a new conversation instead of continuing the last one; an agent
|
|
83
|
+
already running here keeps running, the new one starts next to
|
|
84
|
+
it (a new tmux window)
|
|
85
|
+
--focused a new conversation, as --new, in a session nothing wakes: the
|
|
86
|
+
agent, its hooks and MCP servers get VDX_FOCUSED=1 — for the
|
|
87
|
+
plugins that wake sessions to leave it alone — and the profile's
|
|
88
|
+
rules marked \`wakes: true\` (a channel flag) are left out. Back
|
|
89
|
+
to it: --focused --conversation <id>. A running agent keeps its
|
|
90
|
+
mode: plain vdx ai attaches to it as it is
|
|
84
91
|
--conversation <id>
|
|
85
|
-
continue this Claude Code conversation
|
|
86
|
-
|
|
92
|
+
continue this Claude Code conversation: attach to the agent in
|
|
93
|
+
it, or start one next to the agents running here
|
|
94
|
+
--restart restart the running agent in its pane — applies a changed profile;
|
|
95
|
+
with --new, --focused or --conversation, it is replaced by that.
|
|
87
96
|
Without it, an agent running without the profile's flags is
|
|
88
97
|
a question in a terminal: Enter attaches to it as is, r
|
|
89
98
|
restarts it, q leaves it
|
|
@@ -251,7 +260,9 @@ function checkArgs(name: string, opts: ParsedArgs): ParsedArgs {
|
|
|
251
260
|
process.exit(2);
|
|
252
261
|
};
|
|
253
262
|
const unknown = [
|
|
254
|
-
...Object.keys(flags)
|
|
263
|
+
...Object.keys(flags)
|
|
264
|
+
.filter((f) => !bools.includes(f) && !values.includes(f))
|
|
265
|
+
.map((f) => `--${f}`),
|
|
255
266
|
...positionals.filter((p) => p.startsWith('-')),
|
|
256
267
|
];
|
|
257
268
|
if (unknown.length > 0) fail(`unknown option ${unknown.join(', ')}`);
|
|
@@ -275,14 +286,20 @@ function cmdAudit(opts: ParsedArgs): void {
|
|
|
275
286
|
);
|
|
276
287
|
}
|
|
277
288
|
|
|
278
|
-
const
|
|
289
|
+
const found = loadManifestAt(projectRoot);
|
|
290
|
+
const manifest = found?.manifest ?? null;
|
|
291
|
+
if (found?.place === 'vdx') {
|
|
292
|
+
process.stderr.write(
|
|
293
|
+
'note: the manifest is in [vdx] of mise.toml, and mise 2026.10 warns about it on every run; ' +
|
|
294
|
+
'its place is [_.vdx], once every machine of the project runs vdx 0.26 or newer\n',
|
|
295
|
+
);
|
|
296
|
+
}
|
|
279
297
|
const overrides = loadOverrides(projectRoot);
|
|
280
298
|
|
|
281
299
|
const rubricPath = (opts.flags.rubric as string) || DEFAULT_RUBRIC;
|
|
282
300
|
const rubric = loadRubric(rubricPath);
|
|
283
301
|
|
|
284
|
-
const stack =
|
|
285
|
-
(opts.flags.stack as string) || manifest?.stack || autoDetectStack(projectRoot);
|
|
302
|
+
const stack = (opts.flags.stack as string) || manifest?.stack || autoDetectStack(projectRoot);
|
|
286
303
|
|
|
287
304
|
const ctx: Ctx = {
|
|
288
305
|
projectRoot,
|
|
@@ -293,8 +310,7 @@ function cmdAudit(opts: ParsedArgs): void {
|
|
|
293
310
|
const baselineRef = manifest?.baseline ?? `file://${rubricPath}`;
|
|
294
311
|
const result = audit(rubric, ctx, overrides, baselineRef, manifest);
|
|
295
312
|
|
|
296
|
-
const formatFlag =
|
|
297
|
-
typeof opts.flags.format === 'string' ? opts.flags.format : undefined;
|
|
313
|
+
const formatFlag = typeof opts.flags.format === 'string' ? opts.flags.format : undefined;
|
|
298
314
|
const wantJson = opts.flags.json === true || formatFlag === 'json';
|
|
299
315
|
const wantMarkdown = formatFlag === 'markdown' || formatFlag === 'md';
|
|
300
316
|
const wantAnsi = formatFlag === 'ansi';
|
|
@@ -338,8 +354,8 @@ function cmdInit(opts: ParsedArgs): void {
|
|
|
338
354
|
try {
|
|
339
355
|
writeInit(plan, { force: Boolean(opts.flags.force) });
|
|
340
356
|
process.stderr.write(`\n✓ wrote ${plan.miseTomlPath}\n`);
|
|
341
|
-
} catch (e
|
|
342
|
-
process.stderr.write(`\nerror: ${
|
|
357
|
+
} catch (e) {
|
|
358
|
+
process.stderr.write(`\nerror: ${errorMessage(e)}\n`);
|
|
343
359
|
process.exit(2);
|
|
344
360
|
}
|
|
345
361
|
}
|
|
@@ -347,9 +363,7 @@ function cmdInit(opts: ParsedArgs): void {
|
|
|
347
363
|
function cmdPublish(opts: ParsedArgs): void {
|
|
348
364
|
const bumpArg = opts.positionals[0];
|
|
349
365
|
if (!bumpArg || !['patch', 'minor', 'major'].includes(bumpArg)) {
|
|
350
|
-
process.stderr.write(
|
|
351
|
-
'Usage: vdx publish <patch|minor|major> [--dry-run] [--force] [--no-push]\n',
|
|
352
|
-
);
|
|
366
|
+
process.stderr.write('Usage: vdx publish <patch|minor|major> [--dry-run] [--force] [--no-push]\n');
|
|
353
367
|
process.exit(1);
|
|
354
368
|
}
|
|
355
369
|
const bump = bumpArg as BumpKind;
|
|
@@ -374,8 +388,8 @@ function cmdPublish(opts: ParsedArgs): void {
|
|
|
374
388
|
const replan = () => {
|
|
375
389
|
try {
|
|
376
390
|
return planPublish(planOpts, auditResult, ctx);
|
|
377
|
-
} catch (e
|
|
378
|
-
process.stderr.write(`error: ${
|
|
391
|
+
} catch (e) {
|
|
392
|
+
process.stderr.write(`error: ${errorMessage(e)}\n`);
|
|
379
393
|
process.exit(2);
|
|
380
394
|
}
|
|
381
395
|
};
|
|
@@ -412,8 +426,8 @@ function cmdPublish(opts: ParsedArgs): void {
|
|
|
412
426
|
|
|
413
427
|
try {
|
|
414
428
|
executePublish(plan, deps);
|
|
415
|
-
} catch (e
|
|
416
|
-
process.stderr.write(`\nerror: ${
|
|
429
|
+
} catch (e) {
|
|
430
|
+
process.stderr.write(`\nerror: ${errorMessage(e)}\n`);
|
|
417
431
|
process.exit(3);
|
|
418
432
|
}
|
|
419
433
|
}
|
|
@@ -428,15 +442,15 @@ function cmdRun(verb: LifecycleVerb): void {
|
|
|
428
442
|
|
|
429
443
|
try {
|
|
430
444
|
execFileSync('mise', ['run', verb], { cwd: projectRoot, stdio: 'inherit' });
|
|
431
|
-
} catch (e
|
|
432
|
-
if (e
|
|
445
|
+
} catch (e) {
|
|
446
|
+
if (at(e, 'code') === 'ENOENT') {
|
|
433
447
|
process.stderr.write(
|
|
434
|
-
`vdx: \`mise\` binary not found on PATH\n` +
|
|
435
|
-
`hint: install mise — https://mise.jdx.dev/getting-started.html\n`,
|
|
448
|
+
`vdx: \`mise\` binary not found on PATH\n` + `hint: install mise — https://mise.jdx.dev/getting-started.html\n`,
|
|
436
449
|
);
|
|
437
450
|
process.exit(127);
|
|
438
451
|
}
|
|
439
|
-
|
|
452
|
+
const status = at(e, 'status');
|
|
453
|
+
process.exit(typeof status === 'number' ? status : 1);
|
|
440
454
|
}
|
|
441
455
|
}
|
|
442
456
|
|
|
@@ -454,16 +468,17 @@ function cmdDoctor(opts: ParsedArgs): void {
|
|
|
454
468
|
if (opts.flags.fix === true) {
|
|
455
469
|
try {
|
|
456
470
|
const done = runDoctorFix();
|
|
457
|
-
process.stderr.write(
|
|
458
|
-
|
|
459
|
-
|
|
471
|
+
process.stderr.write(
|
|
472
|
+
done.length > 0 ? done.map((d) => `✓ ${d}`).join('\n') + '\n' : 'nothing to fix in ~/.gitconfig\n',
|
|
473
|
+
);
|
|
474
|
+
} catch (e) {
|
|
475
|
+
process.stderr.write(`vdx doctor --fix: ${errorMessage(e)}\n`);
|
|
460
476
|
process.exit(2);
|
|
461
477
|
}
|
|
462
478
|
}
|
|
463
479
|
const report = runDoctor(ctx);
|
|
464
480
|
|
|
465
|
-
const formatFlag =
|
|
466
|
-
typeof opts.flags.format === 'string' ? opts.flags.format : undefined;
|
|
481
|
+
const formatFlag = typeof opts.flags.format === 'string' ? opts.flags.format : undefined;
|
|
467
482
|
const wantJson = opts.flags.json === true || formatFlag === 'json';
|
|
468
483
|
const wantMarkdown = formatFlag === 'markdown' || formatFlag === 'md';
|
|
469
484
|
const wantAnsi = formatFlag === 'ansi';
|
package/src/init.ts
CHANGED
|
@@ -1,32 +1,17 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
readJson,
|
|
6
|
-
listAllTasks,
|
|
7
|
-
autoDetectStack,
|
|
8
|
-
findSubPackages,
|
|
9
|
-
type SubPackage,
|
|
10
|
-
} from './facts.ts';
|
|
3
|
+
import { Ctx, readJson, listAllTasks, autoDetectStack, findSubPackages, type SubPackage } from './facts.ts';
|
|
4
|
+
import { at } from './narrow.ts';
|
|
11
5
|
|
|
12
6
|
export const STANDARD_VERBS = ['up', 'down', 'build', 'test', 'check', 'fix'] as const;
|
|
13
7
|
export type Verb = (typeof STANDARD_VERBS)[number];
|
|
14
8
|
|
|
15
|
-
export const DEFAULT_BASELINE = 'github.com/VoDmAl/vdx-rubric-vodmal@v1.
|
|
9
|
+
export const DEFAULT_BASELINE = 'github.com/VoDmAl/vdx-rubric-vodmal@v1.4.2';
|
|
16
10
|
|
|
17
11
|
const VERB_ALIASES: Record<Verb, string[]> = {
|
|
18
12
|
up: ['docker:up', 'docker-up', 'docker:up:detached', 'start', 'dev', 'serve'],
|
|
19
13
|
down: ['docker:down', 'docker-down', 'stop'],
|
|
20
|
-
build: [
|
|
21
|
-
'docker:build',
|
|
22
|
-
'build:prod',
|
|
23
|
-
'build-prod',
|
|
24
|
-
'build:dev',
|
|
25
|
-
'build-dev',
|
|
26
|
-
'build:assets',
|
|
27
|
-
'compile',
|
|
28
|
-
'dist',
|
|
29
|
-
],
|
|
14
|
+
build: ['docker:build', 'build:prod', 'build-prod', 'build:dev', 'build-dev', 'build:assets', 'compile', 'dist'],
|
|
30
15
|
test: [
|
|
31
16
|
'phpunit',
|
|
32
17
|
'pest',
|
|
@@ -143,12 +128,7 @@ function renderRunCommand(task: string, stack: string, projectRoot: string): str
|
|
|
143
128
|
return task;
|
|
144
129
|
}
|
|
145
130
|
|
|
146
|
-
function renderRunCommandInSubpackage(
|
|
147
|
-
task: string,
|
|
148
|
-
subStack: string,
|
|
149
|
-
subAbs: string,
|
|
150
|
-
subRel: string,
|
|
151
|
-
): string {
|
|
131
|
+
function renderRunCommandInSubpackage(task: string, subStack: string, subAbs: string, subRel: string): string {
|
|
152
132
|
return `cd ${subRel} && ${renderRunCommand(task, subStack, subAbs)}`;
|
|
153
133
|
}
|
|
154
134
|
|
|
@@ -246,14 +226,10 @@ export function mapVerb(verb: Verb, tasks: Set<string>): string | null {
|
|
|
246
226
|
|
|
247
227
|
function collectTools(ctx: Ctx): Record<string, string> {
|
|
248
228
|
const tools: Record<string, string> = {};
|
|
249
|
-
const
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const pkg = readJson(ctx, 'package.json');
|
|
254
|
-
if (pkg?.engines?.node && typeof pkg.engines.node === 'string') {
|
|
255
|
-
tools.node = stripVersionConstraint(pkg.engines.node);
|
|
256
|
-
}
|
|
229
|
+
const php = at(readJson(ctx, 'composer.json'), 'require', 'php');
|
|
230
|
+
if (typeof php === 'string' && php) tools.php = stripVersionConstraint(php);
|
|
231
|
+
const node = at(readJson(ctx, 'package.json'), 'engines', 'node');
|
|
232
|
+
if (typeof node === 'string' && node) tools.node = stripVersionConstraint(node);
|
|
257
233
|
return tools;
|
|
258
234
|
}
|
|
259
235
|
|
|
@@ -285,16 +261,12 @@ export function renderMiseToml(
|
|
|
285
261
|
primarySubpackage: string | null = null,
|
|
286
262
|
): string {
|
|
287
263
|
const out: string[] = [];
|
|
288
|
-
out.push('# Generated by vdx init. The [vdx] block is metadata for vdx audit.');
|
|
264
|
+
out.push('# Generated by vdx init. The [_.vdx] block is metadata for vdx audit; mise leaves [_] alone.');
|
|
289
265
|
out.push('# Lifecycle verbs follow the vdx contract: up/down/build/test/check/fix.');
|
|
290
266
|
out.push('');
|
|
291
267
|
|
|
292
268
|
if (stack === 'unknown' || stack === 'monorepo') {
|
|
293
|
-
out.push(
|
|
294
|
-
'# TODO(vdx): stack auto-detection вернул "' +
|
|
295
|
-
stack +
|
|
296
|
-
'". Замени на php/node/go/python/meta',
|
|
297
|
-
);
|
|
269
|
+
out.push('# TODO(vdx): stack auto-detection вернул "' + stack + '". Замени на php/node/go/python/meta');
|
|
298
270
|
out.push('# и заполни [tasks.*] блоки руками, либо перегенери: vdx init --stack <id>.');
|
|
299
271
|
out.push('');
|
|
300
272
|
}
|
|
@@ -319,7 +291,7 @@ export function renderMiseToml(
|
|
|
319
291
|
out.push('');
|
|
320
292
|
}
|
|
321
293
|
|
|
322
|
-
out.push('[vdx]');
|
|
294
|
+
out.push('[_.vdx]');
|
|
323
295
|
out.push('schema_version = "0.2"');
|
|
324
296
|
out.push(`baseline = ${quoteToml(baseline)}`);
|
|
325
297
|
out.push(`stack = ${quoteToml(stack)}`);
|
|
@@ -336,10 +308,7 @@ export function renderMiseToml(
|
|
|
336
308
|
return out.join('\n');
|
|
337
309
|
}
|
|
338
310
|
|
|
339
|
-
export function planInit(
|
|
340
|
-
projectRoot: string,
|
|
341
|
-
opts: { baseline?: string; stack?: string } = {},
|
|
342
|
-
): InitPlan {
|
|
311
|
+
export function planInit(projectRoot: string, opts: { baseline?: string; stack?: string } = {}): InitPlan {
|
|
343
312
|
const detectedStack = autoDetectStack(projectRoot);
|
|
344
313
|
const stackOverridden = opts.stack !== undefined && opts.stack !== detectedStack;
|
|
345
314
|
const stack = opts.stack ?? detectedStack;
|
|
@@ -365,7 +334,7 @@ export function planInit(
|
|
|
365
334
|
const list = subs.map((s) => `${s.relPath}(${s.stack})`).join(', ');
|
|
366
335
|
warnings.push(
|
|
367
336
|
`stack="meta", найдено несколько subpackages: ${list}. ` +
|
|
368
|
-
'primary_subpackage не выбран; укажи его руками в [vdx]
|
|
337
|
+
'primary_subpackage не выбран; укажи его руками в блоке [_.vdx].',
|
|
369
338
|
);
|
|
370
339
|
}
|
|
371
340
|
} else if (!stackOverridden && (detectedStack === 'unknown' || detectedStack === 'monorepo')) {
|
|
@@ -436,9 +405,7 @@ export function planInit(
|
|
|
436
405
|
|
|
437
406
|
export function writeInit(plan: InitPlan, opts: { force: boolean }): void {
|
|
438
407
|
if (fs.existsSync(plan.miseTomlPath) && !opts.force) {
|
|
439
|
-
throw new Error(
|
|
440
|
-
`mise.toml уже существует: ${plan.miseTomlPath}. Используй --force для перезаписи.`,
|
|
441
|
-
);
|
|
408
|
+
throw new Error(`mise.toml уже существует: ${plan.miseTomlPath}. Используй --force для перезаписи.`);
|
|
442
409
|
}
|
|
443
410
|
fs.writeFileSync(plan.miseTomlPath, plan.miseTomlContent, 'utf8');
|
|
444
411
|
}
|
|
@@ -451,7 +418,9 @@ export function renderPlanSummary(plan: InitPlan, opts: { verbose?: boolean } =
|
|
|
451
418
|
|
|
452
419
|
if (!verbose) {
|
|
453
420
|
const lines: string[] = [];
|
|
454
|
-
lines.push(
|
|
421
|
+
lines.push(
|
|
422
|
+
`vdx init: stack=${plan.stack}${plan.stackOverridden ? ' (via --stack)' : ''}, ${mapped}/${total} verbs mapped`,
|
|
423
|
+
);
|
|
455
424
|
if (plan.primarySubpackage) lines.push(` primary subpackage: ${plan.primarySubpackage}`);
|
|
456
425
|
if (unmatched.length > 0) {
|
|
457
426
|
lines.push(` not mapped: ${unmatched.join(', ')} (add [tasks.X] in mise.toml manually)`);
|
|
@@ -462,12 +431,8 @@ export function renderPlanSummary(plan: InitPlan, opts: { verbose?: boolean } =
|
|
|
462
431
|
const lines: string[] = [];
|
|
463
432
|
lines.push(`# vdx init plan`);
|
|
464
433
|
lines.push('');
|
|
465
|
-
lines.push(
|
|
466
|
-
|
|
467
|
-
);
|
|
468
|
-
lines.push(
|
|
469
|
-
`- **Stack**: ${plan.stack}${plan.stackOverridden ? ' (via --stack)' : ''}`,
|
|
470
|
-
);
|
|
434
|
+
lines.push(`- **Project**: ${plan.projectRoot}`);
|
|
435
|
+
lines.push(`- **Stack**: ${plan.stack}${plan.stackOverridden ? ' (via --stack)' : ''}`);
|
|
471
436
|
if (plan.primarySubpackage) {
|
|
472
437
|
lines.push(`- **Primary subpackage**: ${plan.primarySubpackage}`);
|
|
473
438
|
}
|
|
@@ -492,9 +457,7 @@ export function renderPlanSummary(plan: InitPlan, opts: { verbose?: boolean } =
|
|
|
492
457
|
lines.push('|------|-------------|--------|-----------------|');
|
|
493
458
|
for (const m of plan.mappings) {
|
|
494
459
|
if (m.runCommand) {
|
|
495
|
-
lines.push(
|
|
496
|
-
`| \`${m.verb}\` | \`${m.nativeTask}\` | ${m.reason} | \`${m.runCommand}\` |`,
|
|
497
|
-
);
|
|
460
|
+
lines.push(`| \`${m.verb}\` | \`${m.nativeTask}\` | ${m.reason} | \`${m.runCommand}\` |`);
|
|
498
461
|
} else {
|
|
499
462
|
lines.push(`| \`${m.verb}\` | _(not found)_ | not-found | _(skipped)_ |`);
|
|
500
463
|
}
|
|
@@ -506,9 +469,7 @@ export function renderPlanSummary(plan: InitPlan, opts: { verbose?: boolean } =
|
|
|
506
469
|
lines.push('');
|
|
507
470
|
for (const m of withAlts) {
|
|
508
471
|
const alts = m.alternatives.map((a) => `\`${a}\``).join(', ');
|
|
509
|
-
lines.push(
|
|
510
|
-
`- \`${m.verb}\` chose \`${m.nativeTask}\` (${m.reason}); also matched: ${alts}`,
|
|
511
|
-
);
|
|
472
|
+
lines.push(`- \`${m.verb}\` chose \`${m.nativeTask}\` (${m.reason}); also matched: ${alts}`);
|
|
512
473
|
}
|
|
513
474
|
lines.push('');
|
|
514
475
|
}
|
package/src/manifest.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import * as TOML from 'smol-toml';
|
|
4
4
|
import YAML from 'js-yaml';
|
|
5
|
+
import { at, dict, type Dict } from './narrow.ts';
|
|
5
6
|
|
|
6
7
|
export interface VdxManifest {
|
|
7
8
|
schema_version?: string;
|
|
@@ -29,18 +30,38 @@ export interface Override {
|
|
|
29
30
|
until?: string;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Where the manifest lives in mise.toml. `[_.vdx]` — mise documents the `[_]` table as never
|
|
35
|
+
* read, for other tools' data. `[vdx]` — where it lived before vdx 0.26; still read, but mise
|
|
36
|
+
* 2026.10 warns about an unknown field on every run.
|
|
37
|
+
*/
|
|
38
|
+
export type ManifestPlace = '_.vdx' | 'vdx';
|
|
39
|
+
|
|
40
|
+
/** The manifest table of a parsed mise.toml and its place: `[_.vdx]`, else the older `[vdx]`. */
|
|
41
|
+
export function manifestTable(data: unknown): { table: Dict; place: ManifestPlace } | null {
|
|
42
|
+
const table = dict(at(data, '_', 'vdx'));
|
|
43
|
+
if (table) return { table, place: '_.vdx' };
|
|
44
|
+
const older = dict(at(data, 'vdx'));
|
|
45
|
+
return older ? { table: older, place: 'vdx' } : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The project's manifest and where in its mise.toml it was found. */
|
|
49
|
+
export function loadManifestAt(projectRoot: string): { manifest: VdxManifest; place: ManifestPlace } | null {
|
|
33
50
|
const miseToml = path.join(projectRoot, 'mise.toml');
|
|
34
51
|
if (!fs.existsSync(miseToml)) return null;
|
|
35
52
|
try {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
return
|
|
53
|
+
const found = manifestTable(TOML.parse(fs.readFileSync(miseToml, 'utf8')));
|
|
54
|
+
// The fields are the project's to write; readers check what they use.
|
|
55
|
+
return found ? { manifest: found.table, place: found.place } : null;
|
|
39
56
|
} catch {
|
|
40
57
|
return null;
|
|
41
58
|
}
|
|
42
59
|
}
|
|
43
60
|
|
|
61
|
+
export function loadManifest(projectRoot: string): VdxManifest | null {
|
|
62
|
+
return loadManifestAt(projectRoot)?.manifest ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
44
65
|
export function loadOverrides(projectRoot: string): Override[] {
|
|
45
66
|
const file = path.join(projectRoot, '.vdx-overrides.yml');
|
|
46
67
|
if (!fs.existsSync(file)) return [];
|
package/src/mcp-server.ts
CHANGED
|
@@ -8,12 +8,13 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
8
8
|
import * as TOML from 'smol-toml';
|
|
9
9
|
|
|
10
10
|
import { loadRubric } from './rubric.ts';
|
|
11
|
-
import { loadManifest, loadOverrides } from './manifest.ts';
|
|
11
|
+
import { loadManifest, loadOverrides, manifestTable } from './manifest.ts';
|
|
12
12
|
import { autoDetectStack, readToml, type Ctx } from './facts.ts';
|
|
13
13
|
import { audit } from './audit.ts';
|
|
14
14
|
import { reportJson, reportMarkdown } from './report.ts';
|
|
15
15
|
import { DEFAULT_BASELINE, STANDARD_VERBS, type Verb } from './init.ts';
|
|
16
16
|
import { resolveDefaultRubric } from './defaults.ts';
|
|
17
|
+
import { at, dict, errorMessage, isStringList, str, type Dict } from './narrow.ts';
|
|
17
18
|
|
|
18
19
|
const DEFAULT_RUBRIC = resolveDefaultRubric();
|
|
19
20
|
|
|
@@ -30,18 +31,16 @@ function freshCtx(): Ctx {
|
|
|
30
31
|
return { projectRoot: PROJECT_ROOT, stack, cache: new Map() };
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
function readMiseToml():
|
|
34
|
+
function readMiseToml(): unknown {
|
|
34
35
|
return readToml(freshCtx(), 'mise.toml');
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
function getVdxBlock():
|
|
38
|
-
|
|
39
|
-
return t?.vdx ?? null;
|
|
38
|
+
function getVdxBlock(): Dict | null {
|
|
39
|
+
return manifestTable(readMiseToml())?.table ?? null;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function getTasks():
|
|
43
|
-
|
|
44
|
-
return (t?.tasks as Record<string, any>) ?? {};
|
|
42
|
+
function getTasks(): Dict {
|
|
43
|
+
return dict(at(readMiseToml(), 'tasks')) ?? {};
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
interface RunResult {
|
|
@@ -61,10 +60,10 @@ function spawnVerb(verb: Verb, extraArgs: string[]): Promise<RunResult> {
|
|
|
61
60
|
});
|
|
62
61
|
let stdout = '';
|
|
63
62
|
let stderr = '';
|
|
64
|
-
child.stdout.on('data', (b) => {
|
|
63
|
+
child.stdout.on('data', (b: Buffer) => {
|
|
65
64
|
stdout += b.toString();
|
|
66
65
|
});
|
|
67
|
-
child.stderr.on('data', (b) => {
|
|
66
|
+
child.stderr.on('data', (b: Buffer) => {
|
|
68
67
|
stderr += b.toString();
|
|
69
68
|
});
|
|
70
69
|
child.on('error', (e) => {
|
|
@@ -91,33 +90,33 @@ function spawnVerb(verb: Verb, extraArgs: string[]): Promise<RunResult> {
|
|
|
91
90
|
function buildCapabilities() {
|
|
92
91
|
const vdxBlock = getVdxBlock();
|
|
93
92
|
const tasks = getTasks();
|
|
94
|
-
const
|
|
93
|
+
const declared = vdxBlock?.['verbs'];
|
|
94
|
+
const verbsList = isStringList(declared) ? declared : STANDARD_VERBS.slice();
|
|
95
95
|
const verbs = verbsList.map((name) => {
|
|
96
96
|
const t = tasks[name];
|
|
97
|
-
const nativeCommand =
|
|
98
|
-
t && typeof t === 'object' && typeof t.run === 'string' ? t.run : null;
|
|
99
97
|
return {
|
|
100
98
|
name,
|
|
101
99
|
has_manifest: Boolean(t),
|
|
102
|
-
native_command:
|
|
100
|
+
native_command: str(at(t, 'run')) ?? null,
|
|
103
101
|
};
|
|
104
102
|
});
|
|
105
103
|
|
|
106
|
-
const
|
|
104
|
+
const infra = dict(vdxBlock?.['shared_infra']);
|
|
105
|
+
const sharedInfra = infra
|
|
107
106
|
? {
|
|
108
|
-
provider:
|
|
109
|
-
precheck: Boolean(
|
|
107
|
+
provider: str(infra['provider']) ?? '',
|
|
108
|
+
precheck: Boolean(infra['precheck']),
|
|
110
109
|
is_running: false,
|
|
111
110
|
}
|
|
112
111
|
: null;
|
|
113
112
|
|
|
114
113
|
return {
|
|
115
|
-
schema_version:
|
|
116
|
-
stack:
|
|
117
|
-
baseline:
|
|
114
|
+
schema_version: str(vdxBlock?.['schema_version']) ?? '0.2',
|
|
115
|
+
stack: str(vdxBlock?.['stack']) ?? autoDetectStack(PROJECT_ROOT),
|
|
116
|
+
baseline: str(vdxBlock?.['baseline']) ?? `file://${DEFAULT_RUBRIC}`,
|
|
118
117
|
verbs,
|
|
119
118
|
shared_infra: sharedInfra,
|
|
120
|
-
last_verified: vdxBlock
|
|
119
|
+
last_verified: at(vdxBlock, 'success_path', 'last_verified') ?? null,
|
|
121
120
|
};
|
|
122
121
|
}
|
|
123
122
|
|
|
@@ -139,40 +138,44 @@ function recordSuccessPath(input: {
|
|
|
139
138
|
notes?: string;
|
|
140
139
|
}): { ok: boolean; manifest_path: string; agents_md_updated: boolean } {
|
|
141
140
|
const manifestPath = path.join(PROJECT_ROOT, 'mise.toml');
|
|
142
|
-
let doc:
|
|
141
|
+
let doc: Dict = {};
|
|
143
142
|
if (fs.existsSync(manifestPath)) {
|
|
144
143
|
const text = fs.readFileSync(manifestPath, 'utf8');
|
|
145
144
|
try {
|
|
146
145
|
doc = TOML.parse(text);
|
|
147
|
-
} catch (e
|
|
148
|
-
throw new Error(`mise.toml parse error: ${e
|
|
146
|
+
} catch (e) {
|
|
147
|
+
throw new Error(`mise.toml parse error: ${errorMessage(e)}`, { cause: e });
|
|
149
148
|
}
|
|
150
149
|
}
|
|
151
150
|
|
|
152
|
-
|
|
151
|
+
const tasks = dict(doc['tasks']) ?? {};
|
|
153
152
|
for (const v of input.verbs) {
|
|
154
|
-
|
|
153
|
+
tasks[v.name] = { run: v.command };
|
|
155
154
|
}
|
|
155
|
+
doc['tasks'] = tasks;
|
|
156
156
|
|
|
157
|
-
|
|
157
|
+
// An existing manifest is updated where it is; a new one goes into [_.vdx].
|
|
158
|
+
const found = manifestTable(doc);
|
|
159
|
+
const vdx = found?.table ?? {
|
|
158
160
|
schema_version: '0.2',
|
|
159
161
|
baseline: DEFAULT_BASELINE,
|
|
160
162
|
stack: autoDetectStack(PROJECT_ROOT),
|
|
161
163
|
};
|
|
162
|
-
|
|
164
|
+
vdx['verbs'] = input.verbs.map((v) => v.name);
|
|
163
165
|
|
|
164
166
|
if (input.shared_infra) {
|
|
165
|
-
|
|
167
|
+
vdx['shared_infra'] = {
|
|
166
168
|
provider: input.shared_infra.provider,
|
|
167
169
|
precheck: input.shared_infra.precheck,
|
|
168
170
|
};
|
|
169
171
|
}
|
|
170
172
|
|
|
171
|
-
|
|
173
|
+
vdx['success_path'] = {
|
|
172
174
|
last_verified: new Date().toISOString().slice(0, 10),
|
|
173
175
|
verified_by: 'vdx-mcp',
|
|
174
176
|
...(input.notes ? { notes: input.notes } : {}),
|
|
175
177
|
};
|
|
178
|
+
if (!found) doc['_'] = { ...dict(doc['_']), vdx };
|
|
176
179
|
|
|
177
180
|
const newText = TOML.stringify(doc);
|
|
178
181
|
fs.writeFileSync(manifestPath, newText, 'utf8');
|
|
@@ -192,7 +195,7 @@ server.registerTool(
|
|
|
192
195
|
'Возвращает manifest проекта: stack, baseline, доступные глаголы и их native команды, shared_infra и last_verified. Самый частый вызов агента, входящего в проект.',
|
|
193
196
|
inputSchema: {},
|
|
194
197
|
},
|
|
195
|
-
|
|
198
|
+
() => {
|
|
196
199
|
const payload = buildCapabilities();
|
|
197
200
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
198
201
|
},
|
|
@@ -222,7 +225,7 @@ server.registerTool(
|
|
|
222
225
|
format: z.enum(['json', 'markdown']).optional(),
|
|
223
226
|
},
|
|
224
227
|
},
|
|
225
|
-
|
|
228
|
+
({ baseline, format }) => {
|
|
226
229
|
const fmt = format ?? 'json';
|
|
227
230
|
const payload = buildAuditPayload(baseline, fmt);
|
|
228
231
|
return { content: [{ type: 'text', text: payload }] };
|
|
@@ -233,16 +236,14 @@ server.registerTool(
|
|
|
233
236
|
'vdx_record_success_path',
|
|
234
237
|
{
|
|
235
238
|
description:
|
|
236
|
-
'Записывает обнаруженные команды в [tasks.X] секцию mise.toml + обновляет [vdx.success_path]. Используется после успешной верификации команд агентом.',
|
|
239
|
+
'Записывает обнаруженные команды в [tasks.X] секцию mise.toml + обновляет [_.vdx.success_path] (у манифеста в старом [vdx] — там). Используется после успешной верификации команд агентом.',
|
|
237
240
|
inputSchema: {
|
|
238
241
|
verbs: z.array(z.object({ name: z.string(), command: z.string() })),
|
|
239
|
-
shared_infra: z
|
|
240
|
-
.object({ provider: z.string(), precheck: z.boolean() })
|
|
241
|
-
.optional(),
|
|
242
|
+
shared_infra: z.object({ provider: z.string(), precheck: z.boolean() }).optional(),
|
|
242
243
|
notes: z.string().optional(),
|
|
243
244
|
},
|
|
244
245
|
},
|
|
245
|
-
|
|
246
|
+
({ verbs, shared_infra, notes }) => {
|
|
246
247
|
const args: Parameters<typeof recordSuccessPath>[0] = { verbs };
|
|
247
248
|
if (shared_infra) args.shared_infra = shared_infra;
|
|
248
249
|
if (notes !== undefined) args.notes = notes;
|
|
@@ -257,7 +258,7 @@ async function main() {
|
|
|
257
258
|
process.stderr.write(`vdx-mcp connected (project: ${PROJECT_ROOT})\n`);
|
|
258
259
|
}
|
|
259
260
|
|
|
260
|
-
main().catch((e) => {
|
|
261
|
-
process.stderr.write(`vdx-mcp fatal: ${e
|
|
261
|
+
main().catch((e: unknown) => {
|
|
262
|
+
process.stderr.write(`vdx-mcp fatal: ${errorMessage(e)}\n`);
|
|
262
263
|
process.exit(1);
|
|
263
264
|
});
|