@vodmal/vdx-cli 0.25.0 → 0.27.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 +2 -2
- package/package.json +16 -6
- package/rubric/vdx-rubric.yaml +45 -9
- package/src/ai.ts +48 -49
- package/src/author.ts +1 -1
- package/src/clone-checks.ts +1 -1
- package/src/conversations.ts +33 -26
- package/src/doctor.ts +10 -9
- package/src/evaluator.ts +22 -21
- package/src/facts.ts +24 -49
- package/src/gha.ts +36 -28
- package/src/hooks.ts +34 -34
- package/src/index.ts +22 -13
- package/src/init.ts +12 -20
- package/src/manifest.ts +25 -4
- package/src/mcp-server.ts +39 -35
- package/src/narrow.ts +56 -0
- package/src/personal-hooks.ts +2 -8
- package/src/predicates.ts +10 -11
- package/src/publish.ts +28 -26
- package/src/rubric.ts +6 -6
- package/src/run.ts +3 -2
- package/src/vocabulary.ts +10 -14
package/src/index.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
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 { at, errorMessage } from './narrow.ts';
|
|
7
8
|
import { LIFECYCLE_VERBS, type LifecycleVerb, resolveLifecycleVerb, renderResolveError } from './run.ts';
|
|
8
9
|
import { audit } from './audit.ts';
|
|
9
10
|
import {
|
|
@@ -285,7 +286,14 @@ function cmdAudit(opts: ParsedArgs): void {
|
|
|
285
286
|
);
|
|
286
287
|
}
|
|
287
288
|
|
|
288
|
-
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
|
+
}
|
|
289
297
|
const overrides = loadOverrides(projectRoot);
|
|
290
298
|
|
|
291
299
|
const rubricPath = (opts.flags.rubric as string) || DEFAULT_RUBRIC;
|
|
@@ -346,8 +354,8 @@ function cmdInit(opts: ParsedArgs): void {
|
|
|
346
354
|
try {
|
|
347
355
|
writeInit(plan, { force: Boolean(opts.flags.force) });
|
|
348
356
|
process.stderr.write(`\n✓ wrote ${plan.miseTomlPath}\n`);
|
|
349
|
-
} catch (e
|
|
350
|
-
process.stderr.write(`\nerror: ${
|
|
357
|
+
} catch (e) {
|
|
358
|
+
process.stderr.write(`\nerror: ${errorMessage(e)}\n`);
|
|
351
359
|
process.exit(2);
|
|
352
360
|
}
|
|
353
361
|
}
|
|
@@ -380,8 +388,8 @@ function cmdPublish(opts: ParsedArgs): void {
|
|
|
380
388
|
const replan = () => {
|
|
381
389
|
try {
|
|
382
390
|
return planPublish(planOpts, auditResult, ctx);
|
|
383
|
-
} catch (e
|
|
384
|
-
process.stderr.write(`error: ${
|
|
391
|
+
} catch (e) {
|
|
392
|
+
process.stderr.write(`error: ${errorMessage(e)}\n`);
|
|
385
393
|
process.exit(2);
|
|
386
394
|
}
|
|
387
395
|
};
|
|
@@ -418,8 +426,8 @@ function cmdPublish(opts: ParsedArgs): void {
|
|
|
418
426
|
|
|
419
427
|
try {
|
|
420
428
|
executePublish(plan, deps);
|
|
421
|
-
} catch (e
|
|
422
|
-
process.stderr.write(`\nerror: ${
|
|
429
|
+
} catch (e) {
|
|
430
|
+
process.stderr.write(`\nerror: ${errorMessage(e)}\n`);
|
|
423
431
|
process.exit(3);
|
|
424
432
|
}
|
|
425
433
|
}
|
|
@@ -434,14 +442,15 @@ function cmdRun(verb: LifecycleVerb): void {
|
|
|
434
442
|
|
|
435
443
|
try {
|
|
436
444
|
execFileSync('mise', ['run', verb], { cwd: projectRoot, stdio: 'inherit' });
|
|
437
|
-
} catch (e
|
|
438
|
-
if (e
|
|
445
|
+
} catch (e) {
|
|
446
|
+
if (at(e, 'code') === 'ENOENT') {
|
|
439
447
|
process.stderr.write(
|
|
440
448
|
`vdx: \`mise\` binary not found on PATH\n` + `hint: install mise — https://mise.jdx.dev/getting-started.html\n`,
|
|
441
449
|
);
|
|
442
450
|
process.exit(127);
|
|
443
451
|
}
|
|
444
|
-
|
|
452
|
+
const status = at(e, 'status');
|
|
453
|
+
process.exit(typeof status === 'number' ? status : 1);
|
|
445
454
|
}
|
|
446
455
|
}
|
|
447
456
|
|
|
@@ -462,8 +471,8 @@ function cmdDoctor(opts: ParsedArgs): void {
|
|
|
462
471
|
process.stderr.write(
|
|
463
472
|
done.length > 0 ? done.map((d) => `✓ ${d}`).join('\n') + '\n' : 'nothing to fix in ~/.gitconfig\n',
|
|
464
473
|
);
|
|
465
|
-
} catch (e
|
|
466
|
-
process.stderr.write(`vdx doctor --fix: ${
|
|
474
|
+
} catch (e) {
|
|
475
|
+
process.stderr.write(`vdx doctor --fix: ${errorMessage(e)}\n`);
|
|
467
476
|
process.exit(2);
|
|
468
477
|
}
|
|
469
478
|
}
|
package/src/init.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { Ctx, readJson, listAllTasks, autoDetectStack, findSubPackages, type SubPackage } from './facts.ts';
|
|
4
|
+
import { at } from './narrow.ts';
|
|
4
5
|
|
|
5
6
|
export const STANDARD_VERBS = ['up', 'down', 'build', 'test', 'check', 'fix'] as const;
|
|
6
7
|
export type Verb = (typeof STANDARD_VERBS)[number];
|
|
7
8
|
|
|
8
|
-
export const DEFAULT_BASELINE = 'github.com/VoDmAl/vdx-rubric-vodmal@v1.4.
|
|
9
|
+
export const DEFAULT_BASELINE = 'github.com/VoDmAl/vdx-rubric-vodmal@v1.4.2';
|
|
9
10
|
|
|
10
11
|
const VERB_ALIASES: Record<Verb, string[]> = {
|
|
11
12
|
up: ['docker:up', 'docker-up', 'docker:up:detached', 'start', 'dev', 'serve'],
|
|
@@ -77,9 +78,9 @@ const VERB_SUFFIX_GROUPS: Record<Verb, string[]> = {
|
|
|
77
78
|
fix: [':fix', '-fix', ':format'],
|
|
78
79
|
};
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
type MatchReason = 'exact' | 'alias' | 'prefix-group' | 'suffix-group' | 'not-found';
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
interface VerbMapping {
|
|
83
84
|
verb: Verb;
|
|
84
85
|
nativeTask: string | null;
|
|
85
86
|
runCommand: string | null;
|
|
@@ -218,21 +219,12 @@ export function selectVerbTask(verb: Verb, tasks: Set<string>): VerbMatch {
|
|
|
218
219
|
return { task: null, reason: 'not-found', alternatives: [] };
|
|
219
220
|
}
|
|
220
221
|
|
|
221
|
-
/** Kept for back-compat with potential external callers; prefer `selectVerbTask`. */
|
|
222
|
-
export function mapVerb(verb: Verb, tasks: Set<string>): string | null {
|
|
223
|
-
return selectVerbTask(verb, tasks).task;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
222
|
function collectTools(ctx: Ctx): Record<string, string> {
|
|
227
223
|
const tools: Record<string, string> = {};
|
|
228
|
-
const
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const pkg = readJson(ctx, 'package.json');
|
|
233
|
-
if (pkg?.engines?.node && typeof pkg.engines.node === 'string') {
|
|
234
|
-
tools.node = stripVersionConstraint(pkg.engines.node);
|
|
235
|
-
}
|
|
224
|
+
const php = at(readJson(ctx, 'composer.json'), 'require', 'php');
|
|
225
|
+
if (typeof php === 'string' && php) tools.php = stripVersionConstraint(php);
|
|
226
|
+
const node = at(readJson(ctx, 'package.json'), 'engines', 'node');
|
|
227
|
+
if (typeof node === 'string' && node) tools.node = stripVersionConstraint(node);
|
|
236
228
|
return tools;
|
|
237
229
|
}
|
|
238
230
|
|
|
@@ -256,7 +248,7 @@ function quoteToml(s: string): string {
|
|
|
256
248
|
return '"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
257
249
|
}
|
|
258
250
|
|
|
259
|
-
|
|
251
|
+
function renderMiseToml(
|
|
260
252
|
tools: Record<string, string>,
|
|
261
253
|
mappings: VerbMapping[],
|
|
262
254
|
stack: string,
|
|
@@ -264,7 +256,7 @@ export function renderMiseToml(
|
|
|
264
256
|
primarySubpackage: string | null = null,
|
|
265
257
|
): string {
|
|
266
258
|
const out: string[] = [];
|
|
267
|
-
out.push('# Generated by vdx init. The [vdx] block is metadata for vdx audit.');
|
|
259
|
+
out.push('# Generated by vdx init. The [_.vdx] block is metadata for vdx audit; mise leaves [_] alone.');
|
|
268
260
|
out.push('# Lifecycle verbs follow the vdx contract: up/down/build/test/check/fix.');
|
|
269
261
|
out.push('');
|
|
270
262
|
|
|
@@ -294,7 +286,7 @@ export function renderMiseToml(
|
|
|
294
286
|
out.push('');
|
|
295
287
|
}
|
|
296
288
|
|
|
297
|
-
out.push('[vdx]');
|
|
289
|
+
out.push('[_.vdx]');
|
|
298
290
|
out.push('schema_version = "0.2"');
|
|
299
291
|
out.push(`baseline = ${quoteToml(baseline)}`);
|
|
300
292
|
out.push(`stack = ${quoteToml(stack)}`);
|
|
@@ -337,7 +329,7 @@ export function planInit(projectRoot: string, opts: { baseline?: string; stack?:
|
|
|
337
329
|
const list = subs.map((s) => `${s.relPath}(${s.stack})`).join(', ');
|
|
338
330
|
warnings.push(
|
|
339
331
|
`stack="meta", найдено несколько subpackages: ${list}. ` +
|
|
340
|
-
'primary_subpackage не выбран; укажи его руками в [vdx]
|
|
332
|
+
'primary_subpackage не выбран; укажи его руками в блоке [_.vdx].',
|
|
341
333
|
);
|
|
342
334
|
}
|
|
343
335
|
} else if (!stackOverridden && (detectedStack === 'unknown' || detectedStack === 'monorepo')) {
|
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,32 +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 = t && typeof t === 'object' && typeof t.run === 'string' ? t.run : null;
|
|
98
97
|
return {
|
|
99
98
|
name,
|
|
100
99
|
has_manifest: Boolean(t),
|
|
101
|
-
native_command:
|
|
100
|
+
native_command: str(at(t, 'run')) ?? null,
|
|
102
101
|
};
|
|
103
102
|
});
|
|
104
103
|
|
|
105
|
-
const
|
|
104
|
+
const infra = dict(vdxBlock?.['shared_infra']);
|
|
105
|
+
const sharedInfra = infra
|
|
106
106
|
? {
|
|
107
|
-
provider:
|
|
108
|
-
precheck: Boolean(
|
|
107
|
+
provider: str(infra['provider']) ?? '',
|
|
108
|
+
precheck: Boolean(infra['precheck']),
|
|
109
109
|
is_running: false,
|
|
110
110
|
}
|
|
111
111
|
: null;
|
|
112
112
|
|
|
113
113
|
return {
|
|
114
|
-
schema_version:
|
|
115
|
-
stack:
|
|
116
|
-
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}`,
|
|
117
117
|
verbs,
|
|
118
118
|
shared_infra: sharedInfra,
|
|
119
|
-
last_verified: vdxBlock
|
|
119
|
+
last_verified: at(vdxBlock, 'success_path', 'last_verified') ?? null,
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
|
|
@@ -138,40 +138,44 @@ function recordSuccessPath(input: {
|
|
|
138
138
|
notes?: string;
|
|
139
139
|
}): { ok: boolean; manifest_path: string; agents_md_updated: boolean } {
|
|
140
140
|
const manifestPath = path.join(PROJECT_ROOT, 'mise.toml');
|
|
141
|
-
let doc:
|
|
141
|
+
let doc: Dict = {};
|
|
142
142
|
if (fs.existsSync(manifestPath)) {
|
|
143
143
|
const text = fs.readFileSync(manifestPath, 'utf8');
|
|
144
144
|
try {
|
|
145
145
|
doc = TOML.parse(text);
|
|
146
|
-
} catch (e
|
|
147
|
-
throw new Error(`mise.toml parse error: ${e
|
|
146
|
+
} catch (e) {
|
|
147
|
+
throw new Error(`mise.toml parse error: ${errorMessage(e)}`, { cause: e });
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
const tasks = dict(doc['tasks']) ?? {};
|
|
152
152
|
for (const v of input.verbs) {
|
|
153
|
-
|
|
153
|
+
tasks[v.name] = { run: v.command };
|
|
154
154
|
}
|
|
155
|
+
doc['tasks'] = tasks;
|
|
155
156
|
|
|
156
|
-
|
|
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 ?? {
|
|
157
160
|
schema_version: '0.2',
|
|
158
161
|
baseline: DEFAULT_BASELINE,
|
|
159
162
|
stack: autoDetectStack(PROJECT_ROOT),
|
|
160
163
|
};
|
|
161
|
-
|
|
164
|
+
vdx['verbs'] = input.verbs.map((v) => v.name);
|
|
162
165
|
|
|
163
166
|
if (input.shared_infra) {
|
|
164
|
-
|
|
167
|
+
vdx['shared_infra'] = {
|
|
165
168
|
provider: input.shared_infra.provider,
|
|
166
169
|
precheck: input.shared_infra.precheck,
|
|
167
170
|
};
|
|
168
171
|
}
|
|
169
172
|
|
|
170
|
-
|
|
173
|
+
vdx['success_path'] = {
|
|
171
174
|
last_verified: new Date().toISOString().slice(0, 10),
|
|
172
175
|
verified_by: 'vdx-mcp',
|
|
173
176
|
...(input.notes ? { notes: input.notes } : {}),
|
|
174
177
|
};
|
|
178
|
+
if (!found) doc['_'] = { ...dict(doc['_']), vdx };
|
|
175
179
|
|
|
176
180
|
const newText = TOML.stringify(doc);
|
|
177
181
|
fs.writeFileSync(manifestPath, newText, 'utf8');
|
|
@@ -191,7 +195,7 @@ server.registerTool(
|
|
|
191
195
|
'Возвращает manifest проекта: stack, baseline, доступные глаголы и их native команды, shared_infra и last_verified. Самый частый вызов агента, входящего в проект.',
|
|
192
196
|
inputSchema: {},
|
|
193
197
|
},
|
|
194
|
-
|
|
198
|
+
() => {
|
|
195
199
|
const payload = buildCapabilities();
|
|
196
200
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
197
201
|
},
|
|
@@ -221,7 +225,7 @@ server.registerTool(
|
|
|
221
225
|
format: z.enum(['json', 'markdown']).optional(),
|
|
222
226
|
},
|
|
223
227
|
},
|
|
224
|
-
|
|
228
|
+
({ baseline, format }) => {
|
|
225
229
|
const fmt = format ?? 'json';
|
|
226
230
|
const payload = buildAuditPayload(baseline, fmt);
|
|
227
231
|
return { content: [{ type: 'text', text: payload }] };
|
|
@@ -232,14 +236,14 @@ server.registerTool(
|
|
|
232
236
|
'vdx_record_success_path',
|
|
233
237
|
{
|
|
234
238
|
description:
|
|
235
|
-
'Записывает обнаруженные команды в [tasks.X] секцию mise.toml + обновляет [vdx.success_path]. Используется после успешной верификации команд агентом.',
|
|
239
|
+
'Записывает обнаруженные команды в [tasks.X] секцию mise.toml + обновляет [_.vdx.success_path] (у манифеста в старом [vdx] — там). Используется после успешной верификации команд агентом.',
|
|
236
240
|
inputSchema: {
|
|
237
241
|
verbs: z.array(z.object({ name: z.string(), command: z.string() })),
|
|
238
242
|
shared_infra: z.object({ provider: z.string(), precheck: z.boolean() }).optional(),
|
|
239
243
|
notes: z.string().optional(),
|
|
240
244
|
},
|
|
241
245
|
},
|
|
242
|
-
|
|
246
|
+
({ verbs, shared_infra, notes }) => {
|
|
243
247
|
const args: Parameters<typeof recordSuccessPath>[0] = { verbs };
|
|
244
248
|
if (shared_infra) args.shared_infra = shared_infra;
|
|
245
249
|
if (notes !== undefined) args.notes = notes;
|
|
@@ -254,7 +258,7 @@ async function main() {
|
|
|
254
258
|
process.stderr.write(`vdx-mcp connected (project: ${PROJECT_ROOT})\n`);
|
|
255
259
|
}
|
|
256
260
|
|
|
257
|
-
main().catch((e) => {
|
|
258
|
-
process.stderr.write(`vdx-mcp fatal: ${e
|
|
261
|
+
main().catch((e: unknown) => {
|
|
262
|
+
process.stderr.write(`vdx-mcp fatal: ${errorMessage(e)}\n`);
|
|
259
263
|
process.exit(1);
|
|
260
264
|
});
|
package/src/narrow.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a parsed file (JSON, YAML, TOML) or a caught error holds is `unknown` until checked.
|
|
3
|
+
* These helpers check it on the way in, so the code past them works with real types.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type Dict = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
/** A plain object — not null, not an array. */
|
|
9
|
+
export function isDict(v: unknown): v is Dict {
|
|
10
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** `v` when it is a plain object, else undefined. */
|
|
14
|
+
export function dict(v: unknown): Dict | undefined {
|
|
15
|
+
return isDict(v) ? v : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The value under `keys`, one level per key — an object's property or an array's index ("0");
|
|
20
|
+
* undefined as soon as a level is not an object.
|
|
21
|
+
*/
|
|
22
|
+
export function at(v: unknown, ...keys: string[]): unknown {
|
|
23
|
+
let cur = v;
|
|
24
|
+
for (const k of keys) {
|
|
25
|
+
if (typeof cur !== 'object' || cur === null) return undefined;
|
|
26
|
+
cur = (cur as Dict)[k];
|
|
27
|
+
}
|
|
28
|
+
return cur;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** `v` when it is a string, else undefined. */
|
|
32
|
+
export function str(v: unknown): string | undefined {
|
|
33
|
+
return typeof v === 'string' ? v : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** `v` when it is an array, else an empty one. */
|
|
37
|
+
export function list(v: unknown): unknown[] {
|
|
38
|
+
return Array.isArray(v) ? (v as unknown[]) : [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isStringList(v: unknown): v is string[] {
|
|
42
|
+
return Array.isArray(v) && v.every((x) => typeof x === 'string');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The message of a caught value, whatever was thrown. */
|
|
46
|
+
export function errorMessage(e: unknown): string {
|
|
47
|
+
return e instanceof Error ? e.message : String(e);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** What a failed child process said: its stderr, else the error's message. */
|
|
51
|
+
export function stderrOf(e: unknown): string {
|
|
52
|
+
const err = at(e, 'stderr');
|
|
53
|
+
if (typeof err === 'string' && err) return err;
|
|
54
|
+
if (Buffer.isBuffer(err) && err.length > 0) return err.toString();
|
|
55
|
+
return errorMessage(e);
|
|
56
|
+
}
|
package/src/personal-hooks.ts
CHANGED
|
@@ -9,14 +9,13 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import * as fs from 'node:fs';
|
|
11
11
|
import * as os from 'node:os';
|
|
12
|
-
import * as path from 'node:path';
|
|
13
12
|
import { execFileSync } from 'node:child_process';
|
|
14
13
|
import type { CheckResult } from './doctor.ts';
|
|
15
14
|
import { loadEnvironment, resolveEnvironmentPath, type PersonalHook } from './ai.ts';
|
|
16
15
|
import { missingMachinePaths } from './hooks.ts';
|
|
17
16
|
|
|
18
17
|
/** Git reads hooks from config since 2.54; older git skips the keys without a word. */
|
|
19
|
-
|
|
18
|
+
const CONFIG_HOOKS_SINCE: [number, number] = [2, 54];
|
|
20
19
|
|
|
21
20
|
export interface PersonalHookDeps {
|
|
22
21
|
env: NodeJS.ProcessEnv;
|
|
@@ -62,7 +61,7 @@ function events(h: PersonalHook): string[] {
|
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
/** The hooks of the profile that apply on this machine; null — no profile, or none declared. */
|
|
65
|
-
|
|
64
|
+
function applicableHooks(deps: PersonalHookDeps): PersonalHook[] | null {
|
|
66
65
|
const file = resolveEnvironmentPath(deps.env, deps.home);
|
|
67
66
|
if (!file) return null;
|
|
68
67
|
let hooks: PersonalHook[];
|
|
@@ -185,8 +184,3 @@ export function fixPersonalHooks(deps: PersonalHookDeps = defaultPersonalHookDep
|
|
|
185
184
|
}
|
|
186
185
|
return done;
|
|
187
186
|
}
|
|
188
|
-
|
|
189
|
-
/** For messages: where the user config lives. */
|
|
190
|
-
export function userGitConfigPath(deps: PersonalHookDeps): string {
|
|
191
|
-
return deps.env['GIT_CONFIG_GLOBAL'] ?? path.join(deps.home, '.gitconfig');
|
|
192
|
-
}
|
package/src/predicates.ts
CHANGED
|
@@ -15,8 +15,9 @@ import {
|
|
|
15
15
|
import { checkingJobs, ciSystems, ciTaskCalls, mainPushSteps, readWorkflows, shippingJobs, upstream } from './gha.ts';
|
|
16
16
|
import { detectHookFrameworks } from './hooks.ts';
|
|
17
17
|
import { covers, expandTasks, isVocabularyTask, projectTaskCalls } from './vocabulary.ts';
|
|
18
|
+
import { at, isStringList, str } from './narrow.ts';
|
|
18
19
|
|
|
19
|
-
type Args = Record<string,
|
|
20
|
+
export type Args = Record<string, unknown>;
|
|
20
21
|
|
|
21
22
|
/** A fact vdx could not read here — a CI system or a hosting it does not inspect. */
|
|
22
23
|
export interface Unknown {
|
|
@@ -42,7 +43,7 @@ const DEFAULT_TEST_PATTERN =
|
|
|
42
43
|
|
|
43
44
|
function testPattern(args: Args): RegExp {
|
|
44
45
|
try {
|
|
45
|
-
return new RegExp(
|
|
46
|
+
return new RegExp(str(args.pattern) ?? DEFAULT_TEST_PATTERN);
|
|
46
47
|
} catch {
|
|
47
48
|
return new RegExp(DEFAULT_TEST_PATTERN);
|
|
48
49
|
}
|
|
@@ -102,7 +103,7 @@ export const REGISTRY: Record<string, PredicateFn> = {
|
|
|
102
103
|
has_file: (args, ctx) => fileExists(ctx, String(args.path)),
|
|
103
104
|
|
|
104
105
|
file_contains: (args, ctx) => {
|
|
105
|
-
const paths
|
|
106
|
+
const paths = isStringList(args.paths) ? args.paths : [String(args.path)];
|
|
106
107
|
const expanded = expandPaths(paths, ctx);
|
|
107
108
|
let re: RegExp;
|
|
108
109
|
try {
|
|
@@ -119,7 +120,7 @@ export const REGISTRY: Record<string, PredicateFn> = {
|
|
|
119
120
|
|
|
120
121
|
package_present: (args, ctx) => {
|
|
121
122
|
if (typeof args === 'string') return isPackagePresent(args, 'any', ctx);
|
|
122
|
-
return isPackagePresent(String(args.name),
|
|
123
|
+
return isPackagePresent(String(args.name), str(args.ecosystem) ?? 'any', ctx);
|
|
123
124
|
},
|
|
124
125
|
|
|
125
126
|
tsc_flag: (args, ctx) => {
|
|
@@ -143,7 +144,7 @@ export const REGISTRY: Record<string, PredicateFn> = {
|
|
|
143
144
|
gh_workflow_blocks_pr: (args, ctx) => {
|
|
144
145
|
const dir = path.join(ctx.projectRoot, '.github', 'workflows');
|
|
145
146
|
if (!fs.existsSync(dir)) return false;
|
|
146
|
-
const wanted =
|
|
147
|
+
const wanted = str(args.check_name) || null;
|
|
147
148
|
try {
|
|
148
149
|
for (const f of fs.readdirSync(dir)) {
|
|
149
150
|
if (!/\.ya?ml$/.test(f)) continue;
|
|
@@ -166,21 +167,19 @@ export const REGISTRY: Record<string, PredicateFn> = {
|
|
|
166
167
|
const text = readText(ctx, f);
|
|
167
168
|
if (text && new RegExp(`^${hook}:`, 'm').test(text)) return true;
|
|
168
169
|
}
|
|
169
|
-
|
|
170
|
-
if (composer?.extra?.hooks?.[hook]) return true;
|
|
171
|
-
return false;
|
|
170
|
+
return Boolean(at(readJson(ctx, 'composer.json'), 'extra', 'hooks', hook));
|
|
172
171
|
},
|
|
173
172
|
|
|
174
173
|
config_value: (args, ctx) => {
|
|
175
|
-
const filePath =
|
|
176
|
-
const jsonpath =
|
|
174
|
+
const filePath = str(args.path) ?? '';
|
|
175
|
+
const jsonpath = str(args.jsonpath) ?? '';
|
|
177
176
|
if (!filePath || !jsonpath) return false;
|
|
178
177
|
const data = readStructured(ctx, filePath);
|
|
179
178
|
if (data === null || data === undefined) return false;
|
|
180
179
|
const value = resolveConfigPath(data, jsonpath);
|
|
181
180
|
|
|
182
181
|
// Operator resolution: explicit `op`, otherwise infer from arg shape.
|
|
183
|
-
let op =
|
|
182
|
+
let op = str(args.op) || null;
|
|
184
183
|
if (!op) {
|
|
185
184
|
if (args.equals !== undefined) op = 'equals';
|
|
186
185
|
else if (args.gte !== undefined) op = 'gte';
|