yam-flow 2.8.1 → 2.8.2
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/CHANGELOG.md +10 -0
- package/COMMANDS.md +7 -2
- package/README.md +5 -1
- package/dist/bin/yam.js +141 -38
- package/dist/lib/design-production.d.ts +6 -6
- package/dist/lib/external-updates.d.ts +6 -1
- package/dist/lib/external-updates.js +93 -6
- package/dist/lib/hook-readiness.d.ts +54 -0
- package/dist/lib/hook-readiness.js +259 -0
- package/dist/lib/memory-records.d.ts +29 -0
- package/dist/lib/memory-records.js +92 -0
- package/dist/lib/verification-closure.d.ts +1 -1
- package/package.json +6 -2
- package/src/bin/yam.ts +122 -35
- package/src/lib/external-updates.ts +87 -6
- package/src/lib/hook-readiness.ts +215 -0
- package/src/lib/memory-records.ts +109 -0
- package/yam.manifest.json +1 -1
package/src/bin/yam.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { selectHookNode, probeHookDiscovery, inspectHookObservation, recordHookObservation } from '../lib/hook-readiness.js';
|
|
4
|
+
import { readMemoryDirectory } from '../lib/memory-records.js';
|
|
3
5
|
import { runSecurityCommand } from '../lib/security-integration.js';
|
|
4
6
|
import { createHash } from 'node:crypto';
|
|
5
7
|
import fs from 'node:fs';
|
|
@@ -149,8 +151,8 @@ const INSTRUCTION_DUPLICATION_LIMITS = {
|
|
|
149
151
|
minDirectiveChars: 40
|
|
150
152
|
};
|
|
151
153
|
const YAM_HOOK_ENTRYPOINT = path.join(ROOT, 'dist', 'bin', 'yam.js');
|
|
152
|
-
|
|
153
|
-
|
|
154
|
+
let YAM_LITE_HOOK_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run lite`;
|
|
155
|
+
let YAM_STUDY_NOTE_HOOK_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run study-note`;
|
|
154
156
|
const REQUIRED_PACK_SECTIONS = [
|
|
155
157
|
'Product Direction',
|
|
156
158
|
'UI Direction',
|
|
@@ -2014,6 +2016,7 @@ async function hook(args = []) {
|
|
|
2014
2016
|
const subcommand = args[0] || 'status';
|
|
2015
2017
|
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return hookUsage();
|
|
2016
2018
|
if (subcommand === 'status') return hookStatus(args.slice(1));
|
|
2019
|
+
if (subcommand === 'migrate') return hookMigrate(args.slice(1));
|
|
2017
2020
|
if (subcommand === 'enable') return hookEnable(args.slice(1));
|
|
2018
2021
|
if (subcommand === 'disable') return hookDisable(args.slice(1));
|
|
2019
2022
|
if (subcommand === 'run') return hookRun(args.slice(1));
|
|
@@ -2025,14 +2028,17 @@ function hookUsage() {
|
|
|
2025
2028
|
console.log(`yam hook
|
|
2026
2029
|
|
|
2027
2030
|
Usage:
|
|
2028
|
-
yam hook status [--global|--project dir]
|
|
2029
|
-
yam hook
|
|
2031
|
+
yam hook status [--global|--project dir] [--probe] [--json]
|
|
2032
|
+
yam hook migrate [--global|--project dir]
|
|
2033
|
+
yam hook enable <lite|study-note> [--global|--project dir] [--observe]
|
|
2030
2034
|
yam hook disable [lite|study-note] [--global|--project dir]
|
|
2031
2035
|
yam hook run <lite|study-note>
|
|
2032
2036
|
|
|
2033
2037
|
Notes:
|
|
2034
2038
|
hooks are opt-in. lite is advisory-only; study-note adds a prompt reminder and a one-pass Stop completion gate.
|
|
2035
2039
|
Hooks do not generate reports, run tmux, start subagents, or execute verification automatically.
|
|
2040
|
+
--probe queries hooks/list in a separate bounded Codex app-server; active session loading stays unknown.
|
|
2041
|
+
--observe opts into private local entrypoint observations without storing prompt content; not host attestation.
|
|
2036
2042
|
`);
|
|
2037
2043
|
}
|
|
2038
2044
|
|
|
@@ -2057,7 +2063,7 @@ function parseHookArgs(args = []) {
|
|
|
2057
2063
|
}
|
|
2058
2064
|
|
|
2059
2065
|
function hookPathFor(parsed) {
|
|
2060
|
-
if (parsed.mode === 'global') return path.join(os.homedir(), '.codex', 'hooks.json');
|
|
2066
|
+
if (parsed.mode === 'global') return path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'hooks.json');
|
|
2061
2067
|
return path.join(path.resolve(parsed.projectDir || process.cwd()), '.codex', 'hooks.json');
|
|
2062
2068
|
}
|
|
2063
2069
|
|
|
@@ -2073,19 +2079,42 @@ function isYamHookProfile(handler: AnyRecord = {}, profile = 'lite') {
|
|
|
2073
2079
|
return profile === 'study-note' ? isYamStudyNoteHook(handler) : isYamLiteHook(handler);
|
|
2074
2080
|
}
|
|
2075
2081
|
|
|
2082
|
+
const CODEX_HOOK_EVENTS = new Set(['SessionStart', 'SessionEnd', 'PreToolUse', 'PermissionRequest', 'PostToolUse', 'PreCompact', 'PostCompact', 'UserPromptSubmit', 'SubagentStart', 'SubagentStop', 'Stop', 'Interrupt']);
|
|
2083
|
+
|
|
2084
|
+
function hookEventMap(config: AnyRecord = {}) {
|
|
2085
|
+
const nested = config.hooks && typeof config.hooks === 'object' && !Array.isArray(config.hooks) ? config.hooks : {};
|
|
2086
|
+
const events = { ...nested };
|
|
2087
|
+
for (const [event, entries] of Object.entries(config)) {
|
|
2088
|
+
if (CODEX_HOOK_EVENTS.has(event) && Array.isArray(entries)) {
|
|
2089
|
+
events[event] = [...(Array.isArray(events[event]) ? events[event] : []), ...entries];
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
return events;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
function canonicalHookConfig(config: AnyRecord = {}) {
|
|
2096
|
+
if (config.hooks != null && (typeof config.hooks !== 'object' || Array.isArray(config.hooks))) throw new Error('hooks must be an event object; existing file preserved');
|
|
2097
|
+
const next = { ...config, hooks: hookEventMap(config) };
|
|
2098
|
+
for (const event of CODEX_HOOK_EVENTS) {
|
|
2099
|
+
if (event in config && !Array.isArray(config[event])) throw new Error('legacy hook event must be an array; existing file preserved');
|
|
2100
|
+
delete next[event];
|
|
2101
|
+
}
|
|
2102
|
+
return next;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2076
2105
|
function stripYamHooks(config: AnyRecord = {}, profile = 'lite') {
|
|
2077
|
-
const next =
|
|
2078
|
-
for (const event of Object.keys(next)) {
|
|
2079
|
-
if (!Array.isArray(next[event])) continue;
|
|
2080
|
-
const entries = next[event];
|
|
2106
|
+
const next = canonicalHookConfig(config);
|
|
2107
|
+
for (const event of Object.keys(next.hooks)) {
|
|
2108
|
+
if (!Array.isArray(next.hooks[event])) continue;
|
|
2109
|
+
const entries = next.hooks[event];
|
|
2081
2110
|
const keptEntries = [];
|
|
2082
2111
|
for (const entry of entries) {
|
|
2083
2112
|
const hooks = Array.isArray(entry?.hooks) ? entry.hooks.filter((handler) => !isYamHookProfile(handler, profile)) : [];
|
|
2084
2113
|
const rest = { ...entry, hooks };
|
|
2085
2114
|
if (hooks.length > 0) keptEntries.push(rest);
|
|
2086
2115
|
}
|
|
2087
|
-
if (keptEntries.length > 0) next[event] = keptEntries;
|
|
2088
|
-
else delete next[event];
|
|
2116
|
+
if (keptEntries.length > 0) next.hooks[event] = keptEntries;
|
|
2117
|
+
else delete next.hooks[event];
|
|
2089
2118
|
}
|
|
2090
2119
|
return next;
|
|
2091
2120
|
}
|
|
@@ -2103,7 +2132,7 @@ function withYamHook(config: AnyRecord = {}, profile = 'lite') {
|
|
|
2103
2132
|
}
|
|
2104
2133
|
]
|
|
2105
2134
|
};
|
|
2106
|
-
next[event] = [...(Array.isArray(next[event]) ? next[event] : []), entry];
|
|
2135
|
+
next.hooks[event] = [...(Array.isArray(next.hooks[event]) ? next.hooks[event] : []), entry];
|
|
2107
2136
|
}
|
|
2108
2137
|
return next;
|
|
2109
2138
|
}
|
|
@@ -2113,6 +2142,11 @@ async function hookStatus(args = []) {
|
|
|
2113
2142
|
const target = hookPathFor(parsed);
|
|
2114
2143
|
const loaded = await readHookConfig(target);
|
|
2115
2144
|
if (loaded.error) {
|
|
2145
|
+
if (args.includes('--json')) {
|
|
2146
|
+
console.log(JSON.stringify({ schema: 'yam.hook-readiness.v1', scope: parsed.mode, configuration: 'unreadable', loaded: 'unknown', execution_observed: false, truth_status: 'partial' }, null, 2));
|
|
2147
|
+
process.exitCode = 1;
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2116
2150
|
console.log('yam-lite hook: broken');
|
|
2117
2151
|
console.log('yam-study-note hook: broken');
|
|
2118
2152
|
console.log(` - hook config unreadable: ${loaded.error}`);
|
|
@@ -2123,10 +2157,28 @@ async function hookStatus(args = []) {
|
|
|
2123
2157
|
}
|
|
2124
2158
|
const lite = await inspectHookProfile(loaded.config, 'lite');
|
|
2125
2159
|
const studyNote = await inspectHookProfile(loaded.config, 'study-note');
|
|
2160
|
+
const cwd = path.resolve(parsed.projectDir || process.cwd());
|
|
2161
|
+
const commands = { lite: lite.entries.map((entry) => String(entry.handler.command)), 'study-note': studyNote.entries.map((entry) => String(entry.handler.command)) };
|
|
2162
|
+
const host = args.includes('--probe')
|
|
2163
|
+
? await probeHookDiscovery({ executable: await findExecutable('codex') || 'codex', cwd, commands, configPath: target })
|
|
2164
|
+
: { state: 'not_measured', active_session_loaded: 'not_measured', execution_observed: false, truth_status: 'partial' };
|
|
2165
|
+
const profiles = {};
|
|
2166
|
+
for (const [profile, health] of [['lite', lite], ['study-note', studyNote]] as const) {
|
|
2167
|
+
profiles[profile] = { configured: health.state, issues: health.issues, loaded: 'unknown',
|
|
2168
|
+
execution: await inspectHookObservation(cwd, profile, YAM_HOOK_ENTRYPOINT) };
|
|
2169
|
+
}
|
|
2170
|
+
if (args.includes('--json')) {
|
|
2171
|
+
console.log(JSON.stringify({ schema: 'yam.hook-readiness.v1', scope: parsed.mode, profiles, host, truth_status: 'partial' }, null, 2));
|
|
2172
|
+
if (lite.state === 'broken' || studyNote.state === 'broken') process.exitCode = 1;
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2126
2175
|
printHookProfileStatus('lite', lite);
|
|
2127
2176
|
printHookProfileStatus('study-note', studyNote);
|
|
2128
2177
|
console.log(`scope: ${parsed.mode}`);
|
|
2129
2178
|
console.log(`file: ${target}`);
|
|
2179
|
+
console.log('active Codex session loaded: unknown (configuration is not runtime evidence)');
|
|
2180
|
+
console.log(`host discovery: ${host.state}`);
|
|
2181
|
+
for (const profile of ['lite', 'study-note']) console.log(`yam-${profile} execution: ${profiles[profile].execution.state} (local entrypoint evidence, not host attestation)`);
|
|
2130
2182
|
if (lite.state === 'broken' || studyNote.state === 'broken') process.exitCode = 1;
|
|
2131
2183
|
}
|
|
2132
2184
|
|
|
@@ -2135,7 +2187,7 @@ function hookConfigHasYamLite(config = {}) {
|
|
|
2135
2187
|
}
|
|
2136
2188
|
|
|
2137
2189
|
function hookConfigHasProfile(config = {}, profile = 'lite') {
|
|
2138
|
-
return Object.values(config).some((entries) => Array.isArray(entries) && entries.some((entry) => {
|
|
2190
|
+
return Object.values(hookEventMap(config)).some((entries) => Array.isArray(entries) && entries.some((entry) => {
|
|
2139
2191
|
return Array.isArray(entry?.hooks) && entry.hooks.some((handler) => isYamHookProfile(handler, profile));
|
|
2140
2192
|
}));
|
|
2141
2193
|
}
|
|
@@ -2150,7 +2202,7 @@ function hookEventsForProfile(profile = 'lite') {
|
|
|
2150
2202
|
|
|
2151
2203
|
function hookProfileEntries(config: AnyRecord = {}, profile = 'lite') {
|
|
2152
2204
|
const matches = [];
|
|
2153
|
-
for (const [event, entries] of Object.entries(config)) {
|
|
2205
|
+
for (const [event, entries] of Object.entries(hookEventMap(config))) {
|
|
2154
2206
|
if (!Array.isArray(entries)) continue;
|
|
2155
2207
|
for (const entry of entries) {
|
|
2156
2208
|
if (!Array.isArray(entry?.hooks)) continue;
|
|
@@ -2209,6 +2261,7 @@ function splitHookCommand(command = '') {
|
|
|
2209
2261
|
|
|
2210
2262
|
function parseYamHookCommand(command = '', profile = 'lite') {
|
|
2211
2263
|
const words = splitHookCommand(command);
|
|
2264
|
+
if (words.at(-1) === '--observe') words.pop();
|
|
2212
2265
|
if (words.length === 5 && words[2] === 'hook' && words[3] === 'run' && words[4] === profile) {
|
|
2213
2266
|
const nodeName = path.basename(words[0]).toLowerCase();
|
|
2214
2267
|
if (/^node(?:\.exe)?$/.test(nodeName) && path.basename(words[1]).toLowerCase() === 'yam.js') {
|
|
@@ -2249,6 +2302,7 @@ async function inspectHookProfile(config: AnyRecord = {}, profile = 'lite') {
|
|
|
2249
2302
|
const entries = hookProfileEntries(config, profile);
|
|
2250
2303
|
if (!entries.length) return { state: 'disabled', issues: [], entries };
|
|
2251
2304
|
const issues = [];
|
|
2305
|
+
if (Object.keys(config).some((event) => CODEX_HOOK_EVENTS.has(event))) issues.push('legacy hook config requires the top-level hooks wrapper; rerun yam hook enable to migrate with backup');
|
|
2252
2306
|
for (const event of hookEventsForProfile(profile)) {
|
|
2253
2307
|
const count = entries.filter((entry) => entry.event === event).length;
|
|
2254
2308
|
if (count === 0) issues.push(`${event} handler is missing; rerun yam hook enable ${profile}`);
|
|
@@ -2275,6 +2329,9 @@ async function readHookConfig(target) {
|
|
|
2275
2329
|
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
2276
2330
|
return { config: {}, exists: true, error: 'top-level value must be a JSON object' };
|
|
2277
2331
|
}
|
|
2332
|
+
if (value.hooks != null && (typeof value.hooks !== 'object' || Array.isArray(value.hooks))) {
|
|
2333
|
+
return { config: {}, exists: true, error: 'hooks must be an event object' };
|
|
2334
|
+
}
|
|
2278
2335
|
return { config: value, exists: true, error: '' };
|
|
2279
2336
|
} catch (error) {
|
|
2280
2337
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { config: {}, exists: false, error: '' };
|
|
@@ -2294,8 +2351,34 @@ async function writeHookConfig(target, config) {
|
|
|
2294
2351
|
}
|
|
2295
2352
|
}
|
|
2296
2353
|
|
|
2354
|
+
async function hookMigrate(args = []) {
|
|
2355
|
+
const parsed = parseHookArgs(args);
|
|
2356
|
+
const target = hookPathFor(parsed);
|
|
2357
|
+
const loaded = await readHookConfig(target);
|
|
2358
|
+
if (loaded.error) { console.error('hook migration blocked: unreadable configuration'); process.exitCode = 1; return; }
|
|
2359
|
+
if (!loaded.exists || !Object.keys(loaded.config).some((key) => CODEX_HOOK_EVENTS.has(key))) {
|
|
2360
|
+
console.log('hook migration: no legacy event layout found'); return;
|
|
2361
|
+
}
|
|
2362
|
+
const before = await fsp.lstat(target);
|
|
2363
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1) throw new Error('hook migration blocked: unsafe config file');
|
|
2364
|
+
const original = await fsp.readFile(target, 'utf8');
|
|
2365
|
+
if (JSON.stringify(JSON.parse(original)) !== JSON.stringify(loaded.config)) throw new Error('hook configuration changed; retry migration');
|
|
2366
|
+
const next = canonicalHookConfig(loaded.config);
|
|
2367
|
+
const backup = `${target}.yam-backup-${timestampId()}`;
|
|
2368
|
+
await fsp.copyFile(target, backup, fs.constants.COPYFILE_EXCL);
|
|
2369
|
+
const after = await fsp.lstat(target);
|
|
2370
|
+
if (after.dev !== before.dev || after.ino !== before.ino || after.mtimeMs !== before.mtimeMs
|
|
2371
|
+
|| await fsp.readFile(target, 'utf8') !== original) throw new Error('hook configuration changed; backup preserved, retry migration');
|
|
2372
|
+
await writeHookConfig(target, next);
|
|
2373
|
+
console.log(`hook migration: wrapped event groups; handlers and metadata preserved\nbackup: ${backup}\nReview changed definitions in Codex /hooks before enabling execution.`);
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2297
2376
|
async function hookEnable(args = []) {
|
|
2298
2377
|
const parsed = parseHookArgs(args);
|
|
2378
|
+
const node = await selectHookNode();
|
|
2379
|
+
const observe = args.includes('--observe') ? ' --observe' : '';
|
|
2380
|
+
YAM_LITE_HOOK_COMMAND = `${JSON.stringify(node.executable)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run lite${observe}`;
|
|
2381
|
+
YAM_STUDY_NOTE_HOOK_COMMAND = `${JSON.stringify(node.executable)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run study-note${observe}`;
|
|
2299
2382
|
if (!['lite', 'study-note'].includes(parsed.profile)) {
|
|
2300
2383
|
console.error('Only lite and study-note hooks are supported: yam hook enable <lite|study-note>');
|
|
2301
2384
|
process.exitCode = 1;
|
|
@@ -2326,6 +2409,7 @@ async function hookEnable(args = []) {
|
|
|
2326
2409
|
}
|
|
2327
2410
|
await writeHookConfig(target, next);
|
|
2328
2411
|
console.log(`yam-${parsed.profile} hook enabled (${parsed.mode}): ${target}`);
|
|
2412
|
+
console.log(`node launcher: ${node.selection}; alias identity matched the current runtime (no PATH candidate executed)`);
|
|
2329
2413
|
if (needsMigration) console.log(`migrated: ${existing.length} existing yam-${parsed.profile} handler(s) to the current command and event coverage`);
|
|
2330
2414
|
console.log('Restart Codex or start a new thread if the app does not pick up hook changes immediately.');
|
|
2331
2415
|
}
|
|
@@ -2346,7 +2430,7 @@ async function hookDisable(args = []) {
|
|
|
2346
2430
|
return;
|
|
2347
2431
|
}
|
|
2348
2432
|
const next = stripYamHooks(current, parsed.profile);
|
|
2349
|
-
if (Object.keys(next).length === 0) {
|
|
2433
|
+
if (Object.keys(next).length === 1 && Object.keys(next.hooks).length === 0) {
|
|
2350
2434
|
await rmrf(target);
|
|
2351
2435
|
} else {
|
|
2352
2436
|
await writeHookConfig(target, next);
|
|
@@ -2374,6 +2458,7 @@ async function hookRun(args = []) {
|
|
|
2374
2458
|
}
|
|
2375
2459
|
const event = input?.hook_event_name || input?.hookEventName || input?.event || 'UserPromptSubmit';
|
|
2376
2460
|
const cwd = String(input?.cwd || process.cwd());
|
|
2461
|
+
if (args.includes('--observe')) await recordHookObservation(cwd, profile, String(event), YAM_HOOK_ENTRYPOINT).catch(() => {});
|
|
2377
2462
|
if (profile === 'study-note' && event === 'Stop') {
|
|
2378
2463
|
console.log(JSON.stringify(await buildStudyNoteStopOutput(input, cwd)));
|
|
2379
2464
|
return;
|
|
@@ -6109,7 +6194,7 @@ async function memory(args = []) {
|
|
|
6109
6194
|
}
|
|
6110
6195
|
if (subcommand === 'summary' || subcommand === 'summarize') {
|
|
6111
6196
|
const parsed = parseMemoryArgs(args.slice(1));
|
|
6112
|
-
return memorySummary(parsed.dir);
|
|
6197
|
+
return memorySummary(parsed.dir, { json: args.includes('--json') });
|
|
6113
6198
|
}
|
|
6114
6199
|
if (subcommand === 'resolve') return memoryResolve(args.slice(1));
|
|
6115
6200
|
console.error(`unknown memory command: ${subcommand}`);
|
|
@@ -6123,7 +6208,7 @@ Usage:
|
|
|
6123
6208
|
yam memory init [dir]
|
|
6124
6209
|
yam memory add [dir] --kind <kind> --summary <text> [--evidence <text>] [--action <text>] [--source <text>]
|
|
6125
6210
|
yam memory list [dir] [--json]
|
|
6126
|
-
yam memory summary [dir]
|
|
6211
|
+
yam memory summary [dir] [--json]
|
|
6127
6212
|
yam memory resolve [dir] <id> [--note <text>]
|
|
6128
6213
|
|
|
6129
6214
|
Kinds:
|
|
@@ -6198,7 +6283,14 @@ async function memoryAdd(args = []) {
|
|
|
6198
6283
|
}
|
|
6199
6284
|
|
|
6200
6285
|
async function memoryList(targetDir = process.cwd(), { json = false } = {}) {
|
|
6201
|
-
const
|
|
6286
|
+
const report = await readMemoryRecords(targetDir);
|
|
6287
|
+
const records = report.records;
|
|
6288
|
+
if (!report.complete) {
|
|
6289
|
+
if (report.state !== 'missing') process.exitCode = 1;
|
|
6290
|
+
if (json) console.log(JSON.stringify(report, null, 2));
|
|
6291
|
+
else console.error(`memory read ${report.state}: ${JSON.stringify(report.diagnostics)}${report.diagnostics_truncated ? ' (diagnostics truncated)' : ''}`);
|
|
6292
|
+
return;
|
|
6293
|
+
}
|
|
6202
6294
|
if (json) {
|
|
6203
6295
|
console.log(JSON.stringify(records, null, 2));
|
|
6204
6296
|
return;
|
|
@@ -6213,9 +6305,16 @@ async function memoryList(targetDir = process.cwd(), { json = false } = {}) {
|
|
|
6213
6305
|
}
|
|
6214
6306
|
}
|
|
6215
6307
|
|
|
6216
|
-
async function memorySummary(targetDir = process.cwd()) {
|
|
6308
|
+
async function memorySummary(targetDir = process.cwd(), { json = false } = {}) {
|
|
6217
6309
|
const dir = memoryDir(targetDir);
|
|
6218
|
-
const
|
|
6310
|
+
const report = await readMemoryRecords(targetDir);
|
|
6311
|
+
if (!report.complete) {
|
|
6312
|
+
process.exitCode = 1;
|
|
6313
|
+
if (json) console.log(JSON.stringify({ ...report, summary_written: false }, null, 2));
|
|
6314
|
+
else console.error(`memory summary blocked (${report.state}); existing summary preserved: ${JSON.stringify(report.diagnostics)}${report.diagnostics_truncated ? ' (diagnostics truncated)' : ''}`);
|
|
6315
|
+
return;
|
|
6316
|
+
}
|
|
6317
|
+
const records = report.records;
|
|
6219
6318
|
await fsp.mkdir(dir, { recursive: true });
|
|
6220
6319
|
const active = records.filter((record) => record.status !== 'resolved');
|
|
6221
6320
|
const resolved = records.filter((record) => record.status === 'resolved');
|
|
@@ -6246,7 +6345,8 @@ async function memorySummary(targetDir = process.cwd()) {
|
|
|
6246
6345
|
|
|
6247
6346
|
const target = path.join(dir, 'summary.md');
|
|
6248
6347
|
await fsp.writeFile(target, `${lines.join('\n').trim()}\n`);
|
|
6249
|
-
console.log(
|
|
6348
|
+
if (json) console.log(JSON.stringify({ ...report, summary_written: true, path: target }, null, 2));
|
|
6349
|
+
else console.log(`memory summary written: ${target}`);
|
|
6250
6350
|
}
|
|
6251
6351
|
|
|
6252
6352
|
async function memoryResolve(args = []) {
|
|
@@ -6305,20 +6405,7 @@ function memoryDir(targetDir = process.cwd()) {
|
|
|
6305
6405
|
|
|
6306
6406
|
async function readMemoryRecords(targetDir = process.cwd()) {
|
|
6307
6407
|
const recordsDir = path.join(memoryDir(targetDir), 'records');
|
|
6308
|
-
|
|
6309
|
-
const entries = await fsp.readdir(recordsDir, { withFileTypes: true });
|
|
6310
|
-
const records = [];
|
|
6311
|
-
for (const entry of entries) {
|
|
6312
|
-
if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
|
|
6313
|
-
const file = path.join(recordsDir, entry.name);
|
|
6314
|
-
try {
|
|
6315
|
-
const record = await readJson(file);
|
|
6316
|
-
records.push(record);
|
|
6317
|
-
} catch (error) {
|
|
6318
|
-
records.push({ id: entry.name.replace(/\.json$/, ''), kind: 'invalid', status: 'invalid', summary: `invalid JSON: ${errorMessage(error)}` });
|
|
6319
|
-
}
|
|
6320
|
-
}
|
|
6321
|
-
return records.sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || '')));
|
|
6408
|
+
return readMemoryDirectory(recordsDir);
|
|
6322
6409
|
}
|
|
6323
6410
|
|
|
6324
6411
|
function timestampId() {
|
|
@@ -34,6 +34,7 @@ export interface ExternalUpdatePaths {
|
|
|
34
34
|
export interface ExternalUpdateDependencies {
|
|
35
35
|
run?: (command: string, args: string[], options?: ExternalCommandOptions) => Promise<ExternalCommandResult> | ExternalCommandResult;
|
|
36
36
|
fetchJson?: (url: string) => Promise<any>;
|
|
37
|
+
probeArtifact?: (url: string) => Promise<number>;
|
|
37
38
|
now?: () => Date;
|
|
38
39
|
homeDir?: string;
|
|
39
40
|
env?: NodeJS.ProcessEnv;
|
|
@@ -46,7 +47,7 @@ export interface ExternalComponentCheck {
|
|
|
46
47
|
installed_version: string;
|
|
47
48
|
latest_version: string;
|
|
48
49
|
update_available: boolean;
|
|
49
|
-
status: 'up_to_date' | 'update_available' | 'not_installed' | 'check_failed';
|
|
50
|
+
status: 'up_to_date' | 'update_available' | 'not_installed' | 'pending_distribution' | 'check_failed';
|
|
50
51
|
source: string;
|
|
51
52
|
source_revision?: {
|
|
52
53
|
local: string;
|
|
@@ -113,6 +114,7 @@ export interface YamInstallIdentity {
|
|
|
113
114
|
interface ResolvedDependencies {
|
|
114
115
|
run: NonNullable<ExternalUpdateDependencies['run']>;
|
|
115
116
|
fetchJson: NonNullable<ExternalUpdateDependencies['fetchJson']>;
|
|
117
|
+
probeArtifact: NonNullable<ExternalUpdateDependencies['probeArtifact']>;
|
|
116
118
|
now: NonNullable<ExternalUpdateDependencies['now']>;
|
|
117
119
|
homeDir: string;
|
|
118
120
|
env: NodeJS.ProcessEnv;
|
|
@@ -158,7 +160,7 @@ export async function checkExternalUpdates(
|
|
|
158
160
|
checkScrapling(deps),
|
|
159
161
|
checkInsaneSearch(deps)
|
|
160
162
|
]);
|
|
161
|
-
const failed = checks.filter((item) => item.status === 'check_failed');
|
|
163
|
+
const failed = checks.filter((item) => item.status === 'check_failed' || item.status === 'pending_distribution');
|
|
162
164
|
return {
|
|
163
165
|
schema: 'yam.external-update-check.v1',
|
|
164
166
|
generated_at: deps.now().toISOString(),
|
|
@@ -351,6 +353,7 @@ async function resolveDependencies(input: ExternalUpdateDependencies): Promise<R
|
|
|
351
353
|
return {
|
|
352
354
|
run,
|
|
353
355
|
fetchJson: input.fetchJson || defaultFetchJson,
|
|
356
|
+
probeArtifact: input.probeArtifact || defaultProbeArtifact,
|
|
354
357
|
now: input.now || (() => new Date()),
|
|
355
358
|
homeDir,
|
|
356
359
|
env,
|
|
@@ -409,18 +412,52 @@ async function acquireApplyLock(deps: ResolvedDependencies) {
|
|
|
409
412
|
}
|
|
410
413
|
|
|
411
414
|
async function checkYam(currentYamVersion: string, deps: ResolvedDependencies): Promise<ExternalComponentCheck> {
|
|
415
|
+
let pending: ExternalComponentCheck | undefined;
|
|
412
416
|
try {
|
|
413
417
|
const data = await deps.fetchJson(YAM_REGISTRY_URL);
|
|
414
418
|
const latest = requireStableVersion(data?.version, 'yam registry version');
|
|
415
419
|
const installed = requireStableVersion(currentYamVersion, 'installed yam version');
|
|
416
420
|
const integrity = requireIntegrityValue(data?.dist?.integrity, 'yam registry integrity');
|
|
417
421
|
const revision = requireGitRevision(data?.gitHead, 'yam registry gitHead');
|
|
422
|
+
if (data?.name !== 'yam-flow') throw new Error('yam registry package name mismatch');
|
|
423
|
+
const tarball = requireYamTarball(data?.dist?.tarball, latest);
|
|
424
|
+
const exactUrl = `${NPM_REGISTRY}yam-flow/${latest}`;
|
|
425
|
+
pending = {
|
|
426
|
+
...componentCheck('yam', installed, latest, 'npm:yam-flow'),
|
|
427
|
+
status: 'pending_distribution',
|
|
428
|
+
error: 'pending_distribution: exact-version metadata or tarball returned HTTP 404; retry the explicit update later',
|
|
429
|
+
source_receipt: {
|
|
430
|
+
canonical_url: YAM_REGISTRY_URL,
|
|
431
|
+
retrieved_at: deps.now().toISOString(),
|
|
432
|
+
access_path: 'npm latest observed; exact-version distribution readiness incomplete',
|
|
433
|
+
version: latest,
|
|
434
|
+
revision,
|
|
435
|
+
integrity: [integrity]
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
let exact: any;
|
|
439
|
+
try {
|
|
440
|
+
exact = await deps.fetchJson(exactUrl);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
if (error instanceof RegistryHttpError && error.status === 404) return pending;
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
if (exact?.name !== 'yam-flow' || data?.name !== 'yam-flow'
|
|
446
|
+
|| requireStableVersion(exact?.version, 'yam exact registry version') !== latest
|
|
447
|
+
|| requireIntegrityValue(exact?.dist?.integrity, 'yam exact registry integrity') !== integrity
|
|
448
|
+
|| requireGitRevision(exact?.gitHead, 'yam exact registry gitHead') !== revision
|
|
449
|
+
|| requireYamTarball(exact?.dist?.tarball, latest) !== tarball) {
|
|
450
|
+
throw new Error('yam latest and exact-version registry metadata disagree');
|
|
451
|
+
}
|
|
452
|
+
const artifactStatus = await deps.probeArtifact(tarball);
|
|
453
|
+
if (artifactStatus === 404) return pending;
|
|
454
|
+
if (artifactStatus !== 200) throw new Error(`yam tarball availability check failed: HTTP ${artifactStatus}`);
|
|
418
455
|
return {
|
|
419
456
|
...componentCheck('yam', installed, latest, 'npm:yam-flow'),
|
|
420
457
|
source_receipt: {
|
|
421
458
|
canonical_url: YAM_REGISTRY_URL,
|
|
422
459
|
retrieved_at: deps.now().toISOString(),
|
|
423
|
-
access_path: 'npm
|
|
460
|
+
access_path: 'npm latest + exact-version metadata + bounded no-redirect tarball HEAD (availability only)',
|
|
424
461
|
version: latest,
|
|
425
462
|
revision,
|
|
426
463
|
integrity: [integrity]
|
|
@@ -601,7 +638,7 @@ function failedCheck(
|
|
|
601
638
|
|
|
602
639
|
async function applyYam(currentYamVersion: string, deps: ResolvedDependencies) {
|
|
603
640
|
const check = await checkYam(currentYamVersion, deps);
|
|
604
|
-
if (check.status === 'check_failed') return persistFailureFromCheck(check, deps);
|
|
641
|
+
if (check.status === 'check_failed' || check.status === 'pending_distribution') return persistFailureFromCheck(check, deps);
|
|
605
642
|
if (!check.update_available) {
|
|
606
643
|
return persistReceipt(baseReceipt(check, 'up_to_date', [], {
|
|
607
644
|
automatic: true,
|
|
@@ -1609,14 +1646,58 @@ async function defaultRun(command: string, args: string[], options: ExternalComm
|
|
|
1609
1646
|
|
|
1610
1647
|
async function defaultFetchJson(url: string) {
|
|
1611
1648
|
const response = await fetch(url, {
|
|
1649
|
+
redirect: 'error',
|
|
1650
|
+
credentials: 'omit',
|
|
1612
1651
|
headers: {
|
|
1613
1652
|
accept: 'application/json',
|
|
1614
1653
|
'user-agent': 'yam-flow external-update-check'
|
|
1615
1654
|
},
|
|
1616
1655
|
signal: AbortSignal.timeout(30000)
|
|
1617
1656
|
});
|
|
1618
|
-
if (!response.ok)
|
|
1619
|
-
|
|
1657
|
+
if (!response.ok) {
|
|
1658
|
+
await response.body?.cancel();
|
|
1659
|
+
throw new RegistryHttpError(response.status, url);
|
|
1660
|
+
}
|
|
1661
|
+
const reader = response.body?.getReader();
|
|
1662
|
+
if (!reader) throw new Error(`empty registry response for ${url}`);
|
|
1663
|
+
const chunks: Uint8Array[] = [];
|
|
1664
|
+
let size = 0;
|
|
1665
|
+
try {
|
|
1666
|
+
while (true) {
|
|
1667
|
+
const chunk = await reader.read();
|
|
1668
|
+
if (chunk.done) break;
|
|
1669
|
+
size += chunk.value.byteLength;
|
|
1670
|
+
if (size > 1024 * 1024) throw new Error('registry metadata exceeds 1 MiB limit');
|
|
1671
|
+
chunks.push(chunk.value);
|
|
1672
|
+
}
|
|
1673
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
1674
|
+
} finally {
|
|
1675
|
+
await reader.cancel().catch(() => undefined);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
export class RegistryHttpError extends Error {
|
|
1680
|
+
constructor(readonly status: number, url: string) {
|
|
1681
|
+
super(`HTTP ${status} for ${url}`);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
function requireYamTarball(value: unknown, version: string) {
|
|
1686
|
+
const expected = `${NPM_REGISTRY}yam-flow/-/yam-flow-${version}.tgz`;
|
|
1687
|
+
if (value !== expected) throw new Error('yam tarball must be the exact-version artifact on the official npm registry');
|
|
1688
|
+
return expected;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
async function defaultProbeArtifact(url: string) {
|
|
1692
|
+
const response = await fetch(url, {
|
|
1693
|
+
method: 'HEAD',
|
|
1694
|
+
redirect: 'error',
|
|
1695
|
+
credentials: 'omit',
|
|
1696
|
+
headers: { 'user-agent': 'yam-flow external-update-check' },
|
|
1697
|
+
signal: AbortSignal.timeout(10000)
|
|
1698
|
+
});
|
|
1699
|
+
await response.body?.cancel();
|
|
1700
|
+
return response.status;
|
|
1620
1701
|
}
|
|
1621
1702
|
|
|
1622
1703
|
function requireStableVersion(value: unknown, label: string) {
|