@vodmal/vdx-cli 0.2.0 → 0.3.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 +44 -32
- package/package.json +9 -5
- package/rubric/vdx-rubric.yaml +82 -1
- package/src/audit.ts +23 -4
- package/src/facts.ts +1 -1
- package/src/index.ts +11 -2
- package/src/init.ts +119 -17
- package/src/rubric.ts +1 -0
package/README.md
CHANGED
|
@@ -1,51 +1,63 @@
|
|
|
1
|
-
# vdx-cli —
|
|
1
|
+
# @vodmal/vdx-cli — vdx evaluator + MCP server
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Native predicate evaluator for `vdx-rubric-vodmal` + MCP stdio server for the
|
|
4
|
+
Claude Code `vdx` plugin. Decision basis: [docs/decisions.md](../docs/decisions.md)
|
|
5
|
+
D10 (native evaluator, not OPA).
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
Published on npm as **[@vodmal/vdx-cli](https://www.npmjs.com/package/@vodmal/vdx-cli)**.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
7
10
|
|
|
8
11
|
```bash
|
|
9
|
-
|
|
10
|
-
npm install
|
|
12
|
+
# global install
|
|
13
|
+
npm install -g @vodmal/vdx-cli
|
|
14
|
+
|
|
15
|
+
# or one-shot via npx (no install)
|
|
16
|
+
npx -y -p @vodmal/vdx-cli vdx audit /path/to/project
|
|
11
17
|
```
|
|
12
18
|
|
|
13
|
-
##
|
|
19
|
+
## Use
|
|
14
20
|
|
|
15
21
|
```bash
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
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)
|
|
19
25
|
```
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
`--rubric
|
|
27
|
+
By default the bundled `rubric/vdx-rubric.yaml` is used (a mirror of canonical
|
|
28
|
+
[vdx-rubric-vodmal](https://github.com/VoDmAl/vdx-rubric-vodmal) at the time of
|
|
29
|
+
each CLI release). Override via `--rubric <path>` or the env var `VDX_RUBRIC`.
|
|
24
30
|
|
|
25
|
-
##
|
|
31
|
+
## Dev
|
|
26
32
|
|
|
27
33
|
```bash
|
|
28
|
-
|
|
34
|
+
git clone https://github.com/VoDmAl/vdx
|
|
35
|
+
cd vdx/cli
|
|
36
|
+
npm install
|
|
37
|
+
npm run audit -- /path/to/project --json
|
|
38
|
+
npm run smoke # gauge against 3 calibration references
|
|
39
|
+
npm run typecheck
|
|
29
40
|
```
|
|
30
41
|
|
|
31
|
-
|
|
32
|
-
(telegram / t23b / bookmap). Ожидаемо все capped at L2 на оси `ci`.
|
|
33
|
-
|
|
34
|
-
## Структура
|
|
42
|
+
## Structure
|
|
35
43
|
|
|
36
|
-
- `src/rubric.ts` —
|
|
37
|
-
- `src/manifest.ts` —
|
|
38
|
-
- `src/facts.ts` — fact source loaders (tasks, packages, configs)
|
|
39
|
-
- `src/predicates.ts` — registry
|
|
40
|
-
- `src/evaluator.ts` —
|
|
41
|
-
- `src/scoring.ts` — delta-style
|
|
42
|
-
- `src/audit.ts` —
|
|
43
|
-
- `src/report.ts` — markdown / JSON
|
|
44
|
+
- `src/rubric.ts` — types + YAML loader
|
|
45
|
+
- `src/manifest.ts` — parser for `[vdx]` block in `mise.toml` + `.vdx-overrides.yml`
|
|
46
|
+
- `src/facts.ts` — fact source loaders (tasks, packages, configs, sub-package detection)
|
|
47
|
+
- `src/predicates.ts` — predicate registry (11 functions)
|
|
48
|
+
- `src/evaluator.ts` — recursive evaluator + sugar notation
|
|
49
|
+
- `src/scoring.ts` — delta-style levels, flags for orthogonal axes
|
|
50
|
+
- `src/audit.ts` — orchestrator: overrides + applies_to filter + subpackage-ctx
|
|
51
|
+
- `src/report.ts` — markdown / JSON output
|
|
44
52
|
- `src/index.ts` — CLI entry
|
|
53
|
+
- `src/mcp-server.ts` — MCP stdio server (9 tools)
|
|
54
|
+
- `src/defaults.ts` — bundled rubric resolution
|
|
55
|
+
- `bin/vdx.cjs`, `bin/vdx-mcp.cjs` — Node wrappers (tsx ESM loader in-process)
|
|
45
56
|
|
|
46
|
-
##
|
|
57
|
+
## Not implemented yet
|
|
47
58
|
|
|
48
|
-
- `config_value
|
|
49
|
-
-
|
|
50
|
-
|
|
51
|
-
-
|
|
59
|
+
- `config_value`/`command_succeeds` predicates are stubs (warning at evaluation).
|
|
60
|
+
- Baseline loading from a git ref is documented but evaluator still reads file
|
|
61
|
+
paths only — `baseline:` in `mise.toml` is recorded but does not auto-fetch.
|
|
62
|
+
- Watermark drift (phase 2 of `drift-algorithm.md`).
|
|
63
|
+
- O25/O26/O27/O33/O34 — see [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.3.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",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
},
|
|
24
24
|
"type": "module",
|
|
25
25
|
"bin": {
|
|
26
|
-
"vdx": "
|
|
27
|
-
"vdx-mcp": "
|
|
26
|
+
"vdx": "bin/vdx.cjs",
|
|
27
|
+
"vdx-mcp": "bin/vdx-mcp.cjs"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"src",
|
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
"audit": "tsx src/index.ts audit",
|
|
40
40
|
"smoke": "bash smoke.sh",
|
|
41
41
|
"typecheck": "tsc --noEmit",
|
|
42
|
-
"test": "
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:unit": "vitest run tests/unit",
|
|
44
|
+
"coverage": "vitest run --coverage"
|
|
43
45
|
},
|
|
44
46
|
"dependencies": {
|
|
45
47
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -51,7 +53,9 @@
|
|
|
51
53
|
"devDependencies": {
|
|
52
54
|
"@types/js-yaml": "^4.0.9",
|
|
53
55
|
"@types/node": "^22.0.0",
|
|
54
|
-
"
|
|
56
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
57
|
+
"typescript": "^5.6.0",
|
|
58
|
+
"vitest": "^4.1.7"
|
|
55
59
|
},
|
|
56
60
|
"publishConfig": {
|
|
57
61
|
"access": "public"
|
package/rubric/vdx-rubric.yaml
CHANGED
|
@@ -11,7 +11,7 @@ schema_version: "0.2"
|
|
|
11
11
|
|
|
12
12
|
metadata:
|
|
13
13
|
name: vdx-rubric-vodmal
|
|
14
|
-
version: "0.
|
|
14
|
+
version: "0.3.1"
|
|
15
15
|
description: "Personal maturity rubric for multi-stack projects (vodmal)"
|
|
16
16
|
owner: vodmal
|
|
17
17
|
homepage: "https://github.com/vodmal/vdx-rubric"
|
|
@@ -534,3 +534,84 @@ axes:
|
|
|
534
534
|
file_contains:
|
|
535
535
|
paths: [misc/traefik-global/README.md, misc/traefik-global/start.sh]
|
|
536
536
|
pattern: "@v\\d+\\.\\d+\\.\\d+" # semver pin
|
|
537
|
+
|
|
538
|
+
- id: release-artifact
|
|
539
|
+
class: supporting
|
|
540
|
+
description: "Publish-readiness: metadata completeness + entry points + registry signals (O34, v0.3.0). Применяется только к проектам, заявляющим publish-intent (O35, v0.3.1) — иначе excluded."
|
|
541
|
+
applies_to: [node, php, ruby, python]
|
|
542
|
+
# O35 (v0.3.1): ось применяется только если проект декларирует намерение
|
|
543
|
+
# публиковать артефакт. Чистые app'ы (private:true в package.json или
|
|
544
|
+
# composer type:project) → excluded.
|
|
545
|
+
applies_when:
|
|
546
|
+
any_of:
|
|
547
|
+
# Node-сторона: НЕ private И есть хоть один entry-point/publish-signal.
|
|
548
|
+
- all_of:
|
|
549
|
+
- has_file: package.json
|
|
550
|
+
- not:
|
|
551
|
+
config_value: { path: package.json, jsonpath: "private", equals: true }
|
|
552
|
+
- any_of:
|
|
553
|
+
- config_value: { path: package.json, jsonpath: "publishConfig", op: present }
|
|
554
|
+
- config_value: { path: package.json, jsonpath: "bin", op: present }
|
|
555
|
+
- config_value: { path: package.json, jsonpath: "main", op: present }
|
|
556
|
+
- config_value: { path: package.json, jsonpath: "exports", op: present }
|
|
557
|
+
- config_value: { path: package.json, jsonpath: "module", op: present }
|
|
558
|
+
# PHP-сторона: composer.json есть, name задан, type ≠ project (project = app).
|
|
559
|
+
- all_of:
|
|
560
|
+
- has_file: composer.json
|
|
561
|
+
- config_value: { path: composer.json, jsonpath: "name", op: present }
|
|
562
|
+
- not:
|
|
563
|
+
config_value: { path: composer.json, jsonpath: "type", op: equals, equals: "project" }
|
|
564
|
+
storage: level
|
|
565
|
+
default_target: L4
|
|
566
|
+
fact_sources:
|
|
567
|
+
- package.json
|
|
568
|
+
- composer.json
|
|
569
|
+
- LICENSE
|
|
570
|
+
- LICENSE.md
|
|
571
|
+
- LICENSE.txt
|
|
572
|
+
levels:
|
|
573
|
+
L1:
|
|
574
|
+
requires:
|
|
575
|
+
any_of:
|
|
576
|
+
- all_of:
|
|
577
|
+
- config_value: { path: package.json, jsonpath: "name", op: present }
|
|
578
|
+
- config_value: { path: package.json, jsonpath: "version", op: present }
|
|
579
|
+
- config_value: { path: package.json, jsonpath: "license", op: present }
|
|
580
|
+
- all_of:
|
|
581
|
+
- config_value: { path: composer.json, jsonpath: "name", op: present }
|
|
582
|
+
- config_value: { path: composer.json, jsonpath: "license", op: present }
|
|
583
|
+
L2:
|
|
584
|
+
requires:
|
|
585
|
+
any_of:
|
|
586
|
+
- all_of:
|
|
587
|
+
- config_value: { path: package.json, jsonpath: "description", op: present }
|
|
588
|
+
- config_value: { path: package.json, jsonpath: "repository", op: present }
|
|
589
|
+
- any_of:
|
|
590
|
+
- has_file: LICENSE
|
|
591
|
+
- has_file: LICENSE.md
|
|
592
|
+
- has_file: LICENSE.txt
|
|
593
|
+
- all_of:
|
|
594
|
+
- config_value: { path: composer.json, jsonpath: "description", op: present }
|
|
595
|
+
- any_of:
|
|
596
|
+
- has_file: LICENSE
|
|
597
|
+
- has_file: LICENSE.md
|
|
598
|
+
L3:
|
|
599
|
+
requires:
|
|
600
|
+
any_of:
|
|
601
|
+
- all_of:
|
|
602
|
+
- config_value: { path: package.json, jsonpath: "files", op: present }
|
|
603
|
+
- any_of:
|
|
604
|
+
- config_value: { path: package.json, jsonpath: "bin", op: present }
|
|
605
|
+
- config_value: { path: package.json, jsonpath: "main", op: present }
|
|
606
|
+
- config_value: { path: package.json, jsonpath: "exports", op: present }
|
|
607
|
+
- all_of:
|
|
608
|
+
- config_value: { path: composer.json, jsonpath: "autoload", op: present }
|
|
609
|
+
- config_value: { path: composer.json, jsonpath: "type", op: present }
|
|
610
|
+
L4:
|
|
611
|
+
requires:
|
|
612
|
+
any_of:
|
|
613
|
+
- all_of:
|
|
614
|
+
- config_value: { path: package.json, jsonpath: "publishConfig", op: present }
|
|
615
|
+
- config_value: { path: package.json, jsonpath: "homepage", op: present }
|
|
616
|
+
- config_value: { path: package.json, jsonpath: "bugs", op: present }
|
|
617
|
+
- config_value: { path: composer.json, jsonpath: "extra.publish", op: present }
|
package/src/audit.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import type { Rubric, LevelName } from './rubric.ts';
|
|
4
|
-
import { findSubPackages, type Ctx } from './facts.ts';
|
|
4
|
+
import { findSubPackages, stackForDir, type Ctx } from './facts.ts';
|
|
5
|
+
import { evalPredicate } from './evaluator.ts';
|
|
5
6
|
import type { Override, VdxManifest } from './manifest.ts';
|
|
6
7
|
import { evalAxis, projectLevel, type AxisResult } from './scoring.ts';
|
|
7
8
|
|
|
@@ -33,12 +34,14 @@ function resolveSubpackageCtx(
|
|
|
33
34
|
manifest: VdxManifest | null,
|
|
34
35
|
): { ctx: Ctx; relPath: string | null } {
|
|
35
36
|
let relPath: string | null = null;
|
|
37
|
+
let subpackageStack: string | null = null;
|
|
36
38
|
|
|
37
39
|
if (manifest?.primary_subpackage) {
|
|
38
40
|
const p = manifest.primary_subpackage;
|
|
39
41
|
const abs = path.join(ctx.projectRoot, p);
|
|
40
42
|
if (fs.existsSync(abs)) {
|
|
41
43
|
relPath = p;
|
|
44
|
+
subpackageStack = stackForDir(abs);
|
|
42
45
|
} else {
|
|
43
46
|
process.stderr.write(
|
|
44
47
|
`warning: primary_subpackage "${p}" does not exist — falling back to root\n`,
|
|
@@ -49,6 +52,12 @@ function resolveSubpackageCtx(
|
|
|
49
52
|
const matching = subs.filter((s) => s.stack === ctx.stack);
|
|
50
53
|
if (matching.length === 1) {
|
|
51
54
|
relPath = matching[0]!.relPath;
|
|
55
|
+
subpackageStack = matching[0]!.stack;
|
|
56
|
+
} else if (matching.length === 0 && subs.length === 1) {
|
|
57
|
+
// O33: single sub-package with a stack different from ctx.stack —
|
|
58
|
+
// adopt it so applies_to-axes for that stack can evaluate.
|
|
59
|
+
relPath = subs[0]!.relPath;
|
|
60
|
+
subpackageStack = subs[0]!.stack;
|
|
52
61
|
}
|
|
53
62
|
}
|
|
54
63
|
|
|
@@ -56,7 +65,7 @@ function resolveSubpackageCtx(
|
|
|
56
65
|
return {
|
|
57
66
|
ctx: {
|
|
58
67
|
projectRoot: path.join(ctx.projectRoot, relPath),
|
|
59
|
-
stack: ctx.stack,
|
|
68
|
+
stack: subpackageStack ?? ctx.stack,
|
|
60
69
|
cache: new Map(),
|
|
61
70
|
},
|
|
62
71
|
relPath,
|
|
@@ -96,7 +105,18 @@ export function audit(
|
|
|
96
105
|
});
|
|
97
106
|
continue;
|
|
98
107
|
}
|
|
99
|
-
|
|
108
|
+
const evalCtx = axis.applies_to ? subpackageCtx : ctx;
|
|
109
|
+
if (axis.applies_to && !axis.applies_to.includes(evalCtx.stack)) {
|
|
110
|
+
perAxis.push({
|
|
111
|
+
axis_id: axis.id,
|
|
112
|
+
class: axis.class,
|
|
113
|
+
achieved: 'L0',
|
|
114
|
+
target: axis.default_target,
|
|
115
|
+
drift_kind: 'excluded',
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (axis.applies_when && !evalPredicate(axis.applies_when, evalCtx)) {
|
|
100
120
|
perAxis.push({
|
|
101
121
|
axis_id: axis.id,
|
|
102
122
|
class: axis.class,
|
|
@@ -106,7 +126,6 @@ export function audit(
|
|
|
106
126
|
});
|
|
107
127
|
continue;
|
|
108
128
|
}
|
|
109
|
-
const evalCtx = axis.applies_to ? subpackageCtx : ctx;
|
|
110
129
|
const achieved = evalAxis(axis, evalCtx);
|
|
111
130
|
const ov = overrideByAxis.get(axis.id);
|
|
112
131
|
const target = (ov?.target as LevelName) ?? axis.default_target;
|
package/src/facts.ts
CHANGED
|
@@ -216,7 +216,7 @@ const MANIFEST_TO_STACK: Array<[string, string]> = [
|
|
|
216
216
|
['requirements.txt', 'python'],
|
|
217
217
|
];
|
|
218
218
|
|
|
219
|
-
function stackForDir(dir: string): string | null {
|
|
219
|
+
export function stackForDir(dir: string): string | null {
|
|
220
220
|
for (const [manifest, stack] of MANIFEST_TO_STACK) {
|
|
221
221
|
if (fs.existsSync(path.join(dir, manifest))) return stack;
|
|
222
222
|
}
|
package/src/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ function usage(): never {
|
|
|
14
14
|
process.stderr.write(
|
|
15
15
|
`Usage:
|
|
16
16
|
vdx audit <project_path> [--rubric <path>] [--stack <stack>] [--json]
|
|
17
|
-
vdx init <project_path> [--baseline <ref>] [--dry-run] [--force]
|
|
17
|
+
vdx init <project_path> [--stack <id>] [--baseline <ref>] [--dry-run] [--force]
|
|
18
18
|
`,
|
|
19
19
|
);
|
|
20
20
|
process.exit(1);
|
|
@@ -86,10 +86,19 @@ function cmdInit(opts: ParsedArgs): void {
|
|
|
86
86
|
|
|
87
87
|
const baselineFlag = opts.flags.baseline;
|
|
88
88
|
const baseline = typeof baselineFlag === 'string' ? baselineFlag : undefined;
|
|
89
|
-
const
|
|
89
|
+
const stackFlag = opts.flags.stack;
|
|
90
|
+
const stackOverride = typeof stackFlag === 'string' ? stackFlag : undefined;
|
|
91
|
+
const plan = planInit(projectRoot, {
|
|
92
|
+
...(baseline ? { baseline } : {}),
|
|
93
|
+
...(stackOverride ? { stack: stackOverride } : {}),
|
|
94
|
+
});
|
|
90
95
|
|
|
91
96
|
process.stdout.write(renderPlanSummary(plan));
|
|
92
97
|
|
|
98
|
+
for (const w of plan.warnings) {
|
|
99
|
+
process.stderr.write(`\n⚠ ${w}\n`);
|
|
100
|
+
}
|
|
101
|
+
|
|
93
102
|
if (opts.flags['dry-run']) {
|
|
94
103
|
process.stderr.write('\n[dry-run] mise.toml / AGENTS.md не записаны.\n');
|
|
95
104
|
return;
|
package/src/init.ts
CHANGED
|
@@ -7,12 +7,14 @@ import {
|
|
|
7
7
|
readText,
|
|
8
8
|
listAllTasks,
|
|
9
9
|
autoDetectStack,
|
|
10
|
+
findSubPackages,
|
|
11
|
+
type SubPackage,
|
|
10
12
|
} from './facts.ts';
|
|
11
13
|
|
|
12
14
|
export const STANDARD_VERBS = ['up', 'down', 'build', 'test', 'check', 'fix'] as const;
|
|
13
15
|
export type Verb = (typeof STANDARD_VERBS)[number];
|
|
14
16
|
|
|
15
|
-
export const DEFAULT_BASELINE = 'github.com/VoDmAl/vdx-rubric-vodmal@v0.
|
|
17
|
+
export const DEFAULT_BASELINE = 'github.com/VoDmAl/vdx-rubric-vodmal@v0.3.1';
|
|
16
18
|
|
|
17
19
|
const VERB_ALIASES: Record<Verb, string[]> = {
|
|
18
20
|
up: ['docker:up', 'docker-up', 'docker:up:detached', 'start'],
|
|
@@ -41,6 +43,10 @@ export interface VerbMapping {
|
|
|
41
43
|
export interface InitPlan {
|
|
42
44
|
projectRoot: string;
|
|
43
45
|
stack: string;
|
|
46
|
+
/** Was `stack` derived from autoDetectStack (false) or supplied via --stack (true). */
|
|
47
|
+
stackOverridden: boolean;
|
|
48
|
+
/** Non-null only when stack === 'meta' and resolved a single nested manifest. */
|
|
49
|
+
primarySubpackage: string | null;
|
|
44
50
|
pkgManager: 'composer' | 'npm' | 'pnpm' | 'yarn' | null;
|
|
45
51
|
tools: Record<string, string>;
|
|
46
52
|
mappings: VerbMapping[];
|
|
@@ -49,6 +55,8 @@ export interface InitPlan {
|
|
|
49
55
|
agentsMdPath: string;
|
|
50
56
|
agentsMdContent: string;
|
|
51
57
|
baseline: string;
|
|
58
|
+
/** Human-readable advisories for the user — printed to stderr by the CLI. */
|
|
59
|
+
warnings: string[];
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
function detectNodePackageManager(projectRoot: string): 'pnpm' | 'yarn' | 'npm' {
|
|
@@ -67,6 +75,15 @@ function renderRunCommand(task: string, stack: string, projectRoot: string): str
|
|
|
67
75
|
return task;
|
|
68
76
|
}
|
|
69
77
|
|
|
78
|
+
function renderRunCommandInSubpackage(
|
|
79
|
+
task: string,
|
|
80
|
+
subStack: string,
|
|
81
|
+
subAbs: string,
|
|
82
|
+
subRel: string,
|
|
83
|
+
): string {
|
|
84
|
+
return `cd ${subRel} && ${renderRunCommand(task, subStack, subAbs)}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
70
87
|
function pickPrefixGroup(prefixes: string[], tasks: Set<string>, verb: Verb): string | null {
|
|
71
88
|
if (prefixes.length === 0) return null;
|
|
72
89
|
// Prefer entries that look like a canonical "prod/dist/release" first, then any.
|
|
@@ -135,12 +152,23 @@ export function renderMiseToml(
|
|
|
135
152
|
mappings: VerbMapping[],
|
|
136
153
|
stack: string,
|
|
137
154
|
baseline: string,
|
|
155
|
+
primarySubpackage: string | null = null,
|
|
138
156
|
): string {
|
|
139
157
|
const out: string[] = [];
|
|
140
158
|
out.push('# Generated by vdx init. The [vdx] block is metadata for vdx audit.');
|
|
141
159
|
out.push('# Lifecycle verbs follow the vdx contract: up/down/build/test/check/fix.');
|
|
142
160
|
out.push('');
|
|
143
161
|
|
|
162
|
+
if (stack === 'unknown' || stack === 'monorepo') {
|
|
163
|
+
out.push(
|
|
164
|
+
'# TODO(vdx): stack auto-detection вернул "' +
|
|
165
|
+
stack +
|
|
166
|
+
'". Замени на php/node/go/python/meta',
|
|
167
|
+
);
|
|
168
|
+
out.push('# и заполни [tasks.*] блоки руками, либо перегенери: vdx init . --stack <id>.');
|
|
169
|
+
out.push('');
|
|
170
|
+
}
|
|
171
|
+
|
|
144
172
|
if (Object.keys(tools).length > 0) {
|
|
145
173
|
out.push('[tools]');
|
|
146
174
|
for (const [k, v] of Object.entries(tools)) {
|
|
@@ -161,6 +189,9 @@ export function renderMiseToml(
|
|
|
161
189
|
out.push('schema_version = "0.2"');
|
|
162
190
|
out.push(`baseline = ${quoteToml(baseline)}`);
|
|
163
191
|
out.push(`stack = ${quoteToml(stack)}`);
|
|
192
|
+
if (primarySubpackage) {
|
|
193
|
+
out.push(`primary_subpackage = ${quoteToml(primarySubpackage)}`);
|
|
194
|
+
}
|
|
164
195
|
const verbsList = mappings
|
|
165
196
|
.filter((m) => m.runCommand)
|
|
166
197
|
.map((m) => quoteToml(m.verb))
|
|
@@ -217,36 +248,93 @@ export function buildAgentsMd(existing: string | null, section: string): string
|
|
|
217
248
|
return existing + sep + section + '\n';
|
|
218
249
|
}
|
|
219
250
|
|
|
220
|
-
export function planInit(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
251
|
+
export function planInit(
|
|
252
|
+
projectRoot: string,
|
|
253
|
+
opts: { baseline?: string; stack?: string } = {},
|
|
254
|
+
): InitPlan {
|
|
255
|
+
const detectedStack = autoDetectStack(projectRoot);
|
|
256
|
+
const stackOverridden = opts.stack !== undefined && opts.stack !== detectedStack;
|
|
257
|
+
const stack = opts.stack ?? detectedStack;
|
|
258
|
+
const warnings: string[] = [];
|
|
259
|
+
|
|
260
|
+
// For meta-stack — try to resolve a single nested subpackage and scan it for tasks.
|
|
261
|
+
let primarySubpackage: string | null = null;
|
|
262
|
+
let scanRoot = projectRoot;
|
|
263
|
+
let scanStack = stack;
|
|
264
|
+
|
|
265
|
+
if (stack === 'meta') {
|
|
266
|
+
const subs: SubPackage[] = findSubPackages(projectRoot);
|
|
267
|
+
if (subs.length === 1) {
|
|
268
|
+
primarySubpackage = subs[0]!.relPath;
|
|
269
|
+
scanRoot = path.join(projectRoot, primarySubpackage);
|
|
270
|
+
scanStack = subs[0]!.stack;
|
|
271
|
+
} else if (subs.length === 0) {
|
|
272
|
+
warnings.push(
|
|
273
|
+
'stack="meta", но nested manifest не найден. mise.toml будет иметь пустой verbs[]; ' +
|
|
274
|
+
'заполни [tasks.*] руками.',
|
|
275
|
+
);
|
|
276
|
+
} else {
|
|
277
|
+
const list = subs.map((s) => `${s.relPath}(${s.stack})`).join(', ');
|
|
278
|
+
warnings.push(
|
|
279
|
+
`stack="meta", найдено несколько subpackages: ${list}. ` +
|
|
280
|
+
'primary_subpackage не выбран; укажи его руками в [vdx] блоке.',
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
} else if (!stackOverridden && (detectedStack === 'unknown' || detectedStack === 'monorepo')) {
|
|
284
|
+
if (detectedStack === 'unknown') {
|
|
285
|
+
warnings.push(
|
|
286
|
+
'autoDetectStack вернул "unknown" — в корне нет manifest и нет subpackages. ' +
|
|
287
|
+
'Передай --stack <id> (php|node|go|python|meta) либо отредактируй mise.toml вручную.',
|
|
288
|
+
);
|
|
289
|
+
} else {
|
|
290
|
+
const subs = findSubPackages(projectRoot);
|
|
291
|
+
const list = subs.map((s) => `${s.relPath}(${s.stack})`).join(', ');
|
|
292
|
+
warnings.push(
|
|
293
|
+
`autoDetectStack вернул "monorepo": ${list}. ` +
|
|
294
|
+
'Передай --stack <id> чтобы выбрать конкретный стек, либо --stack meta + укажи primary_subpackage.',
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const scanCtx: Ctx = { projectRoot: scanRoot, stack: scanStack, cache: new Map() };
|
|
300
|
+
const tasks = listAllTasks(scanCtx);
|
|
224
301
|
|
|
225
302
|
const mappings: VerbMapping[] = STANDARD_VERBS.map((verb) => {
|
|
226
303
|
const native = mapVerb(verb, tasks);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
304
|
+
let runCommand: string | null = null;
|
|
305
|
+
if (native) {
|
|
306
|
+
runCommand =
|
|
307
|
+
primarySubpackage !== null
|
|
308
|
+
? renderRunCommandInSubpackage(native, scanStack, scanRoot, primarySubpackage)
|
|
309
|
+
: renderRunCommand(native, stack, projectRoot);
|
|
310
|
+
}
|
|
311
|
+
return { verb, nativeTask: native, runCommand };
|
|
232
312
|
});
|
|
233
313
|
|
|
234
|
-
|
|
314
|
+
// tools читаем из root независимо: для PHP/Node корневой manifest часто
|
|
315
|
+
// содержит engines/require php; для meta-проектов вынос tool-версий в
|
|
316
|
+
// root mise.toml — общая практика.
|
|
317
|
+
const rootCtx: Ctx = { projectRoot, stack, cache: new Map() };
|
|
318
|
+
const tools = collectTools(rootCtx);
|
|
235
319
|
const baseline = opts.baseline ?? DEFAULT_BASELINE;
|
|
236
|
-
const miseTomlContent = renderMiseToml(tools, mappings, stack, baseline);
|
|
320
|
+
const miseTomlContent = renderMiseToml(tools, mappings, stack, baseline, primarySubpackage);
|
|
237
321
|
|
|
238
322
|
const agentsMdPath = path.join(projectRoot, 'AGENTS.md');
|
|
239
|
-
const existingAgents = fileExists(
|
|
323
|
+
const existingAgents = fileExists(rootCtx, 'AGENTS.md') ? readText(rootCtx, 'AGENTS.md') : null;
|
|
240
324
|
const section = renderAgentsCommandsSection(mappings, baseline);
|
|
241
325
|
const agentsMdContent = buildAgentsMd(existingAgents, section);
|
|
242
326
|
|
|
243
327
|
let pkgManager: InitPlan['pkgManager'] = null;
|
|
244
|
-
|
|
245
|
-
|
|
328
|
+
const pmStack = primarySubpackage ? scanStack : stack;
|
|
329
|
+
const pmRoot = primarySubpackage ? scanRoot : projectRoot;
|
|
330
|
+
if (pmStack === 'php') pkgManager = 'composer';
|
|
331
|
+
else if (pmStack === 'node') pkgManager = detectNodePackageManager(pmRoot);
|
|
246
332
|
|
|
247
333
|
return {
|
|
248
334
|
projectRoot,
|
|
249
335
|
stack,
|
|
336
|
+
stackOverridden,
|
|
337
|
+
primarySubpackage,
|
|
250
338
|
pkgManager,
|
|
251
339
|
tools,
|
|
252
340
|
mappings,
|
|
@@ -255,6 +343,7 @@ export function planInit(projectRoot: string, opts: { baseline?: string } = {}):
|
|
|
255
343
|
agentsMdPath,
|
|
256
344
|
agentsMdContent,
|
|
257
345
|
baseline,
|
|
346
|
+
warnings,
|
|
258
347
|
};
|
|
259
348
|
}
|
|
260
349
|
|
|
@@ -272,10 +361,23 @@ export function renderPlanSummary(plan: InitPlan): string {
|
|
|
272
361
|
const lines: string[] = [];
|
|
273
362
|
lines.push(`# vdx init plan`);
|
|
274
363
|
lines.push('');
|
|
275
|
-
lines.push(
|
|
276
|
-
|
|
364
|
+
lines.push(
|
|
365
|
+
`- **Project**: ${plan.projectRoot}`,
|
|
366
|
+
);
|
|
367
|
+
lines.push(
|
|
368
|
+
`- **Stack**: ${plan.stack}${plan.stackOverridden ? ' (via --stack)' : ''}`,
|
|
369
|
+
);
|
|
370
|
+
if (plan.primarySubpackage) {
|
|
371
|
+
lines.push(`- **Primary subpackage**: ${plan.primarySubpackage}`);
|
|
372
|
+
}
|
|
277
373
|
if (plan.pkgManager) lines.push(`- **Package manager**: ${plan.pkgManager}`);
|
|
278
374
|
lines.push(`- **Baseline**: ${plan.baseline}`);
|
|
375
|
+
if (plan.warnings.length > 0) {
|
|
376
|
+
lines.push('');
|
|
377
|
+
lines.push('## ⚠ Warnings');
|
|
378
|
+
lines.push('');
|
|
379
|
+
for (const w of plan.warnings) lines.push(`- ${w}`);
|
|
380
|
+
}
|
|
279
381
|
if (Object.keys(plan.tools).length > 0) {
|
|
280
382
|
const toolStr = Object.entries(plan.tools)
|
|
281
383
|
.map(([k, v]) => `${k}=${v}`)
|
package/src/rubric.ts
CHANGED