hyperframes 0.8.81 → 0.8.82
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/dist/backgroundChecksWorker.js +890 -0
- package/dist/cli.js +2928 -1809
- package/dist/commands/layout-audit.browser.js +55 -25
- package/dist/fontLocalizeCli.js +417 -134
- package/dist/hyperframe-runtime.js +53 -53
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +53 -53
- package/dist/hyperframes-player.global.js +3 -3
- package/dist/registry/localSemantic.js +7 -3
- package/dist/renderSetupWorker.js +62 -32
- package/dist/sherpaWorker.js +179 -0
- package/dist/skills/hyperframes/SKILL.md +2 -0
- package/dist/skills/hyperframes/references/plugin-installation.md +43 -0
- package/dist/skills/hyperframes/references/skill-lifecycle.md +2 -0
- package/dist/skills/hyperframes/scripts/plugin-cli.mjs +88 -0
- package/dist/skills/hyperframes/scripts/plugin-cli.test.mjs +117 -0
- package/dist/skills/hyperframes-cli/SKILL.md +2 -0
- package/dist/skills/hyperframes-cli/references/upgrade-info-misc.md +1 -0
- package/dist/skills/media-use/SKILL.md +2 -0
- package/dist/skills/media-use/references/operations.md +7 -5
- package/dist/skills/media-use/references/setup-providers.md +9 -9
- package/dist/skills/media-use/registry/localSemantic.js +7 -3
- package/dist/skills/media-use/scripts/lib/ff-binaries.mjs +37 -0
- package/dist/skills/media-use/scripts/lib/probe.mjs +3 -1
- package/dist/skills/media-use/scripts/lib/probe.test.mjs +118 -1
- package/dist/skills/media-use/scripts/lib/tts-local-provider.mjs +5 -3
- package/dist/skills/media-use/scripts/resolve-plugin.test.mjs +62 -0
- package/dist/skills/media-use/scripts/resolve.mjs +45 -19
- package/dist/skills/media-use/scripts/transcribe.mjs +39 -25
- package/dist/skills/media-use/scripts/transcribe.test.mjs +112 -0
- package/dist/skills/registry/localSemantic.js +7 -3
- package/dist/studio/assets/{hyperframes-player-C68UawR3.js → hyperframes-player-zGIbZhLJ.js} +1 -1
- package/dist/studio/assets/{index-CMzPGv9P.js → index-BiLCTunu.js} +1 -1
- package/dist/studio/assets/{index-hNVM9Pd2.js → index-DSazIt7R.js} +189 -189
- package/dist/studio/assets/{index-CS5SImRT.js → index-DStmfnpZ.js} +1 -1
- package/dist/studio/index.d.ts +172 -7
- package/dist/studio/index.html +1 -1
- package/dist/studio/index.js +2414 -2172
- package/dist/studio/index.js.map +1 -1
- package/dist/templates/_shared/AGENTS.md +5 -1
- package/dist/templates/_shared/CLAUDE.md +5 -1
- package/package.json +1 -1
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
import { createRequire as __hf_createRequire } from "node:module";
|
|
2
|
+
import { fileURLToPath as __hf_fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname as __hf_dirname } from "node:path";
|
|
4
|
+
var require = __hf_createRequire(import.meta.url);
|
|
5
|
+
var __filename = __hf_fileURLToPath(import.meta.url);
|
|
6
|
+
var __dirname = __hf_dirname(__filename);
|
|
7
|
+
|
|
8
|
+
// src/utils/updateCheck.ts
|
|
9
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
10
|
+
import { join as join2 } from "path";
|
|
11
|
+
import { compareVersions } from "compare-versions";
|
|
12
|
+
|
|
13
|
+
// src/telemetry/config.ts
|
|
14
|
+
import * as fs from "fs";
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { homedir } from "os";
|
|
18
|
+
import { randomUUID } from "crypto";
|
|
19
|
+
|
|
20
|
+
// src/utils/errorMessage.ts
|
|
21
|
+
function normalizeErrorMessage(error) {
|
|
22
|
+
if (error instanceof Error) return error.message;
|
|
23
|
+
if (typeof error === "string") return error;
|
|
24
|
+
if (typeof error === "object" && error !== null) {
|
|
25
|
+
const msg = error.message;
|
|
26
|
+
if (typeof msg === "string") return msg;
|
|
27
|
+
try {
|
|
28
|
+
return JSON.stringify(error);
|
|
29
|
+
} catch {
|
|
30
|
+
try {
|
|
31
|
+
return `{${Object.keys(error).join(", ")}}`;
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return String(error ?? "unknown error");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/utils/env.ts
|
|
40
|
+
function isDevMode() {
|
|
41
|
+
try {
|
|
42
|
+
const url = new URL(import.meta.url);
|
|
43
|
+
return url.pathname.endsWith(".ts");
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/telemetry/posthogKey.ts
|
|
50
|
+
var POSTHOG_API_KEY = "phc_zjjbX0PnWxERXrMHhkEJWj9A9BhGVLRReICgsfTMmpx";
|
|
51
|
+
|
|
52
|
+
// src/telemetry/policy.ts
|
|
53
|
+
var ENV_OPT_OUT_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
54
|
+
function isEnvOptOutValue(value) {
|
|
55
|
+
return value !== void 0 && ENV_OPT_OUT_VALUES.has(value.trim().toLowerCase());
|
|
56
|
+
}
|
|
57
|
+
function telemetryRuntimeOverride() {
|
|
58
|
+
if (isEnvOptOutValue(process.env["HYPERFRAMES_NO_TELEMETRY"])) {
|
|
59
|
+
return "HYPERFRAMES_NO_TELEMETRY";
|
|
60
|
+
}
|
|
61
|
+
if (isEnvOptOutValue(process.env["DO_NOT_TRACK"])) {
|
|
62
|
+
return "DO_NOT_TRACK";
|
|
63
|
+
}
|
|
64
|
+
if (isDevMode()) {
|
|
65
|
+
return "dev_mode";
|
|
66
|
+
}
|
|
67
|
+
if (!POSTHOG_API_KEY.startsWith("phc_")) {
|
|
68
|
+
return "telemetry_disabled_build";
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/utils/attendedTerminal.ts
|
|
74
|
+
function envFlagEnabled(name) {
|
|
75
|
+
const value = process.env[name]?.trim().toLowerCase();
|
|
76
|
+
return Boolean(value && value !== "0" && value !== "false" && value !== "no");
|
|
77
|
+
}
|
|
78
|
+
function isAttendedTerminal() {
|
|
79
|
+
return !envFlagEnabled("CI") && process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/media-use/lib/config-lock.mjs
|
|
83
|
+
function withFileLock(lockPath, fs2, task) {
|
|
84
|
+
const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
85
|
+
const started = Date.now();
|
|
86
|
+
for (; ; ) {
|
|
87
|
+
let fd;
|
|
88
|
+
try {
|
|
89
|
+
fd = fs2.openSync(lockPath, "wx");
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error.code !== "EEXIST") throw error;
|
|
92
|
+
let leftover = false;
|
|
93
|
+
try {
|
|
94
|
+
leftover = Date.now() - fs2.statSync(lockPath).mtimeMs > 5e3;
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
if (leftover || Date.now() - started > 1e4)
|
|
98
|
+
throw Object.assign(
|
|
99
|
+
new Error(
|
|
100
|
+
`Settings are locked by another hyperframes process. If none is running, delete ${lockPath}`
|
|
101
|
+
),
|
|
102
|
+
{ code: "HF_SETTINGS_LOCKED" }
|
|
103
|
+
);
|
|
104
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
fs2.writeSync(fd, token);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
fs2.rmSync(lockPath, { force: true });
|
|
111
|
+
throw error;
|
|
112
|
+
} finally {
|
|
113
|
+
fs2.closeSync(fd);
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
return task();
|
|
119
|
+
} finally {
|
|
120
|
+
try {
|
|
121
|
+
fs2.rmSync(lockPath);
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/telemetry/config.ts
|
|
128
|
+
var CONFIG_DIR = join(homedir(), ".hyperframes");
|
|
129
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
130
|
+
var STATE_FILE = join(CONFIG_DIR, "install-state.json");
|
|
131
|
+
var LEGACY_STATE_FILE = join(homedir(), ".local", "state", "hyperframes", "install-state.json");
|
|
132
|
+
function salvageInstallState(parsed) {
|
|
133
|
+
const markerAt = typeof parsed.markerAt === "string" ? parsed.markerAt : void 0;
|
|
134
|
+
const bucketSeed = parseNonEmptyString(parsed.bucketSeed);
|
|
135
|
+
const fired = parsed.deParallelRouterTrialFired === true ? true : void 0;
|
|
136
|
+
if (markerAt === void 0 && bucketSeed === void 0 && fired === void 0) return "corrupt";
|
|
137
|
+
return {
|
|
138
|
+
markerAt: markerAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
139
|
+
deParallelRouterTrialFired: fired,
|
|
140
|
+
bucketSeed
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function parseInstallState(file) {
|
|
144
|
+
if (!existsSync(file)) return "absent";
|
|
145
|
+
try {
|
|
146
|
+
return salvageInstallState(JSON.parse(readFileSync(file, "utf-8")));
|
|
147
|
+
} catch {
|
|
148
|
+
return "corrupt";
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
var seedBackfillWarned = false;
|
|
152
|
+
function warnSeedBackfillFailed(error) {
|
|
153
|
+
if (seedBackfillWarned) return;
|
|
154
|
+
seedBackfillWarned = true;
|
|
155
|
+
if (telemetryRuntimeOverride() !== null) return;
|
|
156
|
+
console.warn(
|
|
157
|
+
`[hyperframes] Could not persist telemetry config${error ? `: ${error}` : ""}. Canary cohort assignment will not be stable across runs.`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
function backfillBucketSeed(config) {
|
|
161
|
+
const recorded = readInstallState();
|
|
162
|
+
config.bucketSeed = (isInstallState(recorded) ? recorded.bucketSeed : void 0) ?? randomUUID();
|
|
163
|
+
const write = writeConfigWithResult(config);
|
|
164
|
+
if (!write.ok) warnSeedBackfillFailed(write.error);
|
|
165
|
+
return write;
|
|
166
|
+
}
|
|
167
|
+
var latchedFiredSeen = false;
|
|
168
|
+
function installStateLatchedFired() {
|
|
169
|
+
if (latchedFiredSeen) return true;
|
|
170
|
+
const state = readInstallState();
|
|
171
|
+
latchedFiredSeen = isInstallState(state) && state.deParallelRouterTrialFired === true;
|
|
172
|
+
return latchedFiredSeen;
|
|
173
|
+
}
|
|
174
|
+
function installStateSeed() {
|
|
175
|
+
const state = readInstallState();
|
|
176
|
+
return isInstallState(state) ? state.bucketSeed : void 0;
|
|
177
|
+
}
|
|
178
|
+
function isInstallState(value) {
|
|
179
|
+
return typeof value !== "string";
|
|
180
|
+
}
|
|
181
|
+
function readInstallState() {
|
|
182
|
+
const current = parseInstallState(STATE_FILE);
|
|
183
|
+
if (isInstallState(current)) {
|
|
184
|
+
removeLegacyStateFile();
|
|
185
|
+
return current;
|
|
186
|
+
}
|
|
187
|
+
const legacy = parseInstallState(LEGACY_STATE_FILE);
|
|
188
|
+
if (!isInstallState(legacy)) {
|
|
189
|
+
if (legacy === "corrupt") removeLegacyStateFile();
|
|
190
|
+
return current === "corrupt" || legacy === "corrupt" ? "corrupt" : "absent";
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
writeInstallState(legacy);
|
|
194
|
+
removeLegacyStateFile();
|
|
195
|
+
} catch {
|
|
196
|
+
}
|
|
197
|
+
return legacy;
|
|
198
|
+
}
|
|
199
|
+
function removeLegacyStateFile() {
|
|
200
|
+
try {
|
|
201
|
+
if (existsSync(LEGACY_STATE_FILE)) rmSync(LEGACY_STATE_FILE, { force: true });
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
var stateMarkerSynced = false;
|
|
206
|
+
var stateFiredSynced = false;
|
|
207
|
+
function writeInstallState(next) {
|
|
208
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
209
|
+
const tmpFile = `${STATE_FILE}.${process.pid}.tmp`;
|
|
210
|
+
writeFileSync(tmpFile, JSON.stringify(next, null, 2) + "\n", { mode: 384 });
|
|
211
|
+
renameSync(tmpFile, STATE_FILE);
|
|
212
|
+
}
|
|
213
|
+
function sameInstallState(a, b) {
|
|
214
|
+
return a.deParallelRouterTrialFired === b.deParallelRouterTrialFired && a.bucketSeed === b.bucketSeed;
|
|
215
|
+
}
|
|
216
|
+
function latchedFired(state, config) {
|
|
217
|
+
return state?.deParallelRouterTrialFired === true || config.deParallelRouterTrialFired === true || void 0;
|
|
218
|
+
}
|
|
219
|
+
function nextInstallState(state, config) {
|
|
220
|
+
const next = {
|
|
221
|
+
markerAt: state?.markerAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
222
|
+
deParallelRouterTrialFired: latchedFired(state, config),
|
|
223
|
+
// Write-once: an existing seed always wins, so cohorts stay
|
|
224
|
+
// anchored to the FIRST install in this config dir, not the latest one.
|
|
225
|
+
bucketSeed: state?.bucketSeed ?? config.bucketSeed
|
|
226
|
+
};
|
|
227
|
+
if (state !== null && sameInstallState(state, next)) return null;
|
|
228
|
+
return next;
|
|
229
|
+
}
|
|
230
|
+
function applyInstallState(config, wantFired) {
|
|
231
|
+
const read = readInstallState();
|
|
232
|
+
const state = isInstallState(read) ? read : null;
|
|
233
|
+
const next = nextInstallState(state, config);
|
|
234
|
+
if (next !== null) writeInstallState(next);
|
|
235
|
+
stateMarkerSynced = true;
|
|
236
|
+
stateFiredSynced = wantFired || state?.deParallelRouterTrialFired === true;
|
|
237
|
+
if (wantFired) latchedFiredSeen = true;
|
|
238
|
+
}
|
|
239
|
+
function syncInstallState(config) {
|
|
240
|
+
const wantFired = config.deParallelRouterTrialFired === true;
|
|
241
|
+
if (stateMarkerSynced && !existsSync(STATE_FILE)) {
|
|
242
|
+
stateMarkerSynced = false;
|
|
243
|
+
stateFiredSynced = false;
|
|
244
|
+
}
|
|
245
|
+
if (stateMarkerSynced && (stateFiredSynced || !wantFired)) return true;
|
|
246
|
+
try {
|
|
247
|
+
applyInstallState(config, wantFired);
|
|
248
|
+
return true;
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function mintAndCacheConfig() {
|
|
254
|
+
const config = mintConfig();
|
|
255
|
+
const write = writeConfigWithResult(config);
|
|
256
|
+
if (!write.ok) warnSeedBackfillFailed(write.error);
|
|
257
|
+
classifyIdentity(
|
|
258
|
+
config.anonymousId,
|
|
259
|
+
write.ok ? "unknown" : "process_only",
|
|
260
|
+
writeOutcomeOf(write)
|
|
261
|
+
);
|
|
262
|
+
cachedConfig = { ...config };
|
|
263
|
+
return { ...config };
|
|
264
|
+
}
|
|
265
|
+
function mintConfig() {
|
|
266
|
+
const read = readInstallState();
|
|
267
|
+
const state = isInstallState(read) ? read : null;
|
|
268
|
+
return {
|
|
269
|
+
...DEFAULT_CONFIG,
|
|
270
|
+
anonymousId: randomUUID(),
|
|
271
|
+
// A corrupt record still means this machine had an install — reporting it
|
|
272
|
+
// as `false` would count a partial disk write as a brand-new machine and
|
|
273
|
+
// understate recoverable churn, which is the one thing this field exists
|
|
274
|
+
// to measure. `undefined` on configs minted before the field existed.
|
|
275
|
+
predecessorFound: state !== null || read === "corrupt",
|
|
276
|
+
stateFileCorrupt: read === "corrupt" ? true : void 0,
|
|
277
|
+
// The rollover itself: a breaker tripped by a previous install on this
|
|
278
|
+
// machine stays tripped for the new one, and the canary bucketing seed is
|
|
279
|
+
// inherited so cohorts hold across a config.json re-mint.
|
|
280
|
+
deParallelRouterTrialFired: state?.deParallelRouterTrialFired === true ? true : void 0,
|
|
281
|
+
bucketSeed: state?.bucketSeed ?? randomUUID()
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
var MAX_RECENT_RENDERS = 5;
|
|
285
|
+
var DEFAULT_CONFIG = {
|
|
286
|
+
telemetryEnabled: true,
|
|
287
|
+
anonymousId: "",
|
|
288
|
+
telemetryNoticeShown: false,
|
|
289
|
+
commandCount: 0,
|
|
290
|
+
renderSuccessCount: 0,
|
|
291
|
+
lastFeedbackPromptAt: 0
|
|
292
|
+
};
|
|
293
|
+
var cachedConfig = null;
|
|
294
|
+
var identityClassification;
|
|
295
|
+
function classifyIdentity(anonymousId, persistence, writeOutcome) {
|
|
296
|
+
if (identityClassification?.anonymousId === anonymousId) return;
|
|
297
|
+
identityClassification = { anonymousId, persistence, writeOutcome };
|
|
298
|
+
}
|
|
299
|
+
function writeOutcomeOf(write) {
|
|
300
|
+
if (!write.ok) return "failed";
|
|
301
|
+
return write.mirrored === false ? "ok_unmirrored" : "ok";
|
|
302
|
+
}
|
|
303
|
+
function parseNonEmptyString(value) {
|
|
304
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
305
|
+
}
|
|
306
|
+
function parseRecentRenders(value) {
|
|
307
|
+
if (!Array.isArray(value)) return void 0;
|
|
308
|
+
return value.filter(
|
|
309
|
+
(r) => typeof r === "object" && r !== null && typeof r.id === "string" && typeof r.at === "string" && typeof r.ok === "boolean"
|
|
310
|
+
).slice(-MAX_RECENT_RENDERS);
|
|
311
|
+
}
|
|
312
|
+
function passthroughFields(parsed) {
|
|
313
|
+
return {
|
|
314
|
+
lastUpdateCheck: parsed.lastUpdateCheck,
|
|
315
|
+
latestVersion: parsed.latestVersion,
|
|
316
|
+
lastStalePinNoticeAt: parsed.lastStalePinNoticeAt,
|
|
317
|
+
lastUpdateAttemptAt: parsed.lastUpdateAttemptAt,
|
|
318
|
+
lastSkillsAttemptAt: parsed.lastSkillsAttemptAt,
|
|
319
|
+
pendingUpdate: parsed.pendingUpdate,
|
|
320
|
+
completedUpdate: parsed.completedUpdate,
|
|
321
|
+
lastSkillsCheck: parsed.lastSkillsCheck,
|
|
322
|
+
skillsUpdateAvailable: parsed.skillsUpdateAvailable,
|
|
323
|
+
skillsOutdatedCount: parsed.skillsOutdatedCount,
|
|
324
|
+
skillsMissingCount: parsed.skillsMissingCount,
|
|
325
|
+
skillsRemovedCount: parsed.skillsRemovedCount,
|
|
326
|
+
localEmbeddingEnabled: consentFrom(parsed.localEmbeddingEnabled)
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function guardedFields(parsed) {
|
|
330
|
+
return {
|
|
331
|
+
// Explicit `=== true`/typeof-number checks rather than a truthy/nullish
|
|
332
|
+
// read — a hand-edited or corrupted config could plausibly carry a
|
|
333
|
+
// non-boolean/non-number JSON value (e.g. the STRING "false", which is
|
|
334
|
+
// truthy in JS) for these two fields specifically, since they're read
|
|
335
|
+
// with a bare truthy check at the call site (review finding).
|
|
336
|
+
// `|| installStateLatchedFired()` — a latch recorded in install-state
|
|
337
|
+
// wins over an untripped config.json, never the reverse.
|
|
338
|
+
deParallelRouterTrialFired: parsed.deParallelRouterTrialFired === true || installStateLatchedFired() ? true : void 0,
|
|
339
|
+
deParallelRouterTrialRenderCount: typeof parsed.deParallelRouterTrialRenderCount === "number" ? parsed.deParallelRouterTrialRenderCount : void 0,
|
|
340
|
+
predecessorFound: typeof parsed.predecessorFound === "boolean" ? parsed.predecessorFound : void 0,
|
|
341
|
+
stateFileCorrupt: parsed.stateFileCorrupt === true ? true : void 0,
|
|
342
|
+
// Install-state wins, exactly as it does for the latch. Without this the
|
|
343
|
+
// two stores could hold different seeds indefinitely: nextInstallState
|
|
344
|
+
// is write-once (state's seed always survives), but the read took
|
|
345
|
+
// config.json's blindly — so restoring or syncing only config.json left
|
|
346
|
+
// the install bucketing on A while install-state kept B, and the next
|
|
347
|
+
// re-mint silently flipped every cohort at once.
|
|
348
|
+
bucketSeed: installStateSeed() ?? parseNonEmptyString(parsed.bucketSeed)
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function materializeConfig(parsed) {
|
|
352
|
+
return {
|
|
353
|
+
...passthroughFields(parsed),
|
|
354
|
+
telemetryEnabled: parsed.telemetryEnabled ?? DEFAULT_CONFIG.telemetryEnabled,
|
|
355
|
+
anonymousId: parsed.anonymousId || randomUUID(),
|
|
356
|
+
telemetryNoticeShown: parsed.telemetryNoticeShown ?? DEFAULT_CONFIG.telemetryNoticeShown,
|
|
357
|
+
commandCount: parsed.commandCount ?? DEFAULT_CONFIG.commandCount,
|
|
358
|
+
renderSuccessCount: parsed.renderSuccessCount ?? DEFAULT_CONFIG.renderSuccessCount,
|
|
359
|
+
lastFeedbackPromptAt: parsed.lastFeedbackPromptAt ?? DEFAULT_CONFIG.lastFeedbackPromptAt,
|
|
360
|
+
...guardedFields(parsed),
|
|
361
|
+
recentRenders: parseRecentRenders(parsed.recentRenders)
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function readConfig() {
|
|
365
|
+
if (cachedConfig) return { ...cachedConfig };
|
|
366
|
+
if (!existsSync(CONFIG_FILE)) return mintAndCacheConfig();
|
|
367
|
+
try {
|
|
368
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
369
|
+
const parsed = JSON.parse(raw);
|
|
370
|
+
const config = materializeConfig(parsed);
|
|
371
|
+
const idFromDisk = parseNonEmptyString(parsed.anonymousId) !== void 0;
|
|
372
|
+
if (config.bucketSeed === void 0) {
|
|
373
|
+
const write = backfillBucketSeed(config);
|
|
374
|
+
if (idFromDisk) classifyIdentity(config.anonymousId, "durable");
|
|
375
|
+
else
|
|
376
|
+
classifyIdentity(
|
|
377
|
+
config.anonymousId,
|
|
378
|
+
write.ok ? "unknown" : "process_only",
|
|
379
|
+
writeOutcomeOf(write)
|
|
380
|
+
);
|
|
381
|
+
cachedConfig = config;
|
|
382
|
+
return { ...config };
|
|
383
|
+
}
|
|
384
|
+
classifyIdentity(config.anonymousId, idFromDisk ? "durable" : "process_only");
|
|
385
|
+
cachedConfig = config;
|
|
386
|
+
return { ...config };
|
|
387
|
+
} catch {
|
|
388
|
+
const config = { ...mintConfig(), telemetryEnabled: false, localEmbeddingEnabled: false };
|
|
389
|
+
const write = writeConfigWithResult(config);
|
|
390
|
+
classifyIdentity(
|
|
391
|
+
config.anonymousId,
|
|
392
|
+
write.ok ? "unknown" : "process_only",
|
|
393
|
+
writeOutcomeOf(write)
|
|
394
|
+
);
|
|
395
|
+
return config;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function readConfigFresh() {
|
|
399
|
+
cachedConfig = null;
|
|
400
|
+
return readConfig();
|
|
401
|
+
}
|
|
402
|
+
function writeConfig(config) {
|
|
403
|
+
return writeConfigWithResult(config).ok;
|
|
404
|
+
}
|
|
405
|
+
function writeConfigWithResult(config) {
|
|
406
|
+
try {
|
|
407
|
+
return withConfigLock(
|
|
408
|
+
() => persistConfig({ ...config, localEmbeddingEnabled: localModelConsentOnDisk() })
|
|
409
|
+
);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
warnSettingsLockedOnce(error);
|
|
412
|
+
return { ok: false, error: normalizeErrorMessage(error) };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
var warnedSettingsLocked = false;
|
|
416
|
+
function warnSettingsLockedOnce(error) {
|
|
417
|
+
if (warnedSettingsLocked || error?.code !== "HF_SETTINGS_LOCKED")
|
|
418
|
+
return;
|
|
419
|
+
warnedSettingsLocked = true;
|
|
420
|
+
if (!isAttendedTerminal() || process.argv.some((arg) => arg === "--json" || arg.startsWith("--json=")))
|
|
421
|
+
return;
|
|
422
|
+
console.error(`${error.message} (see \`hyperframes doctor\`).`);
|
|
423
|
+
}
|
|
424
|
+
function consentFrom(value) {
|
|
425
|
+
return value === void 0 ? void 0 : value === true;
|
|
426
|
+
}
|
|
427
|
+
function localModelConsentOnDisk() {
|
|
428
|
+
try {
|
|
429
|
+
return consentFrom(JSON.parse(readFileSync(CONFIG_FILE, "utf-8")).localEmbeddingEnabled);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
return error.code === "ENOENT" ? void 0 : false;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
var holdingConfigLock = false;
|
|
435
|
+
function withConfigLock(task) {
|
|
436
|
+
if (holdingConfigLock) return task();
|
|
437
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
438
|
+
return withFileLock(`${CONFIG_FILE}.lock`, fs, () => {
|
|
439
|
+
holdingConfigLock = true;
|
|
440
|
+
try {
|
|
441
|
+
return task();
|
|
442
|
+
} finally {
|
|
443
|
+
holdingConfigLock = false;
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
function persistConfig(config) {
|
|
448
|
+
try {
|
|
449
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
450
|
+
const tmpFile = `${CONFIG_FILE}.${process.pid}.tmp`;
|
|
451
|
+
writeFileSync(tmpFile, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
452
|
+
renameSync(tmpFile, CONFIG_FILE);
|
|
453
|
+
cachedConfig = { ...config };
|
|
454
|
+
const mirrored = syncInstallState(config);
|
|
455
|
+
return mirrored ? { ok: true } : { ok: true, mirrored: false };
|
|
456
|
+
} catch (error) {
|
|
457
|
+
return { ok: false, error: normalizeErrorMessage(error) };
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/version.ts
|
|
462
|
+
var VERSION = true ? "0.8.82" : "0.0.0-dev";
|
|
463
|
+
|
|
464
|
+
// src/utils/hostAnswers.ts
|
|
465
|
+
import { promises as dns } from "dns";
|
|
466
|
+
var PROBE_TIMEOUT_MS = 1e3;
|
|
467
|
+
function fetchGoesThroughEnvProxy() {
|
|
468
|
+
if (!(process.env["HTTPS_PROXY"] ?? process.env["https_proxy"])?.trim()) return false;
|
|
469
|
+
const flags = [...process.execArgv, ...process.env["NODE_OPTIONS"]?.split(/\s+/u) ?? []];
|
|
470
|
+
return process.env["NODE_USE_ENV_PROXY"] === "1" || flags.includes("--use-env-proxy");
|
|
471
|
+
}
|
|
472
|
+
async function hostAnswers(host) {
|
|
473
|
+
if (fetchGoesThroughEnvProxy()) return true;
|
|
474
|
+
try {
|
|
475
|
+
const resolver = new dns.Resolver({ timeout: PROBE_TIMEOUT_MS, tries: 1 });
|
|
476
|
+
const results = await Promise.allSettled([resolver.resolve4(host), resolver.resolve6(host)]);
|
|
477
|
+
const anyFamilyTimedOut = results.some(
|
|
478
|
+
(r) => r.status === "rejected" && r.reason.code === "ETIMEOUT"
|
|
479
|
+
);
|
|
480
|
+
return !anyFamilyTimedOut && results.some((r) => r.status === "fulfilled");
|
|
481
|
+
} catch {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/utils/installerDetection.ts
|
|
487
|
+
import { realpathSync } from "fs";
|
|
488
|
+
import { posix } from "path";
|
|
489
|
+
|
|
490
|
+
// src/utils/safeVersion.ts
|
|
491
|
+
function isSafeVersion(v) {
|
|
492
|
+
return /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(v);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/utils/updateCheck.ts
|
|
496
|
+
var NPM_REGISTRY_URL = "https://registry.npmjs.org/hyperframes/latest";
|
|
497
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
498
|
+
var FETCH_TIMEOUT_MS = 3e3;
|
|
499
|
+
function isNewerSemver(a, b) {
|
|
500
|
+
try {
|
|
501
|
+
return compareVersions(a, b) > 0;
|
|
502
|
+
} catch {
|
|
503
|
+
return a !== b;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
async function checkForUpdate(force) {
|
|
507
|
+
const config = readConfig();
|
|
508
|
+
if (!force && !updateCheckDue(config)) return fallbackResult(config.latestVersion);
|
|
509
|
+
try {
|
|
510
|
+
if (!force && !await hostAnswers(new URL(NPM_REGISTRY_URL).hostname)) {
|
|
511
|
+
return fallbackResult(config.latestVersion);
|
|
512
|
+
}
|
|
513
|
+
const controller = new AbortController();
|
|
514
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
515
|
+
let data;
|
|
516
|
+
try {
|
|
517
|
+
const res = await fetch(NPM_REGISTRY_URL, {
|
|
518
|
+
signal: controller.signal,
|
|
519
|
+
headers: { Connection: "close" }
|
|
520
|
+
});
|
|
521
|
+
if (!res.ok) return fallbackResult(config.latestVersion);
|
|
522
|
+
data = await res.json();
|
|
523
|
+
} finally {
|
|
524
|
+
clearTimeout(timeout);
|
|
525
|
+
}
|
|
526
|
+
if (typeof data.version !== "string" || !isSafeVersion(data.version)) {
|
|
527
|
+
return fallbackResult(config.latestVersion);
|
|
528
|
+
}
|
|
529
|
+
const latest = data.version;
|
|
530
|
+
const freshConfig = readConfigFresh();
|
|
531
|
+
freshConfig.lastUpdateCheck = (/* @__PURE__ */ new Date()).toISOString();
|
|
532
|
+
freshConfig.latestVersion = latest;
|
|
533
|
+
writeConfig(freshConfig);
|
|
534
|
+
return { current: VERSION, latest, updateAvailable: isNewerSemver(latest, VERSION) };
|
|
535
|
+
} catch {
|
|
536
|
+
return fallbackResult(config.latestVersion);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function updateCheckDue(config = readConfig()) {
|
|
540
|
+
if (updateCheckDisabled()) return false;
|
|
541
|
+
if (!config.lastUpdateCheck || !config.latestVersion || !isSafeVersion(config.latestVersion)) {
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
const fresh = Date.now() - new Date(config.lastUpdateCheck).getTime() < CHECK_INTERVAL_MS;
|
|
545
|
+
return !fresh;
|
|
546
|
+
}
|
|
547
|
+
function fallbackResult(cachedLatest) {
|
|
548
|
+
const safeCached = cachedLatest && isSafeVersion(cachedLatest) ? cachedLatest : void 0;
|
|
549
|
+
return {
|
|
550
|
+
current: VERSION,
|
|
551
|
+
latest: safeCached ?? VERSION,
|
|
552
|
+
updateAvailable: safeCached ? isNewerSemver(safeCached, VERSION) : false
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function updateCheckDisabled() {
|
|
556
|
+
if (isDevMode()) return true;
|
|
557
|
+
if (process.env["CI"] === "true" || process.env["CI"] === "1") return true;
|
|
558
|
+
return process.env["HYPERFRAMES_NO_UPDATE_CHECK"] === "1";
|
|
559
|
+
}
|
|
560
|
+
var STALE_PIN_THROTTLE_MS = 24 * 60 * 60 * 1e3;
|
|
561
|
+
|
|
562
|
+
// src/utils/skillsManifest.ts
|
|
563
|
+
import { execFile } from "child_process";
|
|
564
|
+
import { createHash } from "crypto";
|
|
565
|
+
import {
|
|
566
|
+
existsSync as existsSync3,
|
|
567
|
+
readdirSync,
|
|
568
|
+
readFileSync as readFileSync3,
|
|
569
|
+
renameSync as renameSync2,
|
|
570
|
+
statSync,
|
|
571
|
+
writeFileSync as writeFileSync2
|
|
572
|
+
} from "fs";
|
|
573
|
+
import { homedir as homedir2 } from "os";
|
|
574
|
+
import { isAbsolute, join as join3, relative, resolve, sep } from "path";
|
|
575
|
+
import { promisify } from "util";
|
|
576
|
+
var execFileAsync = promisify(execFile);
|
|
577
|
+
var TEXT_EXT = /* @__PURE__ */ new Set([
|
|
578
|
+
".md",
|
|
579
|
+
".txt",
|
|
580
|
+
".mjs",
|
|
581
|
+
".js",
|
|
582
|
+
".ts",
|
|
583
|
+
".jsx",
|
|
584
|
+
".tsx",
|
|
585
|
+
".html",
|
|
586
|
+
".css",
|
|
587
|
+
".json",
|
|
588
|
+
".svg",
|
|
589
|
+
".csv",
|
|
590
|
+
".yml",
|
|
591
|
+
".yaml"
|
|
592
|
+
]);
|
|
593
|
+
var DEFAULT_REPO_SLUG = "heygen-com/hyperframes";
|
|
594
|
+
var MANIFEST_FILE = "skills-manifest.json";
|
|
595
|
+
var FETCH_TIMEOUT_MS2 = 4e3;
|
|
596
|
+
var ENTRY_SKILL = "hyperframes";
|
|
597
|
+
function isCoreSkill(name) {
|
|
598
|
+
return name === ENTRY_SKILL || name.startsWith("hyperframes-") || name === "media-use";
|
|
599
|
+
}
|
|
600
|
+
function listFilesSorted(dir) {
|
|
601
|
+
const out = [];
|
|
602
|
+
const walk = (d) => {
|
|
603
|
+
for (const name of readdirSync(d)) {
|
|
604
|
+
if (name === ".DS_Store") continue;
|
|
605
|
+
const p = join3(d, name);
|
|
606
|
+
if (statSync(p).isDirectory()) walk(p);
|
|
607
|
+
else out.push(p);
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
walk(dir);
|
|
611
|
+
return out.sort();
|
|
612
|
+
}
|
|
613
|
+
function hashSkillBundle(skillDir) {
|
|
614
|
+
const files = listFilesSorted(skillDir);
|
|
615
|
+
const h = createHash("sha256");
|
|
616
|
+
for (const f of files) {
|
|
617
|
+
const rel = relative(skillDir, f).split(sep).join("/");
|
|
618
|
+
h.update(rel);
|
|
619
|
+
h.update("\0");
|
|
620
|
+
const ext = rel.slice(rel.lastIndexOf("."));
|
|
621
|
+
const buf = readFileSync3(f);
|
|
622
|
+
if (TEXT_EXT.has(ext)) h.update(buf.toString("utf8").replace(/\r\n/g, "\n"), "utf8");
|
|
623
|
+
else h.update(buf);
|
|
624
|
+
h.update("\0");
|
|
625
|
+
}
|
|
626
|
+
return { hash: h.digest("hex").slice(0, 16), files: files.length };
|
|
627
|
+
}
|
|
628
|
+
function buildManifest(skillsRoot, meta) {
|
|
629
|
+
const names = readdirSync(skillsRoot).filter((n) => existsSync3(join3(skillsRoot, n, "SKILL.md"))).sort();
|
|
630
|
+
const skills = {};
|
|
631
|
+
for (const name of names) skills[name] = hashSkillBundle(join3(skillsRoot, name));
|
|
632
|
+
return { source: meta.source, skills };
|
|
633
|
+
}
|
|
634
|
+
function agentLabel(hostDir) {
|
|
635
|
+
const name = hostDir.replace(/^\.+/, "");
|
|
636
|
+
return name === "claude" ? "claude-code" : name || "unknown";
|
|
637
|
+
}
|
|
638
|
+
function agentFromDir(dir) {
|
|
639
|
+
const parts = dir.split(sep).filter(Boolean);
|
|
640
|
+
const i = parts.lastIndexOf("skills");
|
|
641
|
+
return agentLabel(i > 0 ? parts[i - 1] : parts[parts.length - 1] ?? "");
|
|
642
|
+
}
|
|
643
|
+
function listSubdirs(dir) {
|
|
644
|
+
try {
|
|
645
|
+
return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
|
|
646
|
+
} catch {
|
|
647
|
+
return [];
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function discoverSkillRoots(base, scope) {
|
|
651
|
+
const candidates = [];
|
|
652
|
+
const add = (hostBase, host) => {
|
|
653
|
+
const dir = join3(hostBase, host, "skills");
|
|
654
|
+
if (existsSync3(dir) && statSync(dir).isDirectory())
|
|
655
|
+
candidates.push({ dir, agent: agentLabel(host), scope });
|
|
656
|
+
};
|
|
657
|
+
for (const host of listSubdirs(base)) add(base, host);
|
|
658
|
+
const xdg = join3(base, ".config");
|
|
659
|
+
for (const host of listSubdirs(xdg)) add(xdg, host);
|
|
660
|
+
return candidates.sort((a, b) => {
|
|
661
|
+
if (a.agent !== b.agent) {
|
|
662
|
+
if (a.agent === "claude-code") return -1;
|
|
663
|
+
if (b.agent === "claude-code") return 1;
|
|
664
|
+
return a.agent.localeCompare(b.agent);
|
|
665
|
+
}
|
|
666
|
+
return a.dir.localeCompare(b.dir);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
function scopeForDir(dir, home, cwd) {
|
|
670
|
+
const norm = (p) => {
|
|
671
|
+
const r = resolve(p);
|
|
672
|
+
return r.endsWith(sep) ? r : r + sep;
|
|
673
|
+
};
|
|
674
|
+
const d = norm(dir);
|
|
675
|
+
if (d.startsWith(norm(cwd))) return "project";
|
|
676
|
+
if (d.startsWith(norm(home))) return "global";
|
|
677
|
+
return "project";
|
|
678
|
+
}
|
|
679
|
+
function locateInstall(skillNames, opts = {}) {
|
|
680
|
+
if (opts.dir) {
|
|
681
|
+
return existsSync3(opts.dir) ? {
|
|
682
|
+
dir: opts.dir,
|
|
683
|
+
agent: agentFromDir(opts.dir),
|
|
684
|
+
scope: scopeForDir(opts.dir, opts.home ?? homedir2(), opts.cwd ?? process.cwd())
|
|
685
|
+
} : null;
|
|
686
|
+
}
|
|
687
|
+
const roots = [
|
|
688
|
+
...discoverSkillRoots(opts.home ?? homedir2(), "global"),
|
|
689
|
+
...discoverSkillRoots(opts.cwd ?? process.cwd(), "project")
|
|
690
|
+
];
|
|
691
|
+
for (const root of roots) {
|
|
692
|
+
if (skillNames.some((n) => existsSync3(join3(root.dir, n, "SKILL.md")))) return root;
|
|
693
|
+
}
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
function hashInstalled(root, skillNames) {
|
|
697
|
+
const out = {};
|
|
698
|
+
for (const name of skillNames) {
|
|
699
|
+
const skillDir = join3(root.dir, name);
|
|
700
|
+
if (existsSync3(join3(skillDir, "SKILL.md"))) out[name] = hashSkillBundle(skillDir);
|
|
701
|
+
}
|
|
702
|
+
return out;
|
|
703
|
+
}
|
|
704
|
+
function diffSkills(installed, latest) {
|
|
705
|
+
const skills = [];
|
|
706
|
+
const summary = { current: 0, outdated: 0, missing: 0, coreMissing: 0 };
|
|
707
|
+
for (const name of Object.keys(latest.skills).sort()) {
|
|
708
|
+
const latestEntry = latest.skills[name];
|
|
709
|
+
const installedEntry = installed[name];
|
|
710
|
+
let status;
|
|
711
|
+
if (!installedEntry) status = "missing";
|
|
712
|
+
else if (installedEntry.hash === latestEntry.hash) status = "current";
|
|
713
|
+
else status = "outdated";
|
|
714
|
+
if (status === "current") summary.current++;
|
|
715
|
+
else if (status === "outdated") summary.outdated++;
|
|
716
|
+
else {
|
|
717
|
+
summary.missing++;
|
|
718
|
+
if (isCoreSkill(name)) summary.coreMissing++;
|
|
719
|
+
}
|
|
720
|
+
skills.push({
|
|
721
|
+
name,
|
|
722
|
+
status,
|
|
723
|
+
installedHash: installedEntry?.hash,
|
|
724
|
+
latestHash: latestEntry.hash
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
// "Update available" means the install is stale, not merely partial:
|
|
729
|
+
// anything installed-but-outdated, or a missing CORE skill (the entry
|
|
730
|
+
// router + shared domain skills every workflow needs). A missing
|
|
731
|
+
// on-demand skill is NOT an update — it installs when its workflow is
|
|
732
|
+
// triggered (`hyperframes skills update <name>`). Counting it here is what
|
|
733
|
+
// used to make `init` re-pull the full skill set onto machines that
|
|
734
|
+
// deliberately installed a subset.
|
|
735
|
+
updateAvailable: summary.outdated > 0 || summary.coreMissing > 0,
|
|
736
|
+
summary,
|
|
737
|
+
skills
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function repoSlug(s) {
|
|
741
|
+
return (s ?? "").replace(/^git\+/, "").replace(/^https?:\/\/github\.com\//, "").replace(/\.git$/, "").toLowerCase();
|
|
742
|
+
}
|
|
743
|
+
function skillsAttributedToSource(lock, source) {
|
|
744
|
+
const want = repoSlug(source);
|
|
745
|
+
if (!want || !lock?.skills) return [];
|
|
746
|
+
return Object.entries(lock.skills).filter(([, e]) => repoSlug(e.source) === want || repoSlug(e.sourceUrl) === want).map(([name]) => name);
|
|
747
|
+
}
|
|
748
|
+
function lockPathForScope(scope, opts) {
|
|
749
|
+
if (scope === "project") return join3(opts.cwd ?? process.cwd(), "skills-lock.json");
|
|
750
|
+
const xdgStateHome = process.env.XDG_STATE_HOME;
|
|
751
|
+
if (xdgStateHome) return join3(xdgStateHome, "skills", ".skill-lock.json");
|
|
752
|
+
return join3(opts.home ?? homedir2(), ".agents", ".skill-lock.json");
|
|
753
|
+
}
|
|
754
|
+
function readSkillLock(path) {
|
|
755
|
+
try {
|
|
756
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
757
|
+
} catch {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
var MANIFEST_COVERAGE_ROOT = "skills/";
|
|
762
|
+
function manifestCoversSkill(entry) {
|
|
763
|
+
const path = entry?.skillPath;
|
|
764
|
+
return typeof path === "string" && path.startsWith(MANIFEST_COVERAGE_ROOT);
|
|
765
|
+
}
|
|
766
|
+
function detectRemoved(root, latest, opts) {
|
|
767
|
+
const lock = readSkillLock(lockPathForScope(root.scope, opts));
|
|
768
|
+
const removed = skillsAttributedToSource(lock, latest.source).filter((name) => manifestCoversSkill(lock?.skills?.[name])).filter((name) => !(name in latest.skills)).sort().map((name) => ({ name, status: "removed" }));
|
|
769
|
+
return { removed, lockMissing: lock === null };
|
|
770
|
+
}
|
|
771
|
+
function findRepoManifest(cwd = process.cwd()) {
|
|
772
|
+
let dir = cwd;
|
|
773
|
+
for (let i = 0; i < 16; i++) {
|
|
774
|
+
const p = join3(dir, MANIFEST_FILE);
|
|
775
|
+
if (existsSync3(p)) return p;
|
|
776
|
+
const parent = join3(dir, "..");
|
|
777
|
+
if (parent === dir) break;
|
|
778
|
+
dir = parent;
|
|
779
|
+
}
|
|
780
|
+
return null;
|
|
781
|
+
}
|
|
782
|
+
function asSkillsManifest(data, sourceLabel) {
|
|
783
|
+
const m = data;
|
|
784
|
+
if (!m || typeof m !== "object" || typeof m.skills !== "object" || m.skills === null) {
|
|
785
|
+
throw new Error(`Malformed skills manifest from ${sourceLabel}`);
|
|
786
|
+
}
|
|
787
|
+
return m;
|
|
788
|
+
}
|
|
789
|
+
async function fetchManifest(url) {
|
|
790
|
+
const controller = new AbortController();
|
|
791
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
|
|
792
|
+
try {
|
|
793
|
+
const res = await fetch(url, { signal: controller.signal, headers: { Connection: "close" } });
|
|
794
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
|
|
795
|
+
return asSkillsManifest(await res.json(), url);
|
|
796
|
+
} finally {
|
|
797
|
+
clearTimeout(timeout);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
async function remoteHeadSha(repoSlug2) {
|
|
801
|
+
try {
|
|
802
|
+
const { stdout } = await execFileAsync(
|
|
803
|
+
"git",
|
|
804
|
+
["ls-remote", `https://github.com/${repoSlug2}.git`, "refs/heads/main"],
|
|
805
|
+
{
|
|
806
|
+
timeout: FETCH_TIMEOUT_MS2,
|
|
807
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
|
|
808
|
+
windowsHide: true
|
|
809
|
+
}
|
|
810
|
+
);
|
|
811
|
+
const sha = stdout.split(/\s+/)[0]?.trim() ?? "";
|
|
812
|
+
return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
|
|
813
|
+
} catch {
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
function resolveLocalManifest(source) {
|
|
818
|
+
const direct = source.endsWith(".json") ? source : join3(source, MANIFEST_FILE);
|
|
819
|
+
if (existsSync3(direct)) return JSON.parse(readFileSync3(direct, "utf8"));
|
|
820
|
+
const skillsRoot = source.endsWith("skills") ? source : join3(source, "skills");
|
|
821
|
+
if (existsSync3(skillsRoot)) return buildManifest(skillsRoot, { source: skillsRoot });
|
|
822
|
+
throw new Error(`No skills manifest found at: ${source}`);
|
|
823
|
+
}
|
|
824
|
+
async function fetchRemoteManifest(source) {
|
|
825
|
+
if (source?.startsWith("http")) return fetchManifest(source);
|
|
826
|
+
const repoSlug2 = source ?? DEFAULT_REPO_SLUG;
|
|
827
|
+
const sha = await remoteHeadSha(repoSlug2);
|
|
828
|
+
if (sha) {
|
|
829
|
+
try {
|
|
830
|
+
return await fetchManifest(
|
|
831
|
+
`https://raw.githubusercontent.com/${repoSlug2}/${sha}/${MANIFEST_FILE}`
|
|
832
|
+
);
|
|
833
|
+
} catch {
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return fetchManifest(`https://raw.githubusercontent.com/${repoSlug2}/main/${MANIFEST_FILE}`);
|
|
837
|
+
}
|
|
838
|
+
async function resolveLatestManifest(source, cwd = process.cwd(), opts = {}) {
|
|
839
|
+
if (source && (source.startsWith(".") || isAbsolute(source))) {
|
|
840
|
+
return resolveLocalManifest(source);
|
|
841
|
+
}
|
|
842
|
+
if (!source && !opts.canonical) {
|
|
843
|
+
const repoManifest = findRepoManifest(cwd);
|
|
844
|
+
if (repoManifest) return JSON.parse(readFileSync3(repoManifest, "utf8"));
|
|
845
|
+
}
|
|
846
|
+
return fetchRemoteManifest(source);
|
|
847
|
+
}
|
|
848
|
+
async function checkSkills(opts = {}) {
|
|
849
|
+
const latest = await resolveLatestManifest(opts.source, opts.cwd, { canonical: opts.canonical });
|
|
850
|
+
const skillNames = Object.keys(latest.skills);
|
|
851
|
+
const root = locateInstall(skillNames, { dir: opts.dir, cwd: opts.cwd, home: opts.home });
|
|
852
|
+
const installed = root ? hashInstalled(root, skillNames) : {};
|
|
853
|
+
const diff = diffSkills(installed, latest);
|
|
854
|
+
const removedResult = root ? detectRemoved(root, latest, { cwd: opts.cwd, home: opts.home }) : { removed: [], lockMissing: false };
|
|
855
|
+
const { removed, lockMissing } = removedResult;
|
|
856
|
+
return {
|
|
857
|
+
location: root?.dir ?? null,
|
|
858
|
+
agent: root?.agent ?? null,
|
|
859
|
+
scope: root?.scope ?? null,
|
|
860
|
+
// Removed skills also mean the install isn't reconciled with the manifest —
|
|
861
|
+
// `skills update` now prunes them, so they count toward "update available".
|
|
862
|
+
updateAvailable: diff.updateAvailable || removed.length > 0,
|
|
863
|
+
summary: { ...diff.summary, removed: removed.length },
|
|
864
|
+
skills: [...diff.skills, ...removed],
|
|
865
|
+
lockMissing
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// src/utils/skillsUpdateCheck.ts
|
|
870
|
+
var CHECK_INTERVAL_MS2 = 24 * 60 * 60 * 1e3;
|
|
871
|
+
async function refreshSkillsCache() {
|
|
872
|
+
if (!await hostAnswers("github.com")) return;
|
|
873
|
+
const result = await checkSkills({ canonical: true });
|
|
874
|
+
if (result.location) {
|
|
875
|
+
const config = readConfigFresh();
|
|
876
|
+
config.lastSkillsCheck = (/* @__PURE__ */ new Date()).toISOString();
|
|
877
|
+
config.skillsUpdateAvailable = result.updateAvailable;
|
|
878
|
+
config.skillsOutdatedCount = result.summary.outdated;
|
|
879
|
+
config.skillsMissingCount = result.summary.coreMissing;
|
|
880
|
+
config.skillsRemovedCount = result.summary.removed;
|
|
881
|
+
writeConfig(config);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/backgroundChecksWorker.ts
|
|
886
|
+
var due = process.argv.slice(2);
|
|
887
|
+
await Promise.allSettled([
|
|
888
|
+
due.includes("update") && checkForUpdate(),
|
|
889
|
+
due.includes("skills") && refreshSkillsCache()
|
|
890
|
+
]);
|