@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/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): any {
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): any {
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): any {
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): any {
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
- const parts = jsonpath.split('.');
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 miseT = readToml(own, 'mise.toml');
146
- if (miseT?.tasks && typeof miseT.tasks === 'object') {
147
- for (const k of Object.keys(miseT.tasks as object)) tasks.add(k);
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 serverPkg = readJson(own, 'server/package.json');
162
- if (serverPkg?.scripts && typeof serverPkg.scripts === 'object') {
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
- if (composer.require?.[name]) return true;
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
- if (pkg.dependencies?.[name]) return true;
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): any {
226
- let cfg = readJson(ctx, 'tsconfig.json');
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
- if (cfg.extends && typeof cfg.extends === 'string' && cfg.extends.startsWith('.')) {
231
- const baseRel = cfg.extends.endsWith('.json') ? cfg.extends : `${cfg.extends}.json`;
232
- const base = readJson(ctx, baseRel);
233
- if (base?.compilerOptions) {
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 cfg;
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 list) {
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 || typeof on !== 'object') return false;
69
- if (!('push' in on)) return false;
70
- const push = (on as Record<string, any>).push;
71
- if (push === null || push === undefined || typeof push !== 'object') return true;
72
- const hasBranchFilter = push.branches !== undefined || push['branches-ignore'] !== undefined;
73
- if (!hasBranchFilter && (push.tags !== undefined || push['tags-ignore'] !== undefined)) return false;
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 (push.branches !== undefined) return branchSelected(push.branches, b);
76
- if (push['branches-ignore'] !== undefined) return !branchSelected(push['branches-ignore'], b);
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: any;
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, job] of Object.entries((data.jobs ?? {}) as Record<string, any>)) {
95
- if (!job || typeof job !== 'object') continue;
96
- const needs = job.needs === undefined ? [] : Array.isArray(job.needs) ? job.needs : [job.needs];
97
- const steps: GhaStep[] = (Array.isArray(job.steps) ? job.steps : [])
98
- .filter((s: unknown) => s && typeof s === 'object')
99
- .map((s: any) => ({
100
- ...(typeof s.run === 'string' ? { run: s.run } : {}),
101
- ...(typeof s.uses === 'string' ? { uses: s.uses } : {}),
102
- continueOnError: truthy(s['continue-on-error']),
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
- ...(typeof job.name === 'string' ? { name: job.name } : {}),
107
- needs: needs.filter((n: unknown): n is string => typeof n === 'string'),
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.strategy?.matrix),
118
+ matrix: Boolean(at(job, 'strategy', 'matrix')),
111
119
  });
112
120
  }
113
- return { file, pushesMain: pushesMain(data.on), jobs };
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')?.packageManager;
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 pkg = readJson(ctx, 'package.json');
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
- if (typeof scripts[name] === 'string') {
87
- out.push({ where: `package.json ${name}`, text: expandNpm(scripts, scripts[name]) });
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 composer = readJson(ctx, 'composer.json');
91
- const cmd = composer?.scripts?.['post-install-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(composer.scripts, cmd) });
94
+ out.push({ where: 'composer post-install-cmd', text: expandComposer(composerScripts, cmd) });
94
95
  }
95
- const up = readToml(ctx, 'mise.toml')?.tasks?.up;
96
- if (up && typeof up === 'object') {
97
- const runs = (Array.isArray(up.run) ? up.run : [up.run]).filter((r: unknown) => typeof r === 'string');
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 || typeof node !== 'object') return out;
166
- const o = node as Record<string, any>;
167
- if (typeof o.run === 'string') out.push(o.run);
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
- if (o[key] && typeof o[key] === 'object') for (const v of Object.values(o[key])) out.push(...lefthookRuns(v));
172
+ for (const v of Object.values(dict(node[key]) ?? {})) out.push(...lefthookRuns(v));
171
173
  }
172
- if (Array.isArray(o.jobs)) for (const j of o.jobs) out.push(...lefthookRuns(j));
173
- if (o.group && typeof o.group === 'object') out.push(...lefthookRuns(o.group));
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: any;
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 ?? {}) as Record<string, unknown>)) {
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 sgh =
235
- (pkg?.['simple-git-hooks'] as Record<string, unknown> | undefined) ??
236
- readJson(ctx, '.simple-git-hooks.json') ??
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: pkg?.['simple-git-hooks'] ? 'package.json simple-git-hooks' : 'simple-git-hooks.json',
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?.extra?.hooks;
259
- if (extra && typeof extra === 'object') {
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 as Record<string, unknown>)) {
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')?.packageManager;
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 manifest = loadManifest(projectRoot);
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: any) {
350
- process.stderr.write(`\nerror: ${e?.message ?? String(e)}\n`);
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: any) {
384
- process.stderr.write(`error: ${e?.message ?? String(e)}\n`);
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: any) {
422
- process.stderr.write(`\nerror: ${e?.message ?? String(e)}\n`);
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: any) {
438
- if (e?.code === 'ENOENT') {
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
- process.exit(typeof e?.status === 'number' ? e.status : 1);
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: any) {
466
- process.stderr.write(`vdx doctor --fix: ${e?.message ?? String(e)}\n`);
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.0';
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 composer = readJson(ctx, 'composer.json');
229
- if (composer?.require?.php && typeof composer.require.php === 'string') {
230
- tools.php = stripVersionConstraint(composer.require.php);
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
- export function loadManifest(projectRoot: string): VdxManifest | null {
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 text = fs.readFileSync(miseToml, 'utf8');
37
- const data = TOML.parse(text) as { vdx?: VdxManifest };
38
- return data?.vdx ?? null;
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 [];