@vodmal/vdx-cli 0.25.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 +2 -2
- package/package.json +8 -3
- package/rubric/vdx-rubric.yaml +45 -9
- package/src/ai.ts +22 -23
- package/src/clone-checks.ts +1 -1
- package/src/conversations.ts +30 -23
- package/src/doctor.ts +5 -4
- package/src/evaluator.ts +21 -20
- package/src/facts.ts +24 -49
- package/src/gha.ts +34 -26
- package/src/hooks.ts +31 -31
- package/src/index.ts +22 -13
- package/src/init.ts +9 -12
- package/src/manifest.ts +25 -4
- package/src/mcp-server.ts +39 -35
- package/src/narrow.ts +56 -0
- package/src/predicates.ts +10 -11
- package/src/publish.ts +27 -25
- package/src/rubric.ts +2 -2
- package/src/run.ts +3 -2
- package/src/vocabulary.ts +8 -12
package/src/facts.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 * as YAML from 'js-yaml';
|
|
5
|
+
import { at, dict, str } from './narrow.ts';
|
|
5
6
|
|
|
6
7
|
export interface Ctx {
|
|
7
8
|
projectRoot: string;
|
|
@@ -52,7 +53,7 @@ export function readText(ctx: Ctx, rel: string): string | null {
|
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
export function readJson(ctx: Ctx, rel: string):
|
|
56
|
+
export function readJson(ctx: Ctx, rel: string): unknown {
|
|
56
57
|
const key = `json:${rel}`;
|
|
57
58
|
if (ctx.cache.has(key)) return ctx.cache.get(key);
|
|
58
59
|
const text = readText(ctx, rel);
|
|
@@ -61,7 +62,7 @@ export function readJson(ctx: Ctx, rel: string): any {
|
|
|
61
62
|
return null;
|
|
62
63
|
}
|
|
63
64
|
try {
|
|
64
|
-
const data = JSON.parse(text);
|
|
65
|
+
const data: unknown = JSON.parse(text);
|
|
65
66
|
ctx.cache.set(key, data);
|
|
66
67
|
return data;
|
|
67
68
|
} catch {
|
|
@@ -70,7 +71,7 @@ export function readJson(ctx: Ctx, rel: string): any {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
export function readToml(ctx: Ctx, rel: string):
|
|
74
|
+
export function readToml(ctx: Ctx, rel: string): unknown {
|
|
74
75
|
const key = `toml:${rel}`;
|
|
75
76
|
if (ctx.cache.has(key)) return ctx.cache.get(key);
|
|
76
77
|
const text = readText(ctx, rel);
|
|
@@ -88,7 +89,7 @@ export function readToml(ctx: Ctx, rel: string): any {
|
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
export function readYaml(ctx: Ctx, rel: string):
|
|
92
|
+
export function readYaml(ctx: Ctx, rel: string): unknown {
|
|
92
93
|
const key = `yaml:${rel}`;
|
|
93
94
|
if (ctx.cache.has(key)) return ctx.cache.get(key);
|
|
94
95
|
const text = readText(ctx, rel);
|
|
@@ -107,7 +108,7 @@ export function readYaml(ctx: Ctx, rel: string): any {
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
/** Parse a structured file by extension. Returns parsed object/array or null. */
|
|
110
|
-
export function readStructured(ctx: Ctx, rel: string):
|
|
111
|
+
export function readStructured(ctx: Ctx, rel: string): unknown {
|
|
111
112
|
const lower = rel.toLowerCase();
|
|
112
113
|
if (lower.endsWith('.json')) return readJson(ctx, rel);
|
|
113
114
|
if (lower.endsWith('.toml')) return readToml(ctx, rel);
|
|
@@ -122,14 +123,7 @@ export function readStructured(ctx: Ctx, rel: string): any {
|
|
|
122
123
|
*/
|
|
123
124
|
export function resolveConfigPath(root: unknown, jsonpath: string): unknown {
|
|
124
125
|
if (root === null || root === undefined) return undefined;
|
|
125
|
-
|
|
126
|
-
let cur: any = root;
|
|
127
|
-
for (const part of parts) {
|
|
128
|
-
if (cur === null || cur === undefined) return undefined;
|
|
129
|
-
if (typeof cur !== 'object') return undefined;
|
|
130
|
-
cur = (cur as Record<string, unknown>)[part];
|
|
131
|
-
}
|
|
132
|
-
return cur;
|
|
126
|
+
return at(root, ...jsonpath.split('.'));
|
|
133
127
|
}
|
|
134
128
|
|
|
135
129
|
/**
|
|
@@ -142,25 +136,12 @@ export function listAllTasks(ctx: Ctx): Set<string> {
|
|
|
142
136
|
const tasks = new Set<string>();
|
|
143
137
|
const own = ownCtx(ctx);
|
|
144
138
|
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const composer = readJson(own, 'composer.json');
|
|
151
|
-
if (composer?.scripts && typeof composer.scripts === 'object') {
|
|
152
|
-
for (const k of Object.keys(composer.scripts)) tasks.add(k);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const pkg = readJson(own, 'package.json');
|
|
156
|
-
if (pkg?.scripts && typeof pkg.scripts === 'object') {
|
|
157
|
-
for (const k of Object.keys(pkg.scripts)) tasks.add(k);
|
|
158
|
-
}
|
|
159
|
-
|
|
139
|
+
for (const k of Object.keys(dict(at(readToml(own, 'mise.toml'), 'tasks')) ?? {})) tasks.add(k);
|
|
140
|
+
for (const k of Object.keys(dict(at(readJson(own, 'composer.json'), 'scripts')) ?? {})) tasks.add(k);
|
|
141
|
+
for (const k of Object.keys(dict(at(readJson(own, 'package.json'), 'scripts')) ?? {})) tasks.add(k);
|
|
160
142
|
// monorepo-ish fallback: server/package.json (см. bookmap)
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
for (const k of Object.keys(serverPkg.scripts)) tasks.add(`server:${k}`);
|
|
143
|
+
for (const k of Object.keys(dict(at(readJson(own, 'server/package.json'), 'scripts')) ?? {})) {
|
|
144
|
+
tasks.add(`server:${k}`);
|
|
164
145
|
}
|
|
165
146
|
|
|
166
147
|
const make = readText(own, 'Makefile');
|
|
@@ -180,18 +161,14 @@ export function isPackagePresent(name: string, ecosystem: string, ctx: Ctx): boo
|
|
|
180
161
|
const eco = ecosystem ?? 'any';
|
|
181
162
|
if (eco === 'any' || eco === 'composer') {
|
|
182
163
|
const composer = readJson(ctx, 'composer.json');
|
|
183
|
-
if (composer)
|
|
184
|
-
|
|
185
|
-
if (composer['require-dev']?.[name]) return true;
|
|
186
|
-
}
|
|
164
|
+
if (at(composer, 'require', name)) return true;
|
|
165
|
+
if (at(composer, 'require-dev', name)) return true;
|
|
187
166
|
}
|
|
188
167
|
if (eco === 'any' || eco === 'npm') {
|
|
189
168
|
for (const rel of ['package.json', 'server/package.json']) {
|
|
190
169
|
const pkg = readJson(ctx, rel);
|
|
191
|
-
if (pkg)
|
|
192
|
-
|
|
193
|
-
if (pkg.devDependencies?.[name]) return true;
|
|
194
|
-
}
|
|
170
|
+
if (at(pkg, 'dependencies', name)) return true;
|
|
171
|
+
if (at(pkg, 'devDependencies', name)) return true;
|
|
195
172
|
}
|
|
196
173
|
}
|
|
197
174
|
return false;
|
|
@@ -222,19 +199,17 @@ export function expandPaths(paths: string[], ctx: Ctx): string[] {
|
|
|
222
199
|
return out;
|
|
223
200
|
}
|
|
224
201
|
|
|
225
|
-
export function readTsconfig(ctx: Ctx):
|
|
226
|
-
|
|
227
|
-
if (!cfg) cfg = readJson(ctx, 'server/tsconfig.json');
|
|
202
|
+
export function readTsconfig(ctx: Ctx): { compilerOptions?: Record<string, unknown> } | null {
|
|
203
|
+
const cfg = dict(readJson(ctx, 'tsconfig.json')) ?? dict(readJson(ctx, 'server/tsconfig.json'));
|
|
228
204
|
if (!cfg) return null;
|
|
205
|
+
const own = dict(cfg['compilerOptions']);
|
|
229
206
|
// basic one-level `extends` resolution
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const base = readJson(ctx,
|
|
233
|
-
if (base
|
|
234
|
-
cfg.compilerOptions = { ...base.compilerOptions, ...(cfg.compilerOptions ?? {}) };
|
|
235
|
-
}
|
|
207
|
+
const ext = str(cfg['extends']);
|
|
208
|
+
if (ext?.startsWith('.')) {
|
|
209
|
+
const base = dict(at(readJson(ctx, ext.endsWith('.json') ? ext : `${ext}.json`), 'compilerOptions'));
|
|
210
|
+
if (base) return { compilerOptions: { ...base, ...own } };
|
|
236
211
|
}
|
|
237
|
-
return
|
|
212
|
+
return own ? { compilerOptions: own } : {};
|
|
238
213
|
}
|
|
239
214
|
|
|
240
215
|
const MANIFEST_TO_STACK: Array<[string, string]> = [
|
package/src/gha.ts
CHANGED
|
@@ -8,6 +8,7 @@ import * as fs from 'node:fs';
|
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import YAML from 'js-yaml';
|
|
10
10
|
import { fileExists, readText, type Ctx } from './facts.ts';
|
|
11
|
+
import { at, dict, isDict, list, str } from './narrow.ts';
|
|
11
12
|
import { projectTaskCalls } from './vocabulary.ts';
|
|
12
13
|
|
|
13
14
|
export interface GhaStep {
|
|
@@ -48,9 +49,8 @@ function filterMatches(pattern: string, branch: string): boolean {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
function branchSelected(filters: unknown, branch: string): boolean {
|
|
51
|
-
const list = Array.isArray(filters) ? filters : [filters];
|
|
52
52
|
let selected = false;
|
|
53
|
-
for (const f of
|
|
53
|
+
for (const f of Array.isArray(filters) ? (filters as unknown[]) : [filters]) {
|
|
54
54
|
if (typeof f !== 'string') continue;
|
|
55
55
|
if (f.startsWith('!')) {
|
|
56
56
|
if (filterMatches(f.slice(1), branch)) selected = false;
|
|
@@ -65,15 +65,16 @@ function branchSelected(filters: unknown, branch: string): boolean {
|
|
|
65
65
|
export function pushesMain(on: unknown): boolean {
|
|
66
66
|
if (on === 'push') return true;
|
|
67
67
|
if (Array.isArray(on)) return on.includes('push');
|
|
68
|
-
if (!on ||
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
68
|
+
if (!isDict(on) || !('push' in on)) return false;
|
|
69
|
+
const push = dict(on['push']);
|
|
70
|
+
if (!push) return true;
|
|
71
|
+
const { branches, tags } = push;
|
|
72
|
+
const branchesIgnore = push['branches-ignore'];
|
|
73
|
+
const hasBranchFilter = branches !== undefined || branchesIgnore !== undefined;
|
|
74
|
+
if (!hasBranchFilter && (tags !== undefined || push['tags-ignore'] !== undefined)) return false;
|
|
74
75
|
return MAIN_BRANCHES.some((b) => {
|
|
75
|
-
if (
|
|
76
|
-
if (
|
|
76
|
+
if (branches !== undefined) return branchSelected(branches, b);
|
|
77
|
+
if (branchesIgnore !== undefined) return !branchSelected(branchesIgnore, b);
|
|
77
78
|
return true;
|
|
78
79
|
});
|
|
79
80
|
}
|
|
@@ -83,34 +84,41 @@ function truthy(v: unknown): boolean {
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
export function parseWorkflow(file: string, text: string): GhaWorkflow | null {
|
|
86
|
-
let data:
|
|
87
|
+
let data: unknown;
|
|
87
88
|
try {
|
|
88
89
|
data = YAML.load(text);
|
|
89
90
|
} catch {
|
|
90
91
|
return null;
|
|
91
92
|
}
|
|
93
|
+
// An array is an object too, and the workflow it would be has no jobs.
|
|
92
94
|
if (!data || typeof data !== 'object') return null;
|
|
93
95
|
const jobs: GhaJob[] = [];
|
|
94
|
-
for (const [id,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
for (const [id, value] of Object.entries(dict(at(data, 'jobs')) ?? {})) {
|
|
97
|
+
const job = dict(value);
|
|
98
|
+
if (!job) continue;
|
|
99
|
+
const needs = job['needs'] === undefined ? [] : Array.isArray(job['needs']) ? list(job['needs']) : [job['needs']];
|
|
100
|
+
const steps: GhaStep[] = list(job['steps'])
|
|
101
|
+
.filter(isDict)
|
|
102
|
+
.map((s) => {
|
|
103
|
+
const run = str(s['run']);
|
|
104
|
+
const uses = str(s['uses']);
|
|
105
|
+
return {
|
|
106
|
+
...(run !== undefined ? { run } : {}),
|
|
107
|
+
...(uses !== undefined ? { uses } : {}),
|
|
108
|
+
continueOnError: truthy(s['continue-on-error']),
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
const name = str(job['name']);
|
|
104
112
|
jobs.push({
|
|
105
113
|
id,
|
|
106
|
-
...(
|
|
107
|
-
needs: needs.filter((n
|
|
114
|
+
...(name !== undefined ? { name } : {}),
|
|
115
|
+
needs: needs.filter((n): n is string => typeof n === 'string'),
|
|
108
116
|
steps,
|
|
109
117
|
continueOnError: truthy(job['continue-on-error']),
|
|
110
|
-
matrix: Boolean(job
|
|
118
|
+
matrix: Boolean(at(job, 'strategy', 'matrix')),
|
|
111
119
|
});
|
|
112
120
|
}
|
|
113
|
-
return { file, pushesMain: pushesMain(data
|
|
121
|
+
return { file, pushesMain: pushesMain(at(data, 'on')), jobs };
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
export function readWorkflows(ctx: Ctx): GhaWorkflow[] {
|
|
@@ -118,7 +126,7 @@ export function readWorkflows(ctx: Ctx): GhaWorkflow[] {
|
|
|
118
126
|
if (ctx.cache.has(key)) return ctx.cache.get(key) as GhaWorkflow[];
|
|
119
127
|
const dir = path.join(ctx.projectRoot, '.github', 'workflows');
|
|
120
128
|
const out: GhaWorkflow[] = [];
|
|
121
|
-
let files: string[]
|
|
129
|
+
let files: string[];
|
|
122
130
|
try {
|
|
123
131
|
files = fs
|
|
124
132
|
.readdirSync(dir)
|
package/src/hooks.ts
CHANGED
|
@@ -16,6 +16,7 @@ import * as path from 'node:path';
|
|
|
16
16
|
import { execFileSync } from 'node:child_process';
|
|
17
17
|
import YAML from 'js-yaml';
|
|
18
18
|
import { fileExists, isPackagePresent, readJson, readText, readToml, type Ctx } from './facts.ts';
|
|
19
|
+
import { at, dict, isDict, list } from './narrow.ts';
|
|
19
20
|
|
|
20
21
|
export type FrameworkId = 'husky' | 'lefthook' | 'simple-git-hooks' | 'cghooks' | 'githooks';
|
|
21
22
|
|
|
@@ -69,7 +70,7 @@ interface InstallStep {
|
|
|
69
70
|
|
|
70
71
|
/** Yarn 2+ does not run a project's `prepare` (husky docs, How To → Yarn). */
|
|
71
72
|
export function yarnBerry(ctx: Ctx): boolean {
|
|
72
|
-
const pm = readJson(ctx, 'package.json')
|
|
73
|
+
const pm = at(readJson(ctx, 'package.json'), 'packageManager');
|
|
73
74
|
if (typeof pm === 'string') {
|
|
74
75
|
const m = pm.match(/^yarn@(\d+)/);
|
|
75
76
|
if (m) return parseInt(m[1]!, 10) >= 2;
|
|
@@ -79,22 +80,24 @@ export function yarnBerry(ctx: Ctx): boolean {
|
|
|
79
80
|
|
|
80
81
|
function installSteps(ctx: Ctx): InstallStep[] {
|
|
81
82
|
const out: InstallStep[] = [];
|
|
82
|
-
const
|
|
83
|
-
const scripts = pkg?.scripts ?? {};
|
|
83
|
+
const scripts = dict(at(readJson(ctx, 'package.json'), 'scripts')) ?? {};
|
|
84
84
|
const npmHooks = yarnBerry(ctx) ? ['postinstall'] : ['prepare', 'postinstall'];
|
|
85
85
|
for (const name of npmHooks) {
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
const body = scripts[name];
|
|
87
|
+
if (typeof body === 'string') {
|
|
88
|
+
out.push({ where: `package.json ${name}`, text: expandNpm(scripts, body) });
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
|
-
const
|
|
91
|
-
const cmd =
|
|
91
|
+
const composerScripts = dict(at(readJson(ctx, 'composer.json'), 'scripts')) ?? {};
|
|
92
|
+
const cmd = composerScripts['post-install-cmd'];
|
|
92
93
|
if (cmd !== undefined) {
|
|
93
|
-
out.push({ where: 'composer post-install-cmd', text: expandComposer(
|
|
94
|
+
out.push({ where: 'composer post-install-cmd', text: expandComposer(composerScripts, cmd) });
|
|
94
95
|
}
|
|
95
|
-
const up = readToml(ctx, 'mise.toml')
|
|
96
|
-
if (up
|
|
97
|
-
const runs = (Array.isArray(up
|
|
96
|
+
const up = dict(at(readToml(ctx, 'mise.toml'), 'tasks', 'up'));
|
|
97
|
+
if (up) {
|
|
98
|
+
const runs = (Array.isArray(up['run']) ? (up['run'] as unknown[]) : [up['run']]).filter(
|
|
99
|
+
(r): r is string => typeof r === 'string',
|
|
100
|
+
);
|
|
98
101
|
if (runs.length > 0) out.push({ where: 'mise task up', text: runs.join('\n') });
|
|
99
102
|
}
|
|
100
103
|
return out;
|
|
@@ -135,7 +138,7 @@ function findStep(steps: InstallStep[], re: RegExp): string | null {
|
|
|
135
138
|
/** Hook scripts in a directory: file name = event; the body is the command. */
|
|
136
139
|
function hooksFromDir(ctx: Ctx, dir: string): Map<string, string[]> {
|
|
137
140
|
const out = new Map<string, string[]>();
|
|
138
|
-
let names: string[]
|
|
141
|
+
let names: string[];
|
|
139
142
|
try {
|
|
140
143
|
names = fs.readdirSync(path.join(ctx.projectRoot, dir));
|
|
141
144
|
} catch {
|
|
@@ -162,15 +165,14 @@ function scriptBody(text: string): string {
|
|
|
162
165
|
|
|
163
166
|
function lefthookRuns(node: unknown): string[] {
|
|
164
167
|
const out: string[] = [];
|
|
165
|
-
if (!node
|
|
166
|
-
|
|
167
|
-
if (typeof
|
|
168
|
-
if (typeof o.runner === 'string') out.push(o.runner);
|
|
168
|
+
if (!isDict(node)) return out;
|
|
169
|
+
if (typeof node['run'] === 'string') out.push(node['run']);
|
|
170
|
+
if (typeof node['runner'] === 'string') out.push(node['runner']);
|
|
169
171
|
for (const key of ['commands', 'scripts']) {
|
|
170
|
-
|
|
172
|
+
for (const v of Object.values(dict(node[key]) ?? {})) out.push(...lefthookRuns(v));
|
|
171
173
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
+
for (const j of list(node['jobs'])) out.push(...lefthookRuns(j));
|
|
175
|
+
out.push(...lefthookRuns(node['group']));
|
|
174
176
|
return out;
|
|
175
177
|
}
|
|
176
178
|
|
|
@@ -204,14 +206,14 @@ export function detectHookFrameworks(ctx: Ctx): HookFramework[] {
|
|
|
204
206
|
for (const file of LEFTHOOK_FILES) {
|
|
205
207
|
const text = readText(ctx, file);
|
|
206
208
|
if (text === null) continue;
|
|
207
|
-
let data:
|
|
209
|
+
let data: unknown;
|
|
208
210
|
try {
|
|
209
211
|
data = YAML.load(text);
|
|
210
212
|
} catch {
|
|
211
213
|
continue;
|
|
212
214
|
}
|
|
213
215
|
const hooks = new Map<string, string[]>();
|
|
214
|
-
for (const [event, def] of Object.entries((data ?? {})
|
|
216
|
+
for (const [event, def] of Object.entries(dict(data) ?? {})) {
|
|
215
217
|
if (GIT_HOOK_EVENTS.has(event)) hooks.set(event, lefthookRuns(def));
|
|
216
218
|
}
|
|
217
219
|
if (hooks.size === 0) continue;
|
|
@@ -231,11 +233,9 @@ export function detectHookFrameworks(ctx: Ctx): HookFramework[] {
|
|
|
231
233
|
break;
|
|
232
234
|
}
|
|
233
235
|
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
readJson(ctx, 'simple-git-hooks.json');
|
|
238
|
-
if (sgh && typeof sgh === 'object') {
|
|
236
|
+
const sghInPackage = dict(at(pkg, 'simple-git-hooks'));
|
|
237
|
+
const sgh = sghInPackage ?? dict(readJson(ctx, '.simple-git-hooks.json') ?? readJson(ctx, 'simple-git-hooks.json'));
|
|
238
|
+
if (sgh) {
|
|
239
239
|
const hooks = new Map<string, string[]>();
|
|
240
240
|
for (const [event, cmd] of Object.entries(sgh)) {
|
|
241
241
|
if (GIT_HOOK_EVENTS.has(event) && typeof cmd === 'string') hooks.set(event, [cmd]);
|
|
@@ -243,7 +243,7 @@ export function detectHookFrameworks(ctx: Ctx): HookFramework[] {
|
|
|
243
243
|
if (hooks.size > 0) {
|
|
244
244
|
out.push({
|
|
245
245
|
id: 'simple-git-hooks',
|
|
246
|
-
declaredIn:
|
|
246
|
+
declaredIn: sghInPackage ? 'package.json simple-git-hooks' : 'simple-git-hooks.json',
|
|
247
247
|
hooks,
|
|
248
248
|
installedBy: isPackagePresent('simple-git-hooks', 'npm', ctx)
|
|
249
249
|
? 'simple-git-hooks package (its postinstall)'
|
|
@@ -255,10 +255,10 @@ export function detectHookFrameworks(ctx: Ctx): HookFramework[] {
|
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
const extra = composer
|
|
259
|
-
if (extra
|
|
258
|
+
const extra = dict(at(composer, 'extra', 'hooks'));
|
|
259
|
+
if (extra) {
|
|
260
260
|
const hooks = new Map<string, string[]>();
|
|
261
|
-
for (const [event, cmd] of Object.entries(extra
|
|
261
|
+
for (const [event, cmd] of Object.entries(extra)) {
|
|
262
262
|
if (!GIT_HOOK_EVENTS.has(event)) continue;
|
|
263
263
|
const list = (Array.isArray(cmd) ? cmd : [cmd]).filter((c): c is string => typeof c === 'string');
|
|
264
264
|
if (list.length > 0) hooks.set(event, list);
|
|
@@ -301,7 +301,7 @@ export function installCommand(fw: HookFramework, ctx: Ctx): string {
|
|
|
301
301
|
if (fw.installedBy === null) return fw.manualInstall;
|
|
302
302
|
if (fw.installedBy.startsWith('composer')) return 'composer install';
|
|
303
303
|
if (fw.installedBy === 'mise task up') return 'vdx up';
|
|
304
|
-
const pm = readJson(ctx, 'package.json')
|
|
304
|
+
const pm = at(readJson(ctx, 'package.json'), 'packageManager');
|
|
305
305
|
if (typeof pm === 'string' && pm.startsWith('pnpm')) return 'pnpm install';
|
|
306
306
|
if ((typeof pm === 'string' && pm.startsWith('yarn')) || fileExists(ctx, 'yarn.lock')) return 'yarn install';
|
|
307
307
|
if (fileExists(ctx, 'pnpm-lock.yaml')) return 'pnpm install';
|
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'],
|
|
@@ -225,14 +226,10 @@ export function mapVerb(verb: Verb, tasks: Set<string>): string | null {
|
|
|
225
226
|
|
|
226
227
|
function collectTools(ctx: Ctx): Record<string, string> {
|
|
227
228
|
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
|
-
}
|
|
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);
|
|
236
233
|
return tools;
|
|
237
234
|
}
|
|
238
235
|
|
|
@@ -264,7 +261,7 @@ export function renderMiseToml(
|
|
|
264
261
|
primarySubpackage: string | null = null,
|
|
265
262
|
): string {
|
|
266
263
|
const out: string[] = [];
|
|
267
|
-
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.');
|
|
268
265
|
out.push('# Lifecycle verbs follow the vdx contract: up/down/build/test/check/fix.');
|
|
269
266
|
out.push('');
|
|
270
267
|
|
|
@@ -294,7 +291,7 @@ export function renderMiseToml(
|
|
|
294
291
|
out.push('');
|
|
295
292
|
}
|
|
296
293
|
|
|
297
|
-
out.push('[vdx]');
|
|
294
|
+
out.push('[_.vdx]');
|
|
298
295
|
out.push('schema_version = "0.2"');
|
|
299
296
|
out.push(`baseline = ${quoteToml(baseline)}`);
|
|
300
297
|
out.push(`stack = ${quoteToml(stack)}`);
|
|
@@ -337,7 +334,7 @@ export function planInit(projectRoot: string, opts: { baseline?: string; stack?:
|
|
|
337
334
|
const list = subs.map((s) => `${s.relPath}(${s.stack})`).join(', ');
|
|
338
335
|
warnings.push(
|
|
339
336
|
`stack="meta", найдено несколько subpackages: ${list}. ` +
|
|
340
|
-
'primary_subpackage не выбран; укажи его руками в [vdx]
|
|
337
|
+
'primary_subpackage не выбран; укажи его руками в блоке [_.vdx].',
|
|
341
338
|
);
|
|
342
339
|
}
|
|
343
340
|
} 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 [];
|