@vodmal/vdx-cli 0.24.0 → 0.25.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 +3 -2
- package/package.json +5 -1
- package/rubric/vdx-rubric.yaml +32 -24
- package/src/ai.ts +199 -53
- package/src/audit.ts +19 -9
- package/src/author.ts +27 -8
- package/src/clone-checks.ts +13 -8
- package/src/conversations.ts +23 -4
- package/src/doctor.ts +9 -11
- package/src/evaluator.ts +1 -1
- package/src/facts.ts +51 -11
- package/src/gha.ts +4 -1
- package/src/hooks.ts +47 -12
- package/src/index.ts +34 -28
- package/src/init.ts +14 -50
- package/src/mcp-server.ts +2 -5
- package/src/personal-hooks.ts +16 -5
- package/src/predicates.ts +2 -5
- package/src/publish.ts +11 -6
- package/src/report.ts +1 -3
- package/src/run.ts +2 -6
- package/src/scoring.ts +2 -8
- package/src/vocabulary.ts +7 -2
package/src/init.ts
CHANGED
|
@@ -1,32 +1,16 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
Ctx,
|
|
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';
|
|
11
4
|
|
|
12
5
|
export const STANDARD_VERBS = ['up', 'down', 'build', 'test', 'check', 'fix'] as const;
|
|
13
6
|
export type Verb = (typeof STANDARD_VERBS)[number];
|
|
14
7
|
|
|
15
|
-
export const DEFAULT_BASELINE = 'github.com/VoDmAl/vdx-rubric-vodmal@v1.
|
|
8
|
+
export const DEFAULT_BASELINE = 'github.com/VoDmAl/vdx-rubric-vodmal@v1.4.0';
|
|
16
9
|
|
|
17
10
|
const VERB_ALIASES: Record<Verb, string[]> = {
|
|
18
11
|
up: ['docker:up', 'docker-up', 'docker:up:detached', 'start', 'dev', 'serve'],
|
|
19
12
|
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
|
-
],
|
|
13
|
+
build: ['docker:build', 'build:prod', 'build-prod', 'build:dev', 'build-dev', 'build:assets', 'compile', 'dist'],
|
|
30
14
|
test: [
|
|
31
15
|
'phpunit',
|
|
32
16
|
'pest',
|
|
@@ -143,12 +127,7 @@ function renderRunCommand(task: string, stack: string, projectRoot: string): str
|
|
|
143
127
|
return task;
|
|
144
128
|
}
|
|
145
129
|
|
|
146
|
-
function renderRunCommandInSubpackage(
|
|
147
|
-
task: string,
|
|
148
|
-
subStack: string,
|
|
149
|
-
subAbs: string,
|
|
150
|
-
subRel: string,
|
|
151
|
-
): string {
|
|
130
|
+
function renderRunCommandInSubpackage(task: string, subStack: string, subAbs: string, subRel: string): string {
|
|
152
131
|
return `cd ${subRel} && ${renderRunCommand(task, subStack, subAbs)}`;
|
|
153
132
|
}
|
|
154
133
|
|
|
@@ -290,11 +269,7 @@ export function renderMiseToml(
|
|
|
290
269
|
out.push('');
|
|
291
270
|
|
|
292
271
|
if (stack === 'unknown' || stack === 'monorepo') {
|
|
293
|
-
out.push(
|
|
294
|
-
'# TODO(vdx): stack auto-detection вернул "' +
|
|
295
|
-
stack +
|
|
296
|
-
'". Замени на php/node/go/python/meta',
|
|
297
|
-
);
|
|
272
|
+
out.push('# TODO(vdx): stack auto-detection вернул "' + stack + '". Замени на php/node/go/python/meta');
|
|
298
273
|
out.push('# и заполни [tasks.*] блоки руками, либо перегенери: vdx init --stack <id>.');
|
|
299
274
|
out.push('');
|
|
300
275
|
}
|
|
@@ -336,10 +311,7 @@ export function renderMiseToml(
|
|
|
336
311
|
return out.join('\n');
|
|
337
312
|
}
|
|
338
313
|
|
|
339
|
-
export function planInit(
|
|
340
|
-
projectRoot: string,
|
|
341
|
-
opts: { baseline?: string; stack?: string } = {},
|
|
342
|
-
): InitPlan {
|
|
314
|
+
export function planInit(projectRoot: string, opts: { baseline?: string; stack?: string } = {}): InitPlan {
|
|
343
315
|
const detectedStack = autoDetectStack(projectRoot);
|
|
344
316
|
const stackOverridden = opts.stack !== undefined && opts.stack !== detectedStack;
|
|
345
317
|
const stack = opts.stack ?? detectedStack;
|
|
@@ -436,9 +408,7 @@ export function planInit(
|
|
|
436
408
|
|
|
437
409
|
export function writeInit(plan: InitPlan, opts: { force: boolean }): void {
|
|
438
410
|
if (fs.existsSync(plan.miseTomlPath) && !opts.force) {
|
|
439
|
-
throw new Error(
|
|
440
|
-
`mise.toml уже существует: ${plan.miseTomlPath}. Используй --force для перезаписи.`,
|
|
441
|
-
);
|
|
411
|
+
throw new Error(`mise.toml уже существует: ${plan.miseTomlPath}. Используй --force для перезаписи.`);
|
|
442
412
|
}
|
|
443
413
|
fs.writeFileSync(plan.miseTomlPath, plan.miseTomlContent, 'utf8');
|
|
444
414
|
}
|
|
@@ -451,7 +421,9 @@ export function renderPlanSummary(plan: InitPlan, opts: { verbose?: boolean } =
|
|
|
451
421
|
|
|
452
422
|
if (!verbose) {
|
|
453
423
|
const lines: string[] = [];
|
|
454
|
-
lines.push(
|
|
424
|
+
lines.push(
|
|
425
|
+
`vdx init: stack=${plan.stack}${plan.stackOverridden ? ' (via --stack)' : ''}, ${mapped}/${total} verbs mapped`,
|
|
426
|
+
);
|
|
455
427
|
if (plan.primarySubpackage) lines.push(` primary subpackage: ${plan.primarySubpackage}`);
|
|
456
428
|
if (unmatched.length > 0) {
|
|
457
429
|
lines.push(` not mapped: ${unmatched.join(', ')} (add [tasks.X] in mise.toml manually)`);
|
|
@@ -462,12 +434,8 @@ export function renderPlanSummary(plan: InitPlan, opts: { verbose?: boolean } =
|
|
|
462
434
|
const lines: string[] = [];
|
|
463
435
|
lines.push(`# vdx init plan`);
|
|
464
436
|
lines.push('');
|
|
465
|
-
lines.push(
|
|
466
|
-
|
|
467
|
-
);
|
|
468
|
-
lines.push(
|
|
469
|
-
`- **Stack**: ${plan.stack}${plan.stackOverridden ? ' (via --stack)' : ''}`,
|
|
470
|
-
);
|
|
437
|
+
lines.push(`- **Project**: ${plan.projectRoot}`);
|
|
438
|
+
lines.push(`- **Stack**: ${plan.stack}${plan.stackOverridden ? ' (via --stack)' : ''}`);
|
|
471
439
|
if (plan.primarySubpackage) {
|
|
472
440
|
lines.push(`- **Primary subpackage**: ${plan.primarySubpackage}`);
|
|
473
441
|
}
|
|
@@ -492,9 +460,7 @@ export function renderPlanSummary(plan: InitPlan, opts: { verbose?: boolean } =
|
|
|
492
460
|
lines.push('|------|-------------|--------|-----------------|');
|
|
493
461
|
for (const m of plan.mappings) {
|
|
494
462
|
if (m.runCommand) {
|
|
495
|
-
lines.push(
|
|
496
|
-
`| \`${m.verb}\` | \`${m.nativeTask}\` | ${m.reason} | \`${m.runCommand}\` |`,
|
|
497
|
-
);
|
|
463
|
+
lines.push(`| \`${m.verb}\` | \`${m.nativeTask}\` | ${m.reason} | \`${m.runCommand}\` |`);
|
|
498
464
|
} else {
|
|
499
465
|
lines.push(`| \`${m.verb}\` | _(not found)_ | not-found | _(skipped)_ |`);
|
|
500
466
|
}
|
|
@@ -506,9 +472,7 @@ export function renderPlanSummary(plan: InitPlan, opts: { verbose?: boolean } =
|
|
|
506
472
|
lines.push('');
|
|
507
473
|
for (const m of withAlts) {
|
|
508
474
|
const alts = m.alternatives.map((a) => `\`${a}\``).join(', ');
|
|
509
|
-
lines.push(
|
|
510
|
-
`- \`${m.verb}\` chose \`${m.nativeTask}\` (${m.reason}); also matched: ${alts}`,
|
|
511
|
-
);
|
|
475
|
+
lines.push(`- \`${m.verb}\` chose \`${m.nativeTask}\` (${m.reason}); also matched: ${alts}`);
|
|
512
476
|
}
|
|
513
477
|
lines.push('');
|
|
514
478
|
}
|
package/src/mcp-server.ts
CHANGED
|
@@ -94,8 +94,7 @@ function buildCapabilities() {
|
|
|
94
94
|
const verbsList = (vdxBlock?.verbs as string[] | undefined) ?? 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;
|
|
97
|
+
const nativeCommand = t && typeof t === 'object' && typeof t.run === 'string' ? t.run : null;
|
|
99
98
|
return {
|
|
100
99
|
name,
|
|
101
100
|
has_manifest: Boolean(t),
|
|
@@ -236,9 +235,7 @@ server.registerTool(
|
|
|
236
235
|
'Записывает обнаруженные команды в [tasks.X] секцию mise.toml + обновляет [vdx.success_path]. Используется после успешной верификации команд агентом.',
|
|
237
236
|
inputSchema: {
|
|
238
237
|
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(),
|
|
238
|
+
shared_infra: z.object({ provider: z.string(), precheck: z.boolean() }).optional(),
|
|
242
239
|
notes: z.string().optional(),
|
|
243
240
|
},
|
|
244
241
|
},
|
package/src/personal-hooks.ts
CHANGED
|
@@ -92,17 +92,25 @@ function stateOf(h: PersonalHook, root: string | null, deps: PersonalHookDeps):
|
|
|
92
92
|
let drift: string | null = null;
|
|
93
93
|
if (have.length === 0 && command === null) drift = 'not in ~/.gitconfig';
|
|
94
94
|
else if (command !== h.command) drift = `its command in ~/.gitconfig differs from the profile`;
|
|
95
|
-
else if (events(h).some((e) => !have.includes(e)))
|
|
95
|
+
else if (events(h).some((e) => !have.includes(e)))
|
|
96
|
+
drift = `not bound to ${events(h)
|
|
97
|
+
.filter((e) => !have.includes(e))
|
|
98
|
+
.join(', ')}`;
|
|
96
99
|
|
|
97
100
|
let local: string | null = null;
|
|
98
101
|
if (root !== null) {
|
|
99
|
-
if (deps.git(['-C', root, 'config', '--local', '--get', `${key}.enabled`]) === 'false')
|
|
100
|
-
|
|
102
|
+
if (deps.git(['-C', root, 'config', '--local', '--get', `${key}.enabled`]) === 'false')
|
|
103
|
+
local = 'off in this repository';
|
|
104
|
+
else if (deps.git(['-C', root, 'config', '--local', '--get', `${key}.command`]) !== null)
|
|
105
|
+
local = 'own command in this repository';
|
|
101
106
|
}
|
|
102
107
|
return { hook: h, drift, local };
|
|
103
108
|
}
|
|
104
109
|
|
|
105
|
-
export function checkPersonalHooks(
|
|
110
|
+
export function checkPersonalHooks(
|
|
111
|
+
projectRoot: string | null,
|
|
112
|
+
deps: PersonalHookDeps = defaultPersonalHookDeps(),
|
|
113
|
+
): CheckResult | null {
|
|
106
114
|
const hooks = applicableHooks(deps);
|
|
107
115
|
if (!hooks) return null;
|
|
108
116
|
const id = 'personal-hooks';
|
|
@@ -132,7 +140,10 @@ export function checkPersonalHooks(projectRoot: string | null, deps: PersonalHoo
|
|
|
132
140
|
};
|
|
133
141
|
}
|
|
134
142
|
|
|
135
|
-
const missing = missingMachinePaths(
|
|
143
|
+
const missing = missingMachinePaths(
|
|
144
|
+
hooks.map((h) => h.command),
|
|
145
|
+
deps.home,
|
|
146
|
+
);
|
|
136
147
|
if (missing.length > 0) {
|
|
137
148
|
return {
|
|
138
149
|
id,
|
package/src/predicates.ts
CHANGED
|
@@ -132,9 +132,7 @@ export const REGISTRY: Record<string, PredicateFn> = {
|
|
|
132
132
|
|
|
133
133
|
phpstan_level_at_least: (args, ctx) => {
|
|
134
134
|
const text =
|
|
135
|
-
readText(ctx, 'phpstan.neon') ||
|
|
136
|
-
readText(ctx, 'phpstan.dist.neon') ||
|
|
137
|
-
readText(ctx, 'phpstan.neon.dist');
|
|
135
|
+
readText(ctx, 'phpstan.neon') || readText(ctx, 'phpstan.dist.neon') || readText(ctx, 'phpstan.neon.dist');
|
|
138
136
|
if (!text) return false;
|
|
139
137
|
const m = text.match(/^\s*level:\s*(\d+|max)/m);
|
|
140
138
|
if (!m || !m[1]) return false;
|
|
@@ -236,8 +234,7 @@ export const REGISTRY: Record<string, PredicateFn> = {
|
|
|
236
234
|
}),
|
|
237
235
|
|
|
238
236
|
/** L4 (library): the testing job runs across a matrix. */
|
|
239
|
-
gha_test_matrix: (args, ctx) =>
|
|
240
|
-
ciVerdict(ctx, () => [...checkingJobs(ctx, testPattern(args))].some((j) => j.matrix)),
|
|
237
|
+
gha_test_matrix: (args, ctx) => ciVerdict(ctx, () => [...checkingJobs(ctx, testPattern(args))].some((j) => j.matrix)),
|
|
241
238
|
|
|
242
239
|
/** L4 (service): whatever CI ships on a push to main waits, via `needs:`, for a checking job. */
|
|
243
240
|
gha_ship_needs_checks: (args, ctx) =>
|
package/src/publish.ts
CHANGED
|
@@ -72,7 +72,10 @@ export function bumpSemver(current: string, kind: BumpKind): string {
|
|
|
72
72
|
|
|
73
73
|
export function compareSemver(a: string, b: string): number {
|
|
74
74
|
const norm = (s: string) =>
|
|
75
|
-
s
|
|
75
|
+
s
|
|
76
|
+
.replace(/^v/, '')
|
|
77
|
+
.split(/[.\-+]/)
|
|
78
|
+
.map((p) => parseInt(p, 10) || 0);
|
|
76
79
|
const pa = norm(a);
|
|
77
80
|
const pb = norm(b);
|
|
78
81
|
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
@@ -249,9 +252,7 @@ export function planPublish(
|
|
|
249
252
|
// Resolve effective stack: prefer subpackage stack if primary_subpackage is set
|
|
250
253
|
// (meta-projects publishing a nested lib, e.g. vdx itself with primary_subpackage=cli).
|
|
251
254
|
const subpkg = audit.primary_subpackage;
|
|
252
|
-
const effectiveStack = subpkg
|
|
253
|
-
? stackForDir(path.join(opts.projectRoot, subpkg))
|
|
254
|
-
: ctx.stack;
|
|
255
|
+
const effectiveStack = subpkg ? stackForDir(path.join(opts.projectRoot, subpkg)) : ctx.stack;
|
|
255
256
|
|
|
256
257
|
if (effectiveStack !== 'node') {
|
|
257
258
|
throw new Error(
|
|
@@ -543,7 +544,9 @@ export function executePublish(plan: PublishPlan, deps: PublishDeps = defaultPub
|
|
|
543
544
|
try {
|
|
544
545
|
deps.run('git', ['push', '--follow-tags'], plan.projectRoot);
|
|
545
546
|
} catch {
|
|
546
|
-
deps.log(
|
|
547
|
+
deps.log(
|
|
548
|
+
`✗ git push failed — ${plan.packageName}@${plan.newVersion} is published and tagged here; push it: git push --follow-tags`,
|
|
549
|
+
);
|
|
547
550
|
throw new Error('git push failed');
|
|
548
551
|
}
|
|
549
552
|
deps.log(`✓ pushed to ${push.upstream} with tag v${plan.newVersion}`);
|
|
@@ -599,7 +602,9 @@ export function renderPublishPlan(plan: PublishPlan): string {
|
|
|
599
602
|
lines.push('## Pipeline (would execute on `vdx publish`)');
|
|
600
603
|
lines.push('');
|
|
601
604
|
if (fixable) {
|
|
602
|
-
const fixes = plan.preflight
|
|
605
|
+
const fixes = plan.preflight
|
|
606
|
+
.filter((c) => !c.ok && c.fix)
|
|
607
|
+
.map((c) => `\`${c.fix!.cmd} ${c.fix!.args.join(' ')}\``);
|
|
603
608
|
lines.push(`0. ${fixes.join(', ')} — at a terminal, then the pre-flight again`);
|
|
604
609
|
}
|
|
605
610
|
lines.push(`1. Set \`version\` in ${files} → \`${plan.newVersion}\``);
|
package/src/report.ts
CHANGED
|
@@ -116,9 +116,7 @@ export function reportDoctorMarkdown(r: DoctorReport): string {
|
|
|
116
116
|
const lines: string[] = [];
|
|
117
117
|
lines.push('# vdx doctor report');
|
|
118
118
|
lines.push('');
|
|
119
|
-
lines.push(
|
|
120
|
-
`**OK**: ${r.ok} **Warning**: ${r.warning} **Missing**: ${r.missing}`,
|
|
121
|
-
);
|
|
119
|
+
lines.push(`**OK**: ${r.ok} **Warning**: ${r.warning} **Missing**: ${r.missing}`);
|
|
122
120
|
lines.push('');
|
|
123
121
|
lines.push('| Check | Status | Detail | Remedy |');
|
|
124
122
|
lines.push('|-------|:------:|--------|--------|');
|
package/src/run.ts
CHANGED
|
@@ -13,10 +13,7 @@ export type VerbResolveResult =
|
|
|
13
13
|
detail?: string;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
export function resolveLifecycleVerb(
|
|
17
|
-
projectRoot: string,
|
|
18
|
-
verb: string,
|
|
19
|
-
): VerbResolveResult {
|
|
16
|
+
export function resolveLifecycleVerb(projectRoot: string, verb: string): VerbResolveResult {
|
|
20
17
|
const miseTomlPath = path.join(projectRoot, 'mise.toml');
|
|
21
18
|
if (!fs.existsSync(miseTomlPath)) {
|
|
22
19
|
return { ok: false, reason: 'no-mise-toml' };
|
|
@@ -42,8 +39,7 @@ export function renderResolveError(
|
|
|
42
39
|
switch (res.reason) {
|
|
43
40
|
case 'no-mise-toml':
|
|
44
41
|
return (
|
|
45
|
-
`vdx: no mise.toml in ${projectRoot}\n` +
|
|
46
|
-
`hint: run \`vdx init\` to generate one, or define it manually.\n`
|
|
42
|
+
`vdx: no mise.toml in ${projectRoot}\n` + `hint: run \`vdx init\` to generate one, or define it manually.\n`
|
|
47
43
|
);
|
|
48
44
|
case 'parse-error':
|
|
49
45
|
return `vdx: failed to parse mise.toml: ${res.detail ?? 'unknown error'}\n`;
|
package/src/scoring.ts
CHANGED
|
@@ -95,15 +95,9 @@ function evalAxisFlags(axis: Axis, ctx: Ctx): LevelName {
|
|
|
95
95
|
return achieved;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
export function projectLevel(
|
|
99
|
-
results: AxisResult[],
|
|
100
|
-
rubric: Rubric,
|
|
101
|
-
suppressed: Set<string>,
|
|
102
|
-
): LevelName {
|
|
98
|
+
export function projectLevel(results: AxisResult[], rubric: Rubric, suppressed: Set<string>): LevelName {
|
|
103
99
|
const supportingThreshold = rubric.scoring.supporting_threshold ?? 0.8;
|
|
104
|
-
const visible = results.filter(
|
|
105
|
-
(r) => !suppressed.has(r.axis_id) && r.drift_kind !== 'excluded',
|
|
106
|
-
);
|
|
100
|
+
const visible = results.filter((r) => !suppressed.has(r.axis_id) && r.drift_kind !== 'excluded');
|
|
107
101
|
const critical = visible.filter((r) => r.class === 'critical');
|
|
108
102
|
const supporting = visible.filter((r) => r.class === 'supporting');
|
|
109
103
|
|
package/src/vocabulary.ts
CHANGED
|
@@ -39,7 +39,11 @@ export function runnerCalls(command: string): string[] {
|
|
|
39
39
|
const toks = tokenize(raw.trim());
|
|
40
40
|
let i = 0;
|
|
41
41
|
// `FOO=1 npm test`, `exec npm test`, `npx --no -- vdx test`
|
|
42
|
-
while (
|
|
42
|
+
while (
|
|
43
|
+
i < toks.length &&
|
|
44
|
+
(/^[A-Za-z_][A-Za-z0-9_]*=/.test(toks[i]!) || ['exec', 'time', 'command'].includes(toks[i]!))
|
|
45
|
+
)
|
|
46
|
+
i++;
|
|
43
47
|
const head = toks[i];
|
|
44
48
|
if (!head) continue;
|
|
45
49
|
const runner = head.split('/').pop()!;
|
|
@@ -116,7 +120,8 @@ export function taskChildren(ctx: Ctx, name: string): string[] {
|
|
|
116
120
|
const mise = readToml(ctx, 'mise.toml');
|
|
117
121
|
const task = mise?.tasks?.[name];
|
|
118
122
|
if (task && typeof task === 'object') {
|
|
119
|
-
if (Array.isArray(task.depends))
|
|
123
|
+
if (Array.isArray(task.depends))
|
|
124
|
+
out.push(...task.depends.filter((d: unknown): d is string => typeof d === 'string'));
|
|
120
125
|
const runs = Array.isArray(task.run) ? task.run : [task.run];
|
|
121
126
|
for (const r of runs) if (typeof r === 'string') out.push(...runnerCalls(r));
|
|
122
127
|
}
|