@vodmal/vdx-cli 0.25.0 → 0.27.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 +16 -6
- package/rubric/vdx-rubric.yaml +45 -9
- package/src/ai.ts +48 -49
- package/src/author.ts +1 -1
- package/src/clone-checks.ts +1 -1
- package/src/conversations.ts +33 -26
- package/src/doctor.ts +10 -9
- package/src/evaluator.ts +22 -21
- package/src/facts.ts +24 -49
- package/src/gha.ts +36 -28
- package/src/hooks.ts +34 -34
- package/src/index.ts +22 -13
- package/src/init.ts +12 -20
- package/src/manifest.ts +25 -4
- package/src/mcp-server.ts +39 -35
- package/src/narrow.ts +56 -0
- package/src/personal-hooks.ts +2 -8
- package/src/predicates.ts +10 -11
- package/src/publish.ts +28 -26
- package/src/rubric.ts +6 -6
- package/src/run.ts +3 -2
- package/src/vocabulary.ts +10 -14
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({
|
|
@@ -384,7 +383,7 @@ export function preflightFixable(plan: PublishPlan): boolean {
|
|
|
384
383
|
}
|
|
385
384
|
|
|
386
385
|
/** Runs a command on this terminal (stdio inherited); throws on a non-zero exit. */
|
|
387
|
-
|
|
386
|
+
type CommandRunner = (cmd: string, args: string[], cwd: string) => void;
|
|
388
387
|
|
|
389
388
|
export interface PublishDeps {
|
|
390
389
|
run: CommandRunner;
|
|
@@ -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
|
@@ -4,20 +4,20 @@ import { COMBINATORS, predicateNames } from './evaluator.ts';
|
|
|
4
4
|
import { REGISTRY, REMOVED_IN_0_3 } from './predicates.ts';
|
|
5
5
|
|
|
6
6
|
export type LevelName = 'L0' | 'L1' | 'L2' | 'L3' | 'L4';
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
type AxisClass = 'critical' | 'supporting';
|
|
8
|
+
type Storage = 'level' | 'flags';
|
|
9
9
|
|
|
10
10
|
// Predicate структура динамическая — оставляем any для дешёвого парсинга
|
|
11
11
|
// со сахаром. Валидация — отдельная история v0.2.
|
|
12
12
|
export type Predicate = unknown;
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
interface Flag {
|
|
15
15
|
id: string;
|
|
16
16
|
weight: number;
|
|
17
17
|
predicate: Predicate;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
interface StackImpl {
|
|
21
21
|
flags?: Flag[];
|
|
22
22
|
level_thresholds?: Partial<Record<LevelName, number>>;
|
|
23
23
|
}
|
|
@@ -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,8 +5,9 @@
|
|
|
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
|
+
const LIFECYCLE_VOCABULARY = ['up', 'down', 'build', 'test', 'check', 'fix'] as const;
|
|
10
11
|
|
|
11
12
|
/** npm-family options that take a value; their value is not the script name. */
|
|
12
13
|
const VALUE_OPTIONS = new Set(['--prefix', '-C', '--dir', '-w', '--workspace', '--filter', '--cwd']);
|
|
@@ -97,13 +98,11 @@ export function isVocabularyTask(name: string, words: readonly string[] = ['test
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
/** Tasks a task's definition calls: composer `@refs`, `npm run X` inside a script, mise `depends`/`run`. */
|
|
100
|
-
|
|
101
|
+
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);
|