@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/mcp-server.ts
CHANGED
|
@@ -8,12 +8,13 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
8
8
|
import * as TOML from 'smol-toml';
|
|
9
9
|
|
|
10
10
|
import { loadRubric } from './rubric.ts';
|
|
11
|
-
import { loadManifest, loadOverrides } from './manifest.ts';
|
|
11
|
+
import { loadManifest, loadOverrides, manifestTable } from './manifest.ts';
|
|
12
12
|
import { autoDetectStack, readToml, type Ctx } from './facts.ts';
|
|
13
13
|
import { audit } from './audit.ts';
|
|
14
14
|
import { reportJson, reportMarkdown } from './report.ts';
|
|
15
15
|
import { DEFAULT_BASELINE, STANDARD_VERBS, type Verb } from './init.ts';
|
|
16
16
|
import { resolveDefaultRubric } from './defaults.ts';
|
|
17
|
+
import { at, dict, errorMessage, isStringList, str, type Dict } from './narrow.ts';
|
|
17
18
|
|
|
18
19
|
const DEFAULT_RUBRIC = resolveDefaultRubric();
|
|
19
20
|
|
|
@@ -30,18 +31,16 @@ function freshCtx(): Ctx {
|
|
|
30
31
|
return { projectRoot: PROJECT_ROOT, stack, cache: new Map() };
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
function readMiseToml():
|
|
34
|
+
function readMiseToml(): unknown {
|
|
34
35
|
return readToml(freshCtx(), 'mise.toml');
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
function getVdxBlock():
|
|
38
|
-
|
|
39
|
-
return t?.vdx ?? null;
|
|
38
|
+
function getVdxBlock(): Dict | null {
|
|
39
|
+
return manifestTable(readMiseToml())?.table ?? null;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function getTasks():
|
|
43
|
-
|
|
44
|
-
return (t?.tasks as Record<string, any>) ?? {};
|
|
42
|
+
function getTasks(): Dict {
|
|
43
|
+
return dict(at(readMiseToml(), 'tasks')) ?? {};
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
interface RunResult {
|
|
@@ -61,10 +60,10 @@ function spawnVerb(verb: Verb, extraArgs: string[]): Promise<RunResult> {
|
|
|
61
60
|
});
|
|
62
61
|
let stdout = '';
|
|
63
62
|
let stderr = '';
|
|
64
|
-
child.stdout.on('data', (b) => {
|
|
63
|
+
child.stdout.on('data', (b: Buffer) => {
|
|
65
64
|
stdout += b.toString();
|
|
66
65
|
});
|
|
67
|
-
child.stderr.on('data', (b) => {
|
|
66
|
+
child.stderr.on('data', (b: Buffer) => {
|
|
68
67
|
stderr += b.toString();
|
|
69
68
|
});
|
|
70
69
|
child.on('error', (e) => {
|
|
@@ -91,32 +90,33 @@ function spawnVerb(verb: Verb, extraArgs: string[]): Promise<RunResult> {
|
|
|
91
90
|
function buildCapabilities() {
|
|
92
91
|
const vdxBlock = getVdxBlock();
|
|
93
92
|
const tasks = getTasks();
|
|
94
|
-
const
|
|
93
|
+
const declared = vdxBlock?.['verbs'];
|
|
94
|
+
const verbsList = isStringList(declared) ? declared : STANDARD_VERBS.slice();
|
|
95
95
|
const verbs = verbsList.map((name) => {
|
|
96
96
|
const t = tasks[name];
|
|
97
|
-
const nativeCommand = t && typeof t === 'object' && typeof t.run === 'string' ? t.run : null;
|
|
98
97
|
return {
|
|
99
98
|
name,
|
|
100
99
|
has_manifest: Boolean(t),
|
|
101
|
-
native_command:
|
|
100
|
+
native_command: str(at(t, 'run')) ?? null,
|
|
102
101
|
};
|
|
103
102
|
});
|
|
104
103
|
|
|
105
|
-
const
|
|
104
|
+
const infra = dict(vdxBlock?.['shared_infra']);
|
|
105
|
+
const sharedInfra = infra
|
|
106
106
|
? {
|
|
107
|
-
provider:
|
|
108
|
-
precheck: Boolean(
|
|
107
|
+
provider: str(infra['provider']) ?? '',
|
|
108
|
+
precheck: Boolean(infra['precheck']),
|
|
109
109
|
is_running: false,
|
|
110
110
|
}
|
|
111
111
|
: null;
|
|
112
112
|
|
|
113
113
|
return {
|
|
114
|
-
schema_version:
|
|
115
|
-
stack:
|
|
116
|
-
baseline:
|
|
114
|
+
schema_version: str(vdxBlock?.['schema_version']) ?? '0.2',
|
|
115
|
+
stack: str(vdxBlock?.['stack']) ?? autoDetectStack(PROJECT_ROOT),
|
|
116
|
+
baseline: str(vdxBlock?.['baseline']) ?? `file://${DEFAULT_RUBRIC}`,
|
|
117
117
|
verbs,
|
|
118
118
|
shared_infra: sharedInfra,
|
|
119
|
-
last_verified: vdxBlock
|
|
119
|
+
last_verified: at(vdxBlock, 'success_path', 'last_verified') ?? null,
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
|
|
@@ -138,40 +138,44 @@ function recordSuccessPath(input: {
|
|
|
138
138
|
notes?: string;
|
|
139
139
|
}): { ok: boolean; manifest_path: string; agents_md_updated: boolean } {
|
|
140
140
|
const manifestPath = path.join(PROJECT_ROOT, 'mise.toml');
|
|
141
|
-
let doc:
|
|
141
|
+
let doc: Dict = {};
|
|
142
142
|
if (fs.existsSync(manifestPath)) {
|
|
143
143
|
const text = fs.readFileSync(manifestPath, 'utf8');
|
|
144
144
|
try {
|
|
145
145
|
doc = TOML.parse(text);
|
|
146
|
-
} catch (e
|
|
147
|
-
throw new Error(`mise.toml parse error: ${e
|
|
146
|
+
} catch (e) {
|
|
147
|
+
throw new Error(`mise.toml parse error: ${errorMessage(e)}`, { cause: e });
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
const tasks = dict(doc['tasks']) ?? {};
|
|
152
152
|
for (const v of input.verbs) {
|
|
153
|
-
|
|
153
|
+
tasks[v.name] = { run: v.command };
|
|
154
154
|
}
|
|
155
|
+
doc['tasks'] = tasks;
|
|
155
156
|
|
|
156
|
-
|
|
157
|
+
// An existing manifest is updated where it is; a new one goes into [_.vdx].
|
|
158
|
+
const found = manifestTable(doc);
|
|
159
|
+
const vdx = found?.table ?? {
|
|
157
160
|
schema_version: '0.2',
|
|
158
161
|
baseline: DEFAULT_BASELINE,
|
|
159
162
|
stack: autoDetectStack(PROJECT_ROOT),
|
|
160
163
|
};
|
|
161
|
-
|
|
164
|
+
vdx['verbs'] = input.verbs.map((v) => v.name);
|
|
162
165
|
|
|
163
166
|
if (input.shared_infra) {
|
|
164
|
-
|
|
167
|
+
vdx['shared_infra'] = {
|
|
165
168
|
provider: input.shared_infra.provider,
|
|
166
169
|
precheck: input.shared_infra.precheck,
|
|
167
170
|
};
|
|
168
171
|
}
|
|
169
172
|
|
|
170
|
-
|
|
173
|
+
vdx['success_path'] = {
|
|
171
174
|
last_verified: new Date().toISOString().slice(0, 10),
|
|
172
175
|
verified_by: 'vdx-mcp',
|
|
173
176
|
...(input.notes ? { notes: input.notes } : {}),
|
|
174
177
|
};
|
|
178
|
+
if (!found) doc['_'] = { ...dict(doc['_']), vdx };
|
|
175
179
|
|
|
176
180
|
const newText = TOML.stringify(doc);
|
|
177
181
|
fs.writeFileSync(manifestPath, newText, 'utf8');
|
|
@@ -191,7 +195,7 @@ server.registerTool(
|
|
|
191
195
|
'Возвращает manifest проекта: stack, baseline, доступные глаголы и их native команды, shared_infra и last_verified. Самый частый вызов агента, входящего в проект.',
|
|
192
196
|
inputSchema: {},
|
|
193
197
|
},
|
|
194
|
-
|
|
198
|
+
() => {
|
|
195
199
|
const payload = buildCapabilities();
|
|
196
200
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
197
201
|
},
|
|
@@ -221,7 +225,7 @@ server.registerTool(
|
|
|
221
225
|
format: z.enum(['json', 'markdown']).optional(),
|
|
222
226
|
},
|
|
223
227
|
},
|
|
224
|
-
|
|
228
|
+
({ baseline, format }) => {
|
|
225
229
|
const fmt = format ?? 'json';
|
|
226
230
|
const payload = buildAuditPayload(baseline, fmt);
|
|
227
231
|
return { content: [{ type: 'text', text: payload }] };
|
|
@@ -232,14 +236,14 @@ server.registerTool(
|
|
|
232
236
|
'vdx_record_success_path',
|
|
233
237
|
{
|
|
234
238
|
description:
|
|
235
|
-
'Записывает обнаруженные команды в [tasks.X] секцию mise.toml + обновляет [vdx.success_path]. Используется после успешной верификации команд агентом.',
|
|
239
|
+
'Записывает обнаруженные команды в [tasks.X] секцию mise.toml + обновляет [_.vdx.success_path] (у манифеста в старом [vdx] — там). Используется после успешной верификации команд агентом.',
|
|
236
240
|
inputSchema: {
|
|
237
241
|
verbs: z.array(z.object({ name: z.string(), command: z.string() })),
|
|
238
242
|
shared_infra: z.object({ provider: z.string(), precheck: z.boolean() }).optional(),
|
|
239
243
|
notes: z.string().optional(),
|
|
240
244
|
},
|
|
241
245
|
},
|
|
242
|
-
|
|
246
|
+
({ verbs, shared_infra, notes }) => {
|
|
243
247
|
const args: Parameters<typeof recordSuccessPath>[0] = { verbs };
|
|
244
248
|
if (shared_infra) args.shared_infra = shared_infra;
|
|
245
249
|
if (notes !== undefined) args.notes = notes;
|
|
@@ -254,7 +258,7 @@ async function main() {
|
|
|
254
258
|
process.stderr.write(`vdx-mcp connected (project: ${PROJECT_ROOT})\n`);
|
|
255
259
|
}
|
|
256
260
|
|
|
257
|
-
main().catch((e) => {
|
|
258
|
-
process.stderr.write(`vdx-mcp fatal: ${e
|
|
261
|
+
main().catch((e: unknown) => {
|
|
262
|
+
process.stderr.write(`vdx-mcp fatal: ${errorMessage(e)}\n`);
|
|
259
263
|
process.exit(1);
|
|
260
264
|
});
|
package/src/narrow.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a parsed file (JSON, YAML, TOML) or a caught error holds is `unknown` until checked.
|
|
3
|
+
* These helpers check it on the way in, so the code past them works with real types.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type Dict = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
/** A plain object — not null, not an array. */
|
|
9
|
+
export function isDict(v: unknown): v is Dict {
|
|
10
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** `v` when it is a plain object, else undefined. */
|
|
14
|
+
export function dict(v: unknown): Dict | undefined {
|
|
15
|
+
return isDict(v) ? v : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The value under `keys`, one level per key — an object's property or an array's index ("0");
|
|
20
|
+
* undefined as soon as a level is not an object.
|
|
21
|
+
*/
|
|
22
|
+
export function at(v: unknown, ...keys: string[]): unknown {
|
|
23
|
+
let cur = v;
|
|
24
|
+
for (const k of keys) {
|
|
25
|
+
if (typeof cur !== 'object' || cur === null) return undefined;
|
|
26
|
+
cur = (cur as Dict)[k];
|
|
27
|
+
}
|
|
28
|
+
return cur;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** `v` when it is a string, else undefined. */
|
|
32
|
+
export function str(v: unknown): string | undefined {
|
|
33
|
+
return typeof v === 'string' ? v : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** `v` when it is an array, else an empty one. */
|
|
37
|
+
export function list(v: unknown): unknown[] {
|
|
38
|
+
return Array.isArray(v) ? (v as unknown[]) : [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isStringList(v: unknown): v is string[] {
|
|
42
|
+
return Array.isArray(v) && v.every((x) => typeof x === 'string');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The message of a caught value, whatever was thrown. */
|
|
46
|
+
export function errorMessage(e: unknown): string {
|
|
47
|
+
return e instanceof Error ? e.message : String(e);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** What a failed child process said: its stderr, else the error's message. */
|
|
51
|
+
export function stderrOf(e: unknown): string {
|
|
52
|
+
const err = at(e, 'stderr');
|
|
53
|
+
if (typeof err === 'string' && err) return err;
|
|
54
|
+
if (Buffer.isBuffer(err) && err.length > 0) return err.toString();
|
|
55
|
+
return errorMessage(e);
|
|
56
|
+
}
|
package/src/predicates.ts
CHANGED
|
@@ -15,8 +15,9 @@ import {
|
|
|
15
15
|
import { checkingJobs, ciSystems, ciTaskCalls, mainPushSteps, readWorkflows, shippingJobs, upstream } from './gha.ts';
|
|
16
16
|
import { detectHookFrameworks } from './hooks.ts';
|
|
17
17
|
import { covers, expandTasks, isVocabularyTask, projectTaskCalls } from './vocabulary.ts';
|
|
18
|
+
import { at, isStringList, str } from './narrow.ts';
|
|
18
19
|
|
|
19
|
-
type Args = Record<string,
|
|
20
|
+
export type Args = Record<string, unknown>;
|
|
20
21
|
|
|
21
22
|
/** A fact vdx could not read here — a CI system or a hosting it does not inspect. */
|
|
22
23
|
export interface Unknown {
|
|
@@ -42,7 +43,7 @@ const DEFAULT_TEST_PATTERN =
|
|
|
42
43
|
|
|
43
44
|
function testPattern(args: Args): RegExp {
|
|
44
45
|
try {
|
|
45
|
-
return new RegExp(
|
|
46
|
+
return new RegExp(str(args.pattern) ?? DEFAULT_TEST_PATTERN);
|
|
46
47
|
} catch {
|
|
47
48
|
return new RegExp(DEFAULT_TEST_PATTERN);
|
|
48
49
|
}
|
|
@@ -102,7 +103,7 @@ export const REGISTRY: Record<string, PredicateFn> = {
|
|
|
102
103
|
has_file: (args, ctx) => fileExists(ctx, String(args.path)),
|
|
103
104
|
|
|
104
105
|
file_contains: (args, ctx) => {
|
|
105
|
-
const paths
|
|
106
|
+
const paths = isStringList(args.paths) ? args.paths : [String(args.path)];
|
|
106
107
|
const expanded = expandPaths(paths, ctx);
|
|
107
108
|
let re: RegExp;
|
|
108
109
|
try {
|
|
@@ -119,7 +120,7 @@ export const REGISTRY: Record<string, PredicateFn> = {
|
|
|
119
120
|
|
|
120
121
|
package_present: (args, ctx) => {
|
|
121
122
|
if (typeof args === 'string') return isPackagePresent(args, 'any', ctx);
|
|
122
|
-
return isPackagePresent(String(args.name),
|
|
123
|
+
return isPackagePresent(String(args.name), str(args.ecosystem) ?? 'any', ctx);
|
|
123
124
|
},
|
|
124
125
|
|
|
125
126
|
tsc_flag: (args, ctx) => {
|
|
@@ -143,7 +144,7 @@ export const REGISTRY: Record<string, PredicateFn> = {
|
|
|
143
144
|
gh_workflow_blocks_pr: (args, ctx) => {
|
|
144
145
|
const dir = path.join(ctx.projectRoot, '.github', 'workflows');
|
|
145
146
|
if (!fs.existsSync(dir)) return false;
|
|
146
|
-
const wanted =
|
|
147
|
+
const wanted = str(args.check_name) || null;
|
|
147
148
|
try {
|
|
148
149
|
for (const f of fs.readdirSync(dir)) {
|
|
149
150
|
if (!/\.ya?ml$/.test(f)) continue;
|
|
@@ -166,21 +167,19 @@ export const REGISTRY: Record<string, PredicateFn> = {
|
|
|
166
167
|
const text = readText(ctx, f);
|
|
167
168
|
if (text && new RegExp(`^${hook}:`, 'm').test(text)) return true;
|
|
168
169
|
}
|
|
169
|
-
|
|
170
|
-
if (composer?.extra?.hooks?.[hook]) return true;
|
|
171
|
-
return false;
|
|
170
|
+
return Boolean(at(readJson(ctx, 'composer.json'), 'extra', 'hooks', hook));
|
|
172
171
|
},
|
|
173
172
|
|
|
174
173
|
config_value: (args, ctx) => {
|
|
175
|
-
const filePath =
|
|
176
|
-
const jsonpath =
|
|
174
|
+
const filePath = str(args.path) ?? '';
|
|
175
|
+
const jsonpath = str(args.jsonpath) ?? '';
|
|
177
176
|
if (!filePath || !jsonpath) return false;
|
|
178
177
|
const data = readStructured(ctx, filePath);
|
|
179
178
|
if (data === null || data === undefined) return false;
|
|
180
179
|
const value = resolveConfigPath(data, jsonpath);
|
|
181
180
|
|
|
182
181
|
// Operator resolution: explicit `op`, otherwise infer from arg shape.
|
|
183
|
-
let op =
|
|
182
|
+
let op = str(args.op) || null;
|
|
184
183
|
if (!op) {
|
|
185
184
|
if (args.equals !== undefined) op = 'equals';
|
|
186
185
|
else if (args.gte !== undefined) op = 'gte';
|
package/src/publish.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as path from 'node:path';
|
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
4
|
import type { AuditResult } from './audit.ts';
|
|
5
5
|
import { stackForDir, type Ctx } from './facts.ts';
|
|
6
|
+
import { at, dict, errorMessage, stderrOf, str } from './narrow.ts';
|
|
6
7
|
|
|
7
8
|
export type BumpKind = 'patch' | 'minor' | 'major';
|
|
8
9
|
|
|
@@ -115,7 +116,7 @@ const runGit: GitRunner = (args, cwd) =>
|
|
|
115
116
|
* since the last fetch; nothing is fetched.
|
|
116
117
|
*/
|
|
117
118
|
export function planPush(projectRoot: string, enabled: boolean, git: GitRunner = runGit): PushPlan {
|
|
118
|
-
let upstream: string | null
|
|
119
|
+
let upstream: string | null;
|
|
119
120
|
try {
|
|
120
121
|
upstream = git(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], projectRoot).trim() || null;
|
|
121
122
|
} catch {
|
|
@@ -144,10 +145,10 @@ function fetchPublishedVersion(packageName: string, npm: NpmRunner): string | nu
|
|
|
144
145
|
try {
|
|
145
146
|
const out = npm(['view', packageName, 'version']);
|
|
146
147
|
return out.trim() || null;
|
|
147
|
-
} catch (e
|
|
148
|
-
const stderr =
|
|
148
|
+
} catch (e) {
|
|
149
|
+
const stderr = stderrOf(e);
|
|
149
150
|
if (stderr.includes('E404') || stderr.includes('404 Not Found')) return null;
|
|
150
|
-
throw new Error(`npm view failed: ${stderr.split('\n')[0] ?? stderr}
|
|
151
|
+
throw new Error(`npm view failed: ${stderr.split('\n')[0] ?? stderr}`, { cause: e });
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
|
|
@@ -155,12 +156,10 @@ function fetchPublishedVersion(packageName: string, npm: NpmRunner): string | nu
|
|
|
155
156
|
* The registry `npm publish` sends this package to, when it is not npm's
|
|
156
157
|
* default: `publishConfig.registry`, else the scope's registry from npm config.
|
|
157
158
|
*/
|
|
158
|
-
export function publishRegistry(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (pkg.publishConfig?.registry) return pkg.publishConfig.registry;
|
|
163
|
-
const scope = pkg.name?.match(/^(@[^/]+)\//)?.[1];
|
|
159
|
+
export function publishRegistry(pkg: unknown, npm: NpmRunner): string | null {
|
|
160
|
+
const registry = str(at(pkg, 'publishConfig', 'registry'));
|
|
161
|
+
if (registry) return registry;
|
|
162
|
+
const scope = str(at(pkg, 'name'))?.match(/^(@[^/]+)\//)?.[1];
|
|
164
163
|
if (!scope) return null;
|
|
165
164
|
try {
|
|
166
165
|
const value = npm(['config', 'get', `${scope}:registry`]).trim();
|
|
@@ -184,8 +183,8 @@ export function checkNpmAuth(registry: string | null, npm: NpmRunner): Preflight
|
|
|
184
183
|
const who = npm(registry ? ['whoami', '--registry', registry] : ['whoami']).trim();
|
|
185
184
|
if (who) return { name: 'npm-auth', ok: true, message: `OK (logged in to ${where} as ${who})` };
|
|
186
185
|
return { name: 'npm-auth', ok: false, message: `npm whoami answered nothing for ${where}. Run: ${login}`, fix };
|
|
187
|
-
} catch (e
|
|
188
|
-
const stderr =
|
|
186
|
+
} catch (e) {
|
|
187
|
+
const stderr = stderrOf(e);
|
|
189
188
|
if (/E401|ENEEDAUTH/.test(stderr)) {
|
|
190
189
|
return {
|
|
191
190
|
name: 'npm-auth',
|
|
@@ -268,9 +267,9 @@ export function planPublish(
|
|
|
268
267
|
throw new Error(`vdx publish: package.json not found at ${pkgJsonPath}`);
|
|
269
268
|
}
|
|
270
269
|
|
|
271
|
-
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
272
|
-
const packageName
|
|
273
|
-
const currentVersion
|
|
270
|
+
const pkg: unknown = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
271
|
+
const packageName = str(at(pkg, 'name')) ?? '(unnamed)';
|
|
272
|
+
const currentVersion = str(at(pkg, 'version')) ?? '0.0.0';
|
|
274
273
|
const newVersion = bumpSemver(currentVersion, opts.bump);
|
|
275
274
|
|
|
276
275
|
const preflight: PreflightCheck[] = [];
|
|
@@ -312,8 +311,8 @@ export function planPublish(
|
|
|
312
311
|
let registryError: string | null = null;
|
|
313
312
|
try {
|
|
314
313
|
published = fetchPublishedVersion(packageName, npm);
|
|
315
|
-
} catch (e
|
|
316
|
-
registryError =
|
|
314
|
+
} catch (e) {
|
|
315
|
+
registryError = errorMessage(e);
|
|
317
316
|
}
|
|
318
317
|
if (registryError) {
|
|
319
318
|
preflight.push({
|
|
@@ -416,9 +415,10 @@ export function defaultPublishDeps(): PublishDeps {
|
|
|
416
415
|
|
|
417
416
|
/** Set `version` in package-lock.json — the root and its own entry under `packages`. */
|
|
418
417
|
export function bumpLockText(text: string, version: string): string {
|
|
419
|
-
const lock = JSON.parse(text);
|
|
420
|
-
lock
|
|
421
|
-
|
|
418
|
+
const lock = dict(JSON.parse(text)) ?? {};
|
|
419
|
+
lock['version'] = version;
|
|
420
|
+
const self = dict(at(lock, 'packages', ''));
|
|
421
|
+
if (self) self['version'] = version;
|
|
422
422
|
return JSON.stringify(lock, null, 2) + '\n';
|
|
423
423
|
}
|
|
424
424
|
|
|
@@ -502,8 +502,8 @@ export function executePublish(plan: PublishPlan, deps: PublishDeps = defaultPub
|
|
|
502
502
|
fs.writeFileSync(file, next(text), 'utf8');
|
|
503
503
|
};
|
|
504
504
|
rewrite(plan.packageJsonPath, (text) => {
|
|
505
|
-
const pkg = JSON.parse(text);
|
|
506
|
-
pkg
|
|
505
|
+
const pkg = dict(JSON.parse(text)) ?? {};
|
|
506
|
+
pkg['version'] = plan.newVersion;
|
|
507
507
|
return JSON.stringify(pkg, null, 2) + '\n';
|
|
508
508
|
});
|
|
509
509
|
if (plan.lockPath) rewrite(plan.lockPath, (text) => bumpLockText(text, plan.newVersion));
|
|
@@ -521,11 +521,13 @@ export function executePublish(plan: PublishPlan, deps: PublishDeps = defaultPub
|
|
|
521
521
|
for (const s of signals) process.on(s, holdOut);
|
|
522
522
|
try {
|
|
523
523
|
deps.run('npm', ['publish'], path.dirname(plan.packageJsonPath));
|
|
524
|
-
} catch (e
|
|
524
|
+
} catch (e) {
|
|
525
525
|
for (const [file, text] of originals) fs.writeFileSync(file, text, 'utf8');
|
|
526
|
-
const
|
|
526
|
+
const signal = str(at(e, 'signal'));
|
|
527
|
+
const status = at(e, 'status');
|
|
528
|
+
const what = signal ? `interrupted (${signal})` : `failed (exit ${typeof status === 'number' ? status : '?'})`;
|
|
527
529
|
deps.log(`✗ npm publish ${what}; reverted ${bumped.map((f) => path.basename(f)).join(' and ')}`);
|
|
528
|
-
throw new Error(`npm publish ${
|
|
530
|
+
throw new Error(`npm publish ${signal ? 'interrupted' : 'failed'}`, { cause: e });
|
|
529
531
|
} finally {
|
|
530
532
|
for (const s of signals) process.off(s, holdOut);
|
|
531
533
|
}
|
package/src/rubric.ts
CHANGED
|
@@ -124,8 +124,8 @@ export function parseRubric(text: string, source: string): Rubric {
|
|
|
124
124
|
}
|
|
125
125
|
// нормализуем default_target (если кто-то напишет "L4" со spaces)
|
|
126
126
|
for (const ax of data.axes) {
|
|
127
|
-
ax.default_target =
|
|
128
|
-
ax.storage =
|
|
127
|
+
ax.default_target = ax.default_target ?? 'L4';
|
|
128
|
+
ax.storage = ax.storage ?? 'level';
|
|
129
129
|
}
|
|
130
130
|
if (compareSchema(schema, '0.3')! >= 0) {
|
|
131
131
|
const bad = invalidPredicates(data);
|
package/src/run.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import * as TOML from 'smol-toml';
|
|
4
|
+
import { errorMessage } from './narrow.ts';
|
|
4
5
|
|
|
5
6
|
export const LIFECYCLE_VERBS = ['up', 'down', 'build', 'test', 'check', 'fix'] as const;
|
|
6
7
|
export type LifecycleVerb = (typeof LIFECYCLE_VERBS)[number];
|
|
@@ -21,8 +22,8 @@ export function resolveLifecycleVerb(projectRoot: string, verb: string): VerbRes
|
|
|
21
22
|
let parsed: unknown;
|
|
22
23
|
try {
|
|
23
24
|
parsed = TOML.parse(fs.readFileSync(miseTomlPath, 'utf8'));
|
|
24
|
-
} catch (e
|
|
25
|
-
return { ok: false, reason: 'parse-error', detail:
|
|
25
|
+
} catch (e) {
|
|
26
|
+
return { ok: false, reason: 'parse-error', detail: errorMessage(e) };
|
|
26
27
|
}
|
|
27
28
|
const tasks = (parsed as { tasks?: Record<string, unknown> })?.tasks;
|
|
28
29
|
if (!tasks || typeof tasks !== 'object' || !(verb in tasks)) {
|
package/src/vocabulary.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* predicates (what a hook runs), so both read "the project's task" the same way.
|
|
6
6
|
*/
|
|
7
7
|
import { listAllTasks, readJson, readToml, type Ctx } from './facts.ts';
|
|
8
|
+
import { at, dict, list } from './narrow.ts';
|
|
8
9
|
|
|
9
10
|
export const LIFECYCLE_VOCABULARY = ['up', 'down', 'build', 'test', 'check', 'fix'] as const;
|
|
10
11
|
|
|
@@ -99,11 +100,9 @@ export function isVocabularyTask(name: string, words: readonly string[] = ['test
|
|
|
99
100
|
/** Tasks a task's definition calls: composer `@refs`, `npm run X` inside a script, mise `depends`/`run`. */
|
|
100
101
|
export function taskChildren(ctx: Ctx, name: string): string[] {
|
|
101
102
|
const out: string[] = [];
|
|
102
|
-
const
|
|
103
|
-
const entry = composer?.scripts?.[name];
|
|
103
|
+
const entry = at(readJson(ctx, 'composer.json'), 'scripts', name);
|
|
104
104
|
if (entry !== undefined) {
|
|
105
|
-
const
|
|
106
|
-
for (const e of list) {
|
|
105
|
+
for (const e of Array.isArray(entry) ? (entry as unknown[]) : [entry]) {
|
|
107
106
|
if (typeof e !== 'string') continue;
|
|
108
107
|
if (e.startsWith('@')) {
|
|
109
108
|
const [ref, ...rest] = e.slice(1).trim().split(/\s+/);
|
|
@@ -114,15 +113,12 @@ export function taskChildren(ctx: Ctx, name: string): string[] {
|
|
|
114
113
|
}
|
|
115
114
|
}
|
|
116
115
|
}
|
|
117
|
-
const
|
|
118
|
-
const script = pkg?.scripts?.[name];
|
|
116
|
+
const script = at(readJson(ctx, 'package.json'), 'scripts', name);
|
|
119
117
|
if (typeof script === 'string') out.push(...runnerCalls(script));
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
out.push(...task.depends.filter((d: unknown): d is string => typeof d === 'string'));
|
|
125
|
-
const runs = Array.isArray(task.run) ? task.run : [task.run];
|
|
118
|
+
const task = dict(at(readToml(ctx, 'mise.toml'), 'tasks', name));
|
|
119
|
+
if (task) {
|
|
120
|
+
out.push(...list(task['depends']).filter((d): d is string => typeof d === 'string'));
|
|
121
|
+
const runs = Array.isArray(task['run']) ? (task['run'] as unknown[]) : [task['run']];
|
|
126
122
|
for (const r of runs) if (typeof r === 'string') out.push(...runnerCalls(r));
|
|
127
123
|
}
|
|
128
124
|
const tasks = listAllTasks(ctx);
|