yam-flow 2.8.0 → 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/AGENTS.md +4 -0
- package/CHANGELOG.md +22 -0
- package/COMMANDS.md +17 -2
- package/README.md +5 -1
- package/dist/bin/yam.js +162 -42
- 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/security-check.d.ts +98 -0
- package/dist/lib/security-check.js +262 -0
- package/dist/lib/security-integration.d.ts +8 -0
- package/dist/lib/security-integration.js +428 -0
- package/dist/lib/verification-closure.d.ts +1 -1
- package/package.json +10 -3
- package/references/final-report.md +2 -0
- package/references/security-check.md +131 -0
- package/references/study-note.md +6 -0
- package/skills/deep/SKILL.md +4 -0
- package/skills/mission/SKILL.md +4 -0
- package/skills/quick/SKILL.md +4 -0
- package/skills/ueye/SKILL.md +4 -0
- package/src/bin/yam.ts +142 -39
- 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/src/lib/security-check.ts +184 -0
- package/src/lib/security-integration.ts +299 -0
- package/templates/security-check-note.md +42 -0
- package/templates/security-gate.yml +44 -0
- package/yam.manifest.json +1 -1
package/references/study-note.md
CHANGED
|
@@ -48,6 +48,12 @@ Study Note:
|
|
|
48
48
|
- Limits: this does not infer requirements from prose; the agent or user still has to provide them.
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
## Security check in Yam note
|
|
52
|
+
|
|
53
|
+
When documenting commit, push, or deployment security checks, include a separate `-- Security check --` block using `references/security-check.md` and `templates/security-check-note.md`. Every SEC ID must have a concrete Korean security explanation, its status, and supporting evidence or an explicit limitation. Keep this block outside the Study Note / Next step pair so their required adjacency is preserved.
|
|
54
|
+
|
|
55
|
+
The security CLI appends this block to the configured note without adding a `yam note` command or changing `yam.study-note.v1`. Git hooks require explicit setup; remote CI/deployment enforcement requires project configuration.
|
|
56
|
+
|
|
51
57
|
## When Nothing Changed
|
|
52
58
|
|
|
53
59
|
For pure Q&A, planning, scouting, or review with no changed artifact, do not force a Study Note. Say `No code or project artifact changed` when that prevents ambiguity.
|
package/skills/deep/SKILL.md
CHANGED
|
@@ -73,6 +73,10 @@ Stop condition:
|
|
|
73
73
|
- Stop earlier when the same blocker repeats, a required tool/auth/runtime is unavailable, or a user decision is needed.
|
|
74
74
|
- Do not keep retrying the same failure; classify the cause, give a recovery hint, and hand off.
|
|
75
75
|
|
|
76
|
+
## Commit, push and deployment security gate
|
|
77
|
+
|
|
78
|
+
Before performing a requested commit, push or deployment, follow `references/security-check.md`: validate stage-bound evidence with `yam security check`, require a successful persisted Security check note, and stop the operation when the gate blocks. If the project is not configured, report that blocker rather than claim security approval. Do not overwrite existing hooks or treat supplied evidence as an automatic vulnerability scan.
|
|
79
|
+
|
|
76
80
|
## Final Response
|
|
77
81
|
|
|
78
82
|
Include:
|
package/skills/mission/SKILL.md
CHANGED
|
@@ -120,6 +120,10 @@ Good mission prompts include:
|
|
|
120
120
|
|
|
121
121
|
If the user invokes `$mission` without an approved plan, ask for the plan or propose a compact plan first.
|
|
122
122
|
|
|
123
|
+
## Commit, push and deployment security gate
|
|
124
|
+
|
|
125
|
+
Before performing a requested commit, push or deployment, follow `references/security-check.md`: validate stage-bound evidence with `yam security check`, require a successful persisted Security check note, and stop the operation when the gate blocks. If the project is not configured, report that blocker rather than claim security approval. Do not overwrite existing hooks or treat supplied evidence as an automatic vulnerability scan.
|
|
126
|
+
|
|
123
127
|
## Final Response
|
|
124
128
|
|
|
125
129
|
Report:
|
package/skills/quick/SKILL.md
CHANGED
|
@@ -74,6 +74,10 @@ Use `references/study-note.md` when any code, config, docs, or project artifact
|
|
|
74
74
|
Use `references/final-report.md` to close with remaining tasks and fix-first items when useful.
|
|
75
75
|
Use `references/token-budget-reporter.md` when a run needs measured budget feedback.
|
|
76
76
|
|
|
77
|
+
## Commit, push and deployment security gate
|
|
78
|
+
|
|
79
|
+
Before performing a requested commit, push or deployment, follow `references/security-check.md`: validate stage-bound evidence with `yam security check`, require a successful persisted Security check note, and stop the operation when the gate blocks. If the project is not configured, report that blocker rather than claim security approval. Do not overwrite existing hooks or treat supplied evidence as an automatic vulnerability scan.
|
|
80
|
+
|
|
77
81
|
## Final Response
|
|
78
82
|
|
|
79
83
|
Keep it compact:
|
package/skills/ueye/SKILL.md
CHANGED
|
@@ -182,6 +182,10 @@ Always consider:
|
|
|
182
182
|
- Interaction affordance.
|
|
183
183
|
- Consistency with the project's visual language.
|
|
184
184
|
|
|
185
|
+
## Commit, push and deployment security gate
|
|
186
|
+
|
|
187
|
+
Before performing a requested commit, push or deployment, follow `references/security-check.md`: validate stage-bound evidence with `yam security check`, require a successful persisted Security check note, and stop the operation when the gate blocks. If the project is not configured, report that blocker rather than claim security approval. Do not overwrite existing hooks or treat supplied evidence as an automatic vulnerability scan.
|
|
188
|
+
|
|
185
189
|
## Final Response
|
|
186
190
|
|
|
187
191
|
Report:
|
package/src/bin/yam.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
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';
|
|
5
|
+
import { runSecurityCommand } from '../lib/security-integration.js';
|
|
3
6
|
import { createHash } from 'node:crypto';
|
|
4
7
|
import fs from 'node:fs';
|
|
5
8
|
import fsp from 'node:fs/promises';
|
|
@@ -148,8 +151,8 @@ const INSTRUCTION_DUPLICATION_LIMITS = {
|
|
|
148
151
|
minDirectiveChars: 40
|
|
149
152
|
};
|
|
150
153
|
const YAM_HOOK_ENTRYPOINT = path.join(ROOT, 'dist', 'bin', 'yam.js');
|
|
151
|
-
|
|
152
|
-
|
|
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`;
|
|
153
156
|
const REQUIRED_PACK_SECTIONS = [
|
|
154
157
|
'Product Direction',
|
|
155
158
|
'UI Direction',
|
|
@@ -224,6 +227,7 @@ Usage:
|
|
|
224
227
|
yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
|
|
225
228
|
yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
|
|
226
229
|
yam study-note check [dir] [--report file|--text text] [--json]
|
|
230
|
+
yam security <init|check|hooks|status> [--stage commit|push|ci|deploy] [--json]
|
|
227
231
|
yam next-step <report|verify> [--spec file|--receipt file] [--json]
|
|
228
232
|
yam loop report [--route route] [--intent text] [--stage id:status:note] [--evidence text] [--json]
|
|
229
233
|
yam scout receipt <create|verify> [options]
|
|
@@ -249,7 +253,7 @@ Usage:
|
|
|
249
253
|
yam safety [text...]
|
|
250
254
|
yam memory <init|add|list|summary|resolve> [dir] [options]
|
|
251
255
|
yam hook <status|enable|disable|run> [lite|study-note] [--global|--project dir]
|
|
252
|
-
yam template <project|ueye|mission|proof|tuning>
|
|
256
|
+
yam template <project|ueye|mission|proof|tuning|security|security-ci>
|
|
253
257
|
yam tune-log [dir]
|
|
254
258
|
yam install [--dry-run] [--json] [--replace-user-skill name]
|
|
255
259
|
yam uninstall
|
|
@@ -570,6 +574,7 @@ async function verify({ quiet = false } = {}) {
|
|
|
570
574
|
'context-reuse.md',
|
|
571
575
|
'markdown-management.md',
|
|
572
576
|
'study-note.md',
|
|
577
|
+
'security-check.md',
|
|
573
578
|
'final-report.md',
|
|
574
579
|
'token-budget-reporter.md',
|
|
575
580
|
'memory.md'
|
|
@@ -585,7 +590,7 @@ async function verify({ quiet = false } = {}) {
|
|
|
585
590
|
if (!hasHeading(projectTemplate, section)) issues.push(`project template missing section: ${section}`);
|
|
586
591
|
}
|
|
587
592
|
}
|
|
588
|
-
for (const template of ['ueye-review.md', 'ueye-comparison.md', 'mission-plan.md', 'runtime-proof.md', 'tuning-log.md']) {
|
|
593
|
+
for (const template of ['ueye-review.md', 'ueye-comparison.md', 'mission-plan.md', 'runtime-proof.md', 'tuning-log.md', 'security-check-note.md', 'security-gate.yml']) {
|
|
589
594
|
if (!await exists(path.join(ROOT, 'templates', template))) issues.push(`missing template: ${template}`);
|
|
590
595
|
}
|
|
591
596
|
for (const module of ['trust-kernel.js', 'ueye-artifacts.js']) {
|
|
@@ -2011,6 +2016,7 @@ async function hook(args = []) {
|
|
|
2011
2016
|
const subcommand = args[0] || 'status';
|
|
2012
2017
|
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return hookUsage();
|
|
2013
2018
|
if (subcommand === 'status') return hookStatus(args.slice(1));
|
|
2019
|
+
if (subcommand === 'migrate') return hookMigrate(args.slice(1));
|
|
2014
2020
|
if (subcommand === 'enable') return hookEnable(args.slice(1));
|
|
2015
2021
|
if (subcommand === 'disable') return hookDisable(args.slice(1));
|
|
2016
2022
|
if (subcommand === 'run') return hookRun(args.slice(1));
|
|
@@ -2022,14 +2028,17 @@ function hookUsage() {
|
|
|
2022
2028
|
console.log(`yam hook
|
|
2023
2029
|
|
|
2024
2030
|
Usage:
|
|
2025
|
-
yam hook status [--global|--project dir]
|
|
2026
|
-
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]
|
|
2027
2034
|
yam hook disable [lite|study-note] [--global|--project dir]
|
|
2028
2035
|
yam hook run <lite|study-note>
|
|
2029
2036
|
|
|
2030
2037
|
Notes:
|
|
2031
2038
|
hooks are opt-in. lite is advisory-only; study-note adds a prompt reminder and a one-pass Stop completion gate.
|
|
2032
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.
|
|
2033
2042
|
`);
|
|
2034
2043
|
}
|
|
2035
2044
|
|
|
@@ -2054,7 +2063,7 @@ function parseHookArgs(args = []) {
|
|
|
2054
2063
|
}
|
|
2055
2064
|
|
|
2056
2065
|
function hookPathFor(parsed) {
|
|
2057
|
-
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');
|
|
2058
2067
|
return path.join(path.resolve(parsed.projectDir || process.cwd()), '.codex', 'hooks.json');
|
|
2059
2068
|
}
|
|
2060
2069
|
|
|
@@ -2070,19 +2079,42 @@ function isYamHookProfile(handler: AnyRecord = {}, profile = 'lite') {
|
|
|
2070
2079
|
return profile === 'study-note' ? isYamStudyNoteHook(handler) : isYamLiteHook(handler);
|
|
2071
2080
|
}
|
|
2072
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
|
+
|
|
2073
2105
|
function stripYamHooks(config: AnyRecord = {}, profile = 'lite') {
|
|
2074
|
-
const next =
|
|
2075
|
-
for (const event of Object.keys(next)) {
|
|
2076
|
-
if (!Array.isArray(next[event])) continue;
|
|
2077
|
-
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];
|
|
2078
2110
|
const keptEntries = [];
|
|
2079
2111
|
for (const entry of entries) {
|
|
2080
2112
|
const hooks = Array.isArray(entry?.hooks) ? entry.hooks.filter((handler) => !isYamHookProfile(handler, profile)) : [];
|
|
2081
2113
|
const rest = { ...entry, hooks };
|
|
2082
2114
|
if (hooks.length > 0) keptEntries.push(rest);
|
|
2083
2115
|
}
|
|
2084
|
-
if (keptEntries.length > 0) next[event] = keptEntries;
|
|
2085
|
-
else delete next[event];
|
|
2116
|
+
if (keptEntries.length > 0) next.hooks[event] = keptEntries;
|
|
2117
|
+
else delete next.hooks[event];
|
|
2086
2118
|
}
|
|
2087
2119
|
return next;
|
|
2088
2120
|
}
|
|
@@ -2100,7 +2132,7 @@ function withYamHook(config: AnyRecord = {}, profile = 'lite') {
|
|
|
2100
2132
|
}
|
|
2101
2133
|
]
|
|
2102
2134
|
};
|
|
2103
|
-
next[event] = [...(Array.isArray(next[event]) ? next[event] : []), entry];
|
|
2135
|
+
next.hooks[event] = [...(Array.isArray(next.hooks[event]) ? next.hooks[event] : []), entry];
|
|
2104
2136
|
}
|
|
2105
2137
|
return next;
|
|
2106
2138
|
}
|
|
@@ -2110,6 +2142,11 @@ async function hookStatus(args = []) {
|
|
|
2110
2142
|
const target = hookPathFor(parsed);
|
|
2111
2143
|
const loaded = await readHookConfig(target);
|
|
2112
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
|
+
}
|
|
2113
2150
|
console.log('yam-lite hook: broken');
|
|
2114
2151
|
console.log('yam-study-note hook: broken');
|
|
2115
2152
|
console.log(` - hook config unreadable: ${loaded.error}`);
|
|
@@ -2120,10 +2157,28 @@ async function hookStatus(args = []) {
|
|
|
2120
2157
|
}
|
|
2121
2158
|
const lite = await inspectHookProfile(loaded.config, 'lite');
|
|
2122
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
|
+
}
|
|
2123
2175
|
printHookProfileStatus('lite', lite);
|
|
2124
2176
|
printHookProfileStatus('study-note', studyNote);
|
|
2125
2177
|
console.log(`scope: ${parsed.mode}`);
|
|
2126
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)`);
|
|
2127
2182
|
if (lite.state === 'broken' || studyNote.state === 'broken') process.exitCode = 1;
|
|
2128
2183
|
}
|
|
2129
2184
|
|
|
@@ -2132,7 +2187,7 @@ function hookConfigHasYamLite(config = {}) {
|
|
|
2132
2187
|
}
|
|
2133
2188
|
|
|
2134
2189
|
function hookConfigHasProfile(config = {}, profile = 'lite') {
|
|
2135
|
-
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) => {
|
|
2136
2191
|
return Array.isArray(entry?.hooks) && entry.hooks.some((handler) => isYamHookProfile(handler, profile));
|
|
2137
2192
|
}));
|
|
2138
2193
|
}
|
|
@@ -2147,7 +2202,7 @@ function hookEventsForProfile(profile = 'lite') {
|
|
|
2147
2202
|
|
|
2148
2203
|
function hookProfileEntries(config: AnyRecord = {}, profile = 'lite') {
|
|
2149
2204
|
const matches = [];
|
|
2150
|
-
for (const [event, entries] of Object.entries(config)) {
|
|
2205
|
+
for (const [event, entries] of Object.entries(hookEventMap(config))) {
|
|
2151
2206
|
if (!Array.isArray(entries)) continue;
|
|
2152
2207
|
for (const entry of entries) {
|
|
2153
2208
|
if (!Array.isArray(entry?.hooks)) continue;
|
|
@@ -2206,6 +2261,7 @@ function splitHookCommand(command = '') {
|
|
|
2206
2261
|
|
|
2207
2262
|
function parseYamHookCommand(command = '', profile = 'lite') {
|
|
2208
2263
|
const words = splitHookCommand(command);
|
|
2264
|
+
if (words.at(-1) === '--observe') words.pop();
|
|
2209
2265
|
if (words.length === 5 && words[2] === 'hook' && words[3] === 'run' && words[4] === profile) {
|
|
2210
2266
|
const nodeName = path.basename(words[0]).toLowerCase();
|
|
2211
2267
|
if (/^node(?:\.exe)?$/.test(nodeName) && path.basename(words[1]).toLowerCase() === 'yam.js') {
|
|
@@ -2246,6 +2302,7 @@ async function inspectHookProfile(config: AnyRecord = {}, profile = 'lite') {
|
|
|
2246
2302
|
const entries = hookProfileEntries(config, profile);
|
|
2247
2303
|
if (!entries.length) return { state: 'disabled', issues: [], entries };
|
|
2248
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');
|
|
2249
2306
|
for (const event of hookEventsForProfile(profile)) {
|
|
2250
2307
|
const count = entries.filter((entry) => entry.event === event).length;
|
|
2251
2308
|
if (count === 0) issues.push(`${event} handler is missing; rerun yam hook enable ${profile}`);
|
|
@@ -2272,6 +2329,9 @@ async function readHookConfig(target) {
|
|
|
2272
2329
|
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
2273
2330
|
return { config: {}, exists: true, error: 'top-level value must be a JSON object' };
|
|
2274
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
|
+
}
|
|
2275
2335
|
return { config: value, exists: true, error: '' };
|
|
2276
2336
|
} catch (error) {
|
|
2277
2337
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { config: {}, exists: false, error: '' };
|
|
@@ -2291,8 +2351,34 @@ async function writeHookConfig(target, config) {
|
|
|
2291
2351
|
}
|
|
2292
2352
|
}
|
|
2293
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
|
+
|
|
2294
2376
|
async function hookEnable(args = []) {
|
|
2295
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}`;
|
|
2296
2382
|
if (!['lite', 'study-note'].includes(parsed.profile)) {
|
|
2297
2383
|
console.error('Only lite and study-note hooks are supported: yam hook enable <lite|study-note>');
|
|
2298
2384
|
process.exitCode = 1;
|
|
@@ -2323,6 +2409,7 @@ async function hookEnable(args = []) {
|
|
|
2323
2409
|
}
|
|
2324
2410
|
await writeHookConfig(target, next);
|
|
2325
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)`);
|
|
2326
2413
|
if (needsMigration) console.log(`migrated: ${existing.length} existing yam-${parsed.profile} handler(s) to the current command and event coverage`);
|
|
2327
2414
|
console.log('Restart Codex or start a new thread if the app does not pick up hook changes immediately.');
|
|
2328
2415
|
}
|
|
@@ -2343,7 +2430,7 @@ async function hookDisable(args = []) {
|
|
|
2343
2430
|
return;
|
|
2344
2431
|
}
|
|
2345
2432
|
const next = stripYamHooks(current, parsed.profile);
|
|
2346
|
-
if (Object.keys(next).length === 0) {
|
|
2433
|
+
if (Object.keys(next).length === 1 && Object.keys(next.hooks).length === 0) {
|
|
2347
2434
|
await rmrf(target);
|
|
2348
2435
|
} else {
|
|
2349
2436
|
await writeHookConfig(target, next);
|
|
@@ -2371,6 +2458,7 @@ async function hookRun(args = []) {
|
|
|
2371
2458
|
}
|
|
2372
2459
|
const event = input?.hook_event_name || input?.hookEventName || input?.event || 'UserPromptSubmit';
|
|
2373
2460
|
const cwd = String(input?.cwd || process.cwd());
|
|
2461
|
+
if (args.includes('--observe')) await recordHookObservation(cwd, profile, String(event), YAM_HOOK_ENTRYPOINT).catch(() => {});
|
|
2374
2462
|
if (profile === 'study-note' && event === 'Stop') {
|
|
2375
2463
|
console.log(JSON.stringify(await buildStudyNoteStopOutput(input, cwd)));
|
|
2376
2464
|
return;
|
|
@@ -6069,11 +6157,13 @@ async function printTemplate(name = '') {
|
|
|
6069
6157
|
mission: 'mission-plan.md',
|
|
6070
6158
|
proof: 'runtime-proof.md',
|
|
6071
6159
|
runtime: 'runtime-proof.md',
|
|
6072
|
-
tuning: 'tuning-log.md'
|
|
6160
|
+
tuning: 'tuning-log.md',
|
|
6161
|
+
security: 'security-check-note.md',
|
|
6162
|
+
'security-ci': 'security-gate.yml'
|
|
6073
6163
|
};
|
|
6074
6164
|
const file = map[key];
|
|
6075
6165
|
if (!file) {
|
|
6076
|
-
console.error('usage: yam template <project|ueye|ueye-comparison|mission|proof|tuning>');
|
|
6166
|
+
console.error('usage: yam template <project|ueye|ueye-comparison|mission|proof|tuning|security|security-ci>');
|
|
6077
6167
|
process.exitCode = 1;
|
|
6078
6168
|
return;
|
|
6079
6169
|
}
|
|
@@ -6104,7 +6194,7 @@ async function memory(args = []) {
|
|
|
6104
6194
|
}
|
|
6105
6195
|
if (subcommand === 'summary' || subcommand === 'summarize') {
|
|
6106
6196
|
const parsed = parseMemoryArgs(args.slice(1));
|
|
6107
|
-
return memorySummary(parsed.dir);
|
|
6197
|
+
return memorySummary(parsed.dir, { json: args.includes('--json') });
|
|
6108
6198
|
}
|
|
6109
6199
|
if (subcommand === 'resolve') return memoryResolve(args.slice(1));
|
|
6110
6200
|
console.error(`unknown memory command: ${subcommand}`);
|
|
@@ -6118,7 +6208,7 @@ Usage:
|
|
|
6118
6208
|
yam memory init [dir]
|
|
6119
6209
|
yam memory add [dir] --kind <kind> --summary <text> [--evidence <text>] [--action <text>] [--source <text>]
|
|
6120
6210
|
yam memory list [dir] [--json]
|
|
6121
|
-
yam memory summary [dir]
|
|
6211
|
+
yam memory summary [dir] [--json]
|
|
6122
6212
|
yam memory resolve [dir] <id> [--note <text>]
|
|
6123
6213
|
|
|
6124
6214
|
Kinds:
|
|
@@ -6193,7 +6283,14 @@ async function memoryAdd(args = []) {
|
|
|
6193
6283
|
}
|
|
6194
6284
|
|
|
6195
6285
|
async function memoryList(targetDir = process.cwd(), { json = false } = {}) {
|
|
6196
|
-
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
|
+
}
|
|
6197
6294
|
if (json) {
|
|
6198
6295
|
console.log(JSON.stringify(records, null, 2));
|
|
6199
6296
|
return;
|
|
@@ -6208,9 +6305,16 @@ async function memoryList(targetDir = process.cwd(), { json = false } = {}) {
|
|
|
6208
6305
|
}
|
|
6209
6306
|
}
|
|
6210
6307
|
|
|
6211
|
-
async function memorySummary(targetDir = process.cwd()) {
|
|
6308
|
+
async function memorySummary(targetDir = process.cwd(), { json = false } = {}) {
|
|
6212
6309
|
const dir = memoryDir(targetDir);
|
|
6213
|
-
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;
|
|
6214
6318
|
await fsp.mkdir(dir, { recursive: true });
|
|
6215
6319
|
const active = records.filter((record) => record.status !== 'resolved');
|
|
6216
6320
|
const resolved = records.filter((record) => record.status === 'resolved');
|
|
@@ -6241,7 +6345,8 @@ async function memorySummary(targetDir = process.cwd()) {
|
|
|
6241
6345
|
|
|
6242
6346
|
const target = path.join(dir, 'summary.md');
|
|
6243
6347
|
await fsp.writeFile(target, `${lines.join('\n').trim()}\n`);
|
|
6244
|
-
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}`);
|
|
6245
6350
|
}
|
|
6246
6351
|
|
|
6247
6352
|
async function memoryResolve(args = []) {
|
|
@@ -6300,20 +6405,7 @@ function memoryDir(targetDir = process.cwd()) {
|
|
|
6300
6405
|
|
|
6301
6406
|
async function readMemoryRecords(targetDir = process.cwd()) {
|
|
6302
6407
|
const recordsDir = path.join(memoryDir(targetDir), 'records');
|
|
6303
|
-
|
|
6304
|
-
const entries = await fsp.readdir(recordsDir, { withFileTypes: true });
|
|
6305
|
-
const records = [];
|
|
6306
|
-
for (const entry of entries) {
|
|
6307
|
-
if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
|
|
6308
|
-
const file = path.join(recordsDir, entry.name);
|
|
6309
|
-
try {
|
|
6310
|
-
const record = await readJson(file);
|
|
6311
|
-
records.push(record);
|
|
6312
|
-
} catch (error) {
|
|
6313
|
-
records.push({ id: entry.name.replace(/\.json$/, ''), kind: 'invalid', status: 'invalid', summary: `invalid JSON: ${errorMessage(error)}` });
|
|
6314
|
-
}
|
|
6315
|
-
}
|
|
6316
|
-
return records.sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || '')));
|
|
6408
|
+
return readMemoryDirectory(recordsDir);
|
|
6317
6409
|
}
|
|
6318
6410
|
|
|
6319
6411
|
function timestampId() {
|
|
@@ -6349,6 +6441,7 @@ function showRequestedHelp(args: string[]) {
|
|
|
6349
6441
|
}
|
|
6350
6442
|
|
|
6351
6443
|
const commandUsage: Record<string, () => void> = {
|
|
6444
|
+
security: () => console.log('yam security <init|check|hooks enable|status> [--stage commit|push|ci|deploy] [--policy file --policy-digest sha256 --evidence file --note file]\nSee references/security-check.md for exact bindings and CI/deploy trust configuration.'),
|
|
6352
6445
|
context: contextUsage,
|
|
6353
6446
|
cleanup: cleanupUsage,
|
|
6354
6447
|
'study-note': studyNoteUsage,
|
|
@@ -6375,6 +6468,16 @@ async function main() {
|
|
|
6375
6468
|
const command = args[0] || 'help';
|
|
6376
6469
|
if (showRequestedHelp(args)) return;
|
|
6377
6470
|
if (command === 'help') return usage();
|
|
6471
|
+
if (command === 'security') {
|
|
6472
|
+
if (args.length === 1 || ['help', '--help', '-h'].includes(args[1])) {
|
|
6473
|
+
console.log('yam security <init|check|hooks enable|status> [--stage commit|push|ci|deploy] [--policy file --policy-digest sha256 --evidence file --note file]\nSee references/security-check.md for exact bindings and CI/deploy trust configuration.');
|
|
6474
|
+
return;
|
|
6475
|
+
}
|
|
6476
|
+
const result = await runSecurityCommand(args.slice(1), { cwd: process.cwd(), cliPath: fileURLToPath(import.meta.url) });
|
|
6477
|
+
console.log(JSON.stringify(result, null, 2));
|
|
6478
|
+
if (!result.ok) process.exitCode = 1;
|
|
6479
|
+
return;
|
|
6480
|
+
}
|
|
6378
6481
|
if (command === 'install') return install(args.slice(1));
|
|
6379
6482
|
if (command === 'uninstall') return uninstall(args.slice(1));
|
|
6380
6483
|
if (command === 'version') return console.log(VERSION);
|
|
@@ -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) {
|