@outerlayer/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +202 -0
  3. package/README.md +320 -0
  4. package/dist/build-info.json +1 -0
  5. package/dist/chunk-4H56Y7O6.js +589 -0
  6. package/dist/chunk-A3WLZX2F.js +120 -0
  7. package/dist/chunk-A6NNKRJU.js +650 -0
  8. package/dist/chunk-D77LS3UI.js +42 -0
  9. package/dist/chunk-DEDV5V4U.js +87324 -0
  10. package/dist/chunk-ECSYRPCC.js +334 -0
  11. package/dist/chunk-IWIUYLDR.js +472 -0
  12. package/dist/chunk-JJP7YLMN.js +25 -0
  13. package/dist/chunk-KFYJV2ZG.js +7978 -0
  14. package/dist/chunk-OZ7C3XUE.js +34 -0
  15. package/dist/chunk-RCQXYLMO.js +96 -0
  16. package/dist/chunk-TFUIDMOB.js +34 -0
  17. package/dist/chunk-VNVZDWO3.js +10 -0
  18. package/dist/chunk-VU34RWDU.js +339 -0
  19. package/dist/chunk-WQ6VGRGZ.js +150 -0
  20. package/dist/chunk-XN2XZFTK.js +165 -0
  21. package/dist/chunk-YMTXR7PJ.js +200 -0
  22. package/dist/chunk-YRYMNBGQ.js +54 -0
  23. package/dist/chunk-Z5GEFMRW.js +45 -0
  24. package/dist/chunk-ZJSNLVCE.js +58 -0
  25. package/dist/cli-3JYGL2OB.js +5311 -0
  26. package/dist/config-POF7DEQW.js +7 -0
  27. package/dist/context-materialize-J6CRQ7O7.js +589 -0
  28. package/dist/emit-artifact-cmd-23F4YKQ3.js +339 -0
  29. package/dist/emit-cmd-TWSYYEZI.js +208 -0
  30. package/dist/emit-commit-credit-cmd-2GX5M2BR.js +121 -0
  31. package/dist/emit-finding-cmd-6K7ERPKG.js +235 -0
  32. package/dist/emit-result-cmd-NDFHMJX7.js +225 -0
  33. package/dist/hook-fast-CBW4ZETX.js +8 -0
  34. package/dist/hook-wrap-fast-36WGE6EX.js +8 -0
  35. package/dist/import-capture-cmd-EUMBGIV3.js +176 -0
  36. package/dist/import-ruler-cmd-7LH2QOMG.js +173 -0
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +25 -0
  39. package/dist/init-PTBITAUO.js +103 -0
  40. package/dist/login-cmd-IRX6LZT7.js +62 -0
  41. package/dist/logs-TRNPQM42.js +63 -0
  42. package/dist/loop-NFSOBUUJ.js +646 -0
  43. package/dist/mcp-install-cmd-FDQH6SEN.js +75 -0
  44. package/dist/mcp-serve-cmd-57EZZOTL.js +123 -0
  45. package/dist/paths-D2VGWWFI.js +6 -0
  46. package/dist/pidfile-PTW76F56.js +8 -0
  47. package/dist/status-WGOJTXZF.js +94 -0
  48. package/dist/statusline-fast-Z5U5CEC6.js +8 -0
  49. package/dist/sync-cmd-C5UGI4SF.js +15 -0
  50. package/dist/watch-V3K4PESQ.js +70 -0
  51. package/dist/work-claim-cmd-FQDWQVT2.js +98 -0
  52. package/dist/work-cmd-CW26EZZO.js +16 -0
  53. package/dist/work-launch-YNNCKIH3.js +6 -0
  54. package/dist/work-pr-cmd-W2QNMWXP.js +115 -0
  55. package/package.json +58 -0
@@ -0,0 +1,650 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { deadHooksState, huskyPrepareScript, preparePackageManagerCommand } from './chunk-IWIUYLDR.js';
4
+ import { homeFromEnv } from './chunk-VNVZDWO3.js';
5
+ import { readFileSync, mkdirSync, writeFileSync, renameSync, existsSync, statSync, appendFileSync, openSync, opendirSync, unlinkSync } from 'fs';
6
+ import { spawn, execFileSync } from 'child_process';
7
+ import { createHash } from 'crypto';
8
+ import { join, dirname, isAbsolute, resolve } from 'path';
9
+
10
+ createRequire(import.meta.url);
11
+ var SYNC_STALE_AFTER_MS = 6 * 60 * 60 * 1e3;
12
+ function syncStatusPath(home = homeFromEnv()) {
13
+ return join(home, ".outerlayer", "last-sync.json");
14
+ }
15
+ function readSyncStatus(home = homeFromEnv()) {
16
+ try {
17
+ const raw = readFileSync(syncStatusPath(home), "utf8");
18
+ const parsed = JSON.parse(raw);
19
+ if (!parsed || typeof parsed.at !== "string" || typeof parsed.ok !== "boolean") return null;
20
+ return parsed;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ function writeSyncStatus(status, home = homeFromEnv()) {
26
+ try {
27
+ const previous = readSyncStatus(home);
28
+ const merged = {
29
+ ...status,
30
+ lastOkAt: status.ok && status.contacted !== false ? status.at : status.lastOkAt ?? previous?.lastOkAt
31
+ };
32
+ const path = syncStatusPath(home);
33
+ mkdirSync(join(home, ".outerlayer"), { recursive: true });
34
+ const tmp = `${path}.tmp`;
35
+ writeFileSync(tmp, JSON.stringify(merged, null, 2), { mode: 384 });
36
+ renameSync(tmp, path);
37
+ } catch {
38
+ }
39
+ }
40
+ function formatAgo(ms) {
41
+ const age = formatAge(ms);
42
+ return age === "just now" || age === "unknown" ? age : `${age} ago`;
43
+ }
44
+ function formatAge(ms) {
45
+ if (!Number.isFinite(ms) || ms < 0) return "unknown";
46
+ const minutes = Math.floor(ms / 6e4);
47
+ if (minutes < 1) return "just now";
48
+ if (minutes < 60) return `${minutes}m`;
49
+ const hours = Math.floor(minutes / 60);
50
+ if (hours < 48) return `${hours}h`;
51
+ return `${Math.floor(hours / 24)}d`;
52
+ }
53
+ function assessSyncHealth(status, now = Date.now(), staleAfterMs = SYNC_STALE_AFTER_MS) {
54
+ if (!status) {
55
+ return {
56
+ level: "unknown",
57
+ headline: "no sync has run on this machine yet",
58
+ details: ["run `outerlayer sync` once to verify the connection"]
59
+ };
60
+ }
61
+ const host = status.url ? safeHost(status.url) : "the configured endpoint";
62
+ if (!status.ok) {
63
+ const brokenFor = status.lastOkAt ? formatAgo(now - Date.parse(status.lastOkAt)) : null;
64
+ const details = [];
65
+ details.push(
66
+ status.error?.status ? `HTTP ${status.error.status} from ${host}: ${status.error.message}` : `${host}: ${status.error?.message ?? "unknown error"}`
67
+ );
68
+ details.push(
69
+ brokenFor ? `last successful sync ${brokenFor}` : "no successful sync has ever been recorded for this destination"
70
+ );
71
+ if (status.pending) details.push(`${status.pending} session(s) waiting to ship`);
72
+ if (status.held) details.push(`${status.held} transcript(s) held \u2014 their launch could not be determined`);
73
+ details.push("run `outerlayer doctor` for the full picture");
74
+ return {
75
+ level: "error",
76
+ headline: `sync is FAILING \u2014 last attempt ${formatAgo(now - Date.parse(status.at))}`,
77
+ details
78
+ };
79
+ }
80
+ const age = now - Date.parse(status.at);
81
+ const verifiedAt = status.contacted === false ? status.lastOkAt : status.at;
82
+ const verifiedAge = verifiedAt === void 0 ? null : now - Date.parse(verifiedAt);
83
+ if (verifiedAge !== null && verifiedAge > staleAfterMs) {
84
+ return {
85
+ level: "warn",
86
+ headline: `last successful sync ${formatAgo(verifiedAge)}`,
87
+ details: [`destination ${host}`, "expected continuously while you are coding"]
88
+ };
89
+ }
90
+ if (status.synced === 0 && (status.skippedNotLaunched ?? 0) > 0) {
91
+ return {
92
+ level: "warn",
93
+ headline: `synced ${formatAgo(age)} \u2192 ${host}, but nothing uploaded`,
94
+ details: [
95
+ `${status.skippedNotLaunched} transcript(s) were not launched with OUTERLAYER_WORK, so none may upload`,
96
+ "launch a session with OUTERLAYER_WORK naming its issue to upload it"
97
+ ]
98
+ };
99
+ }
100
+ if (verifiedAge === null) {
101
+ return {
102
+ level: "warn",
103
+ headline: `sync ran ${formatAgo(age)} but ${host} has never been reached`,
104
+ details: [
105
+ "every run so far had an empty queue, so the connection is unproven",
106
+ "run `outerlayer sync --all` to verify it"
107
+ ]
108
+ };
109
+ }
110
+ if (status.contacted === false) {
111
+ return {
112
+ level: "ok",
113
+ headline: `nothing to sync \u2014 ${host} last reached ${formatAgo(verifiedAge)}`,
114
+ details: []
115
+ };
116
+ }
117
+ return {
118
+ level: "ok",
119
+ headline: `synced ${formatAgo(age)} \u2192 ${host}`,
120
+ details: []
121
+ };
122
+ }
123
+ function safeHost(url) {
124
+ try {
125
+ return new URL(url).host;
126
+ } catch {
127
+ return url;
128
+ }
129
+ }
130
+ var PKG_NAME = "outerlayer";
131
+ function installedCliDistDir(home) {
132
+ return join(home, ".outerlayer", "cli", "dist");
133
+ }
134
+ function readBuildInfo(distDir) {
135
+ try {
136
+ const raw = readFileSync(join(distDir, "build-info.json"), "utf8");
137
+ const parsed = JSON.parse(raw);
138
+ if (parsed.pkg !== PKG_NAME) return null;
139
+ if (typeof parsed.id !== "string" || parsed.id.length === 0) return null;
140
+ return { id: parsed.id, root: typeof parsed.root === "string" && parsed.root.length > 0 ? parsed.root : null };
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+ function buildTimestampMs(id) {
146
+ const iso = id.split("+")[0];
147
+ const ms = Date.parse(iso);
148
+ return Number.isNaN(ms) ? null : ms;
149
+ }
150
+ var MAX_ANCESTOR_HOPS = 25;
151
+ function resolveWorkspaceCliDist(cwd) {
152
+ let dir = resolve(cwd);
153
+ for (let i = 0; i < MAX_ANCESTOR_HOPS; i++) {
154
+ const candidate = join(dir, "packages", "cli", "dist");
155
+ if (readBuildInfo(candidate) !== null) return candidate;
156
+ const parent = dirname(dir);
157
+ if (parent === dir) return null;
158
+ dir = parent;
159
+ }
160
+ return null;
161
+ }
162
+ var SKIP_CHILD_NAMES = /* @__PURE__ */ new Set(["node_modules"]);
163
+ var MAX_CHILD_PROBES = 200;
164
+ function probeChildCheckouts(cwd) {
165
+ let dir;
166
+ try {
167
+ dir = opendirSync(cwd);
168
+ } catch {
169
+ return { distDir: null, ambiguous: false };
170
+ }
171
+ const candidates = [];
172
+ try {
173
+ let probed = 0;
174
+ let entry;
175
+ while (probed < MAX_CHILD_PROBES && (entry = dir.readSync()) !== null) {
176
+ probed += 1;
177
+ if (!entry.isDirectory()) continue;
178
+ if (entry.name.startsWith(".") || SKIP_CHILD_NAMES.has(entry.name)) continue;
179
+ const candidate = join(cwd, entry.name, "packages", "cli", "dist");
180
+ if (readBuildInfo(candidate) !== null) {
181
+ candidates.push(candidate);
182
+ if (candidates.length > 1) break;
183
+ }
184
+ }
185
+ } catch {
186
+ return { distDir: null, ambiguous: false };
187
+ } finally {
188
+ try {
189
+ dir.closeSync();
190
+ } catch {
191
+ }
192
+ }
193
+ if (candidates.length === 0) return { distDir: null, ambiguous: false };
194
+ if (candidates.length > 1) return { distDir: null, ambiguous: true };
195
+ return { distDir: candidates[0], ambiguous: false };
196
+ }
197
+ function resolveWorkspaceDist(cwd, recordedRoot) {
198
+ const recordedDist = recordedRoot ? join(recordedRoot, "packages", "cli", "dist") : null;
199
+ const viaWalkUp = resolveWorkspaceCliDist(cwd);
200
+ if (viaWalkUp) return { distDir: viaWalkUp, trusted: viaWalkUp === recordedDist, ambiguous: false };
201
+ const probed = probeChildCheckouts(cwd);
202
+ if (probed.distDir) return { distDir: probed.distDir, trusted: probed.distDir === recordedDist, ambiguous: false };
203
+ if (probed.ambiguous) return { distDir: null, trusted: false, ambiguous: true };
204
+ if (recordedDist && readBuildInfo(recordedDist) !== null) return { distDir: recordedDist, trusted: true, ambiguous: false };
205
+ return { distDir: null, trusted: false, ambiguous: false };
206
+ }
207
+ function checkInstalledCliStaleness(opts) {
208
+ const installedDir = installedCliDistDir(opts.home);
209
+ if (!existsSync(join(installedDir, "index.js"))) return null;
210
+ const installed = readBuildInfo(installedDir);
211
+ const resolved = resolveWorkspaceDist(opts.cwd, installed?.root ?? null);
212
+ const workspaceDistDir = resolved.distDir;
213
+ const workspaceRootTrusted = resolved.trusted;
214
+ const workspace = workspaceDistDir ? readBuildInfo(workspaceDistDir) : null;
215
+ const installedId = installed?.id ?? null;
216
+ const workspaceId = workspace?.id ?? null;
217
+ if (workspaceId === null) {
218
+ return {
219
+ stale: false,
220
+ installedId,
221
+ workspaceId: null,
222
+ workspaceDistDir: null,
223
+ workspaceRootTrusted: false,
224
+ ambiguousWorkspace: resolved.ambiguous,
225
+ unorderable: false,
226
+ installedAhead: false
227
+ };
228
+ }
229
+ if (installedId === null) {
230
+ return {
231
+ stale: true,
232
+ installedId: null,
233
+ workspaceId,
234
+ workspaceDistDir,
235
+ workspaceRootTrusted,
236
+ ambiguousWorkspace: false,
237
+ unorderable: false,
238
+ installedAhead: false
239
+ };
240
+ }
241
+ const installedMs = buildTimestampMs(installedId);
242
+ const workspaceMs = buildTimestampMs(workspaceId);
243
+ if (installedMs === null || workspaceMs === null) {
244
+ return {
245
+ stale: false,
246
+ installedId,
247
+ workspaceId,
248
+ workspaceDistDir,
249
+ workspaceRootTrusted,
250
+ ambiguousWorkspace: false,
251
+ unorderable: true,
252
+ installedAhead: false
253
+ };
254
+ }
255
+ return {
256
+ stale: installedMs < workspaceMs,
257
+ installedId,
258
+ workspaceId,
259
+ workspaceDistDir,
260
+ workspaceRootTrusted,
261
+ ambiguousWorkspace: false,
262
+ unorderable: false,
263
+ installedAhead: installedMs > workspaceMs
264
+ };
265
+ }
266
+ function workspaceRootOf(workspaceDistDir) {
267
+ return dirname(dirname(dirname(workspaceDistDir)));
268
+ }
269
+ var MAX_SANITIZED_LENGTH = 200;
270
+ var MAX_RAW_SCAN_LENGTH = 2e3;
271
+ var UNICODE_FORMAT_CHAR = /\p{Cf}/u;
272
+ function isUnsafeContextChar(ch) {
273
+ const code = ch.codePointAt(0) ?? 0;
274
+ if (code < 32 || code === 127) return true;
275
+ if (code >= 128 && code <= 159) return true;
276
+ if (code === 8232 || code === 8233) return true;
277
+ return UNICODE_FORMAT_CHAR.test(ch);
278
+ }
279
+ function sanitizeForContext(value) {
280
+ const bounded = value.length > MAX_RAW_SCAN_LENGTH ? value.slice(0, MAX_RAW_SCAN_LENGTH) : value;
281
+ let out = "";
282
+ for (const ch of bounded) {
283
+ if (out.length >= MAX_SANITIZED_LENGTH) break;
284
+ if (isUnsafeContextChar(ch)) continue;
285
+ out += ch;
286
+ }
287
+ return out;
288
+ }
289
+ function containsUnsafeContextChar(value) {
290
+ for (const ch of value) {
291
+ if (isUnsafeContextChar(ch)) return true;
292
+ }
293
+ return false;
294
+ }
295
+ var SHELL_UNSAFE = /["$`\\\n]/;
296
+ function refreshCommand(workspaceDistDir, trusted) {
297
+ if (!trusted) return null;
298
+ const root = workspaceRootOf(workspaceDistDir);
299
+ if (SHELL_UNSAFE.test(root) || containsUnsafeContextChar(root)) return null;
300
+ return `cd "${root}" && yarn workspace @outerlayer/cli build && mkdir -p ~/.outerlayer/cli && rm -rf ~/.outerlayer/cli/dist && cp -r packages/cli/dist ~/.outerlayer/cli/dist`;
301
+ }
302
+
303
+ // src/hook-fast.ts
304
+ function spoolDir(home = homeFromEnv()) {
305
+ return join(home, ".outerlayer", "spool");
306
+ }
307
+ function hookFiredMarkerPath(event, home = homeFromEnv()) {
308
+ const safe = event.replace(/[^A-Za-z0-9_-]/g, "").slice(0, 64) || "unknown";
309
+ return join(spoolDir(home), `last-fired-${safe}`);
310
+ }
311
+ function fileSizeBytes(path) {
312
+ try {
313
+ return statSync(path).size;
314
+ } catch {
315
+ return 0;
316
+ }
317
+ }
318
+ function hookErrorsSeenPath(home) {
319
+ return join(spoolDir(home), "hook-errors-seen");
320
+ }
321
+ function hookErrorsSeenBytes(home) {
322
+ try {
323
+ const bytes = Number.parseInt(readFileSync(hookErrorsSeenPath(home), "utf8"), 10);
324
+ return bytes >= 0 ? bytes : 0;
325
+ } catch {
326
+ return 0;
327
+ }
328
+ }
329
+ function recordHookErrorsSeen(home, bytes) {
330
+ try {
331
+ writeFileSync(hookErrorsSeenPath(home), String(bytes) + "\n");
332
+ } catch {
333
+ }
334
+ }
335
+ function markHookFired(event, home) {
336
+ try {
337
+ writeFileSync(hookFiredMarkerPath(event, home), (/* @__PURE__ */ new Date()).toISOString() + "\n");
338
+ } catch {
339
+ }
340
+ }
341
+ var readStdin = () => {
342
+ try {
343
+ return readFileSync(0, "utf8");
344
+ } catch {
345
+ return "";
346
+ }
347
+ };
348
+ var AUTO_SYNC_DEBOUNCE_MS = 5 * 60 * 1e3;
349
+ function appendFd(path) {
350
+ if (path === void 0) return "ignore";
351
+ try {
352
+ return openSync(path, "a");
353
+ } catch {
354
+ return "ignore";
355
+ }
356
+ }
357
+ var spawnDetached = (command, args, stderrPath) => {
358
+ const child = spawn(command, args, { detached: true, stdio: ["ignore", "ignore", appendFd(stderrPath)] });
359
+ child.unref();
360
+ };
361
+ function maybeAutoSync(event, home = homeFromEnv(), spawnImpl = spawnDetached, now = Date.now()) {
362
+ try {
363
+ if (event !== "SessionEnd" && event !== "Stop") return;
364
+ const marker = join(spoolDir(home), "last-auto-sync");
365
+ try {
366
+ if (now - statSync(marker).mtimeMs < AUTO_SYNC_DEBOUNCE_MS) return;
367
+ } catch {
368
+ }
369
+ const configPath = join(home, ".outerlayer", "config.json");
370
+ if (!existsSync(configPath)) return;
371
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
372
+ if (config.autoSync === false) return;
373
+ if (!config.url || !config.apiKey || !config.appId) return;
374
+ writeFileSync(marker, String(now));
375
+ spawnImpl(process.execPath, [process.argv[1], "sync", "--quiet"]);
376
+ } catch {
377
+ }
378
+ }
379
+ var WORK_ADD_RETRIES = 4;
380
+ function hookErrorsContext(home, writtenBytes, seenBytes) {
381
+ if (writtenBytes === 0) return null;
382
+ if (writtenBytes === seenBytes) return null;
383
+ const path = join(spoolDir(home), "hook-errors.log");
384
+ return `OuterLayer recorded a hook failure since your last session started \u2014 a refused OUTERLAYER_WORK value, or a work-item session link that could not reach the Floor. The log is ${path}.`;
385
+ }
386
+ async function handleWorkLaunch(sessionId, home, env, spawnImpl = spawnDetached) {
387
+ if (!sessionId || env.OUTERLAYER_WORK === void 0) return void 0;
388
+ const { parseWorkItemEnv, appendLaunchRecord } = await import('./work-launch-YNNCKIH3.js');
389
+ const parsed = parseWorkItemEnv(env.OUTERLAYER_WORK);
390
+ if ("reason" in parsed) {
391
+ logHookError(home, new Error(`OUTERLAYER_WORK ignored: ${parsed.reason}`));
392
+ return `OuterLayer ignored this session's OUTERLAYER_WORK value: ${sanitizeForContext(parsed.reason)}. The session is not attached to a work item and will not upload. Hook failures are logged to ${join(spoolDir(home), "hook-errors.log")}.`;
393
+ }
394
+ appendLaunchRecord({ sessionId, item: parsed.number }, home);
395
+ spawnImpl(
396
+ process.execPath,
397
+ [
398
+ process.argv[1],
399
+ "work",
400
+ "link-session",
401
+ String(parsed.number),
402
+ "--session-id",
403
+ sessionId,
404
+ // The ordinary failure is a gateway that is briefly unreachable — a
405
+ // laptop that has not joined the network yet, a restart. Nothing else
406
+ // ever retries this link: the launch record is already written, so
407
+ // the session uploads whether or not the Floor ever hears about the
408
+ // link. Retrying costs nothing here (this process is detached; the
409
+ // hook has already returned) and cannot double-link, because the
410
+ // link is keyed by the session id passed above.
411
+ "--retry",
412
+ String(WORK_ADD_RETRIES)
413
+ ],
414
+ // The link runs after this hook has exited, so its own failure
415
+ // (offline, revoked key, unknown item number) has nowhere else to be
416
+ // seen: without this the session uploads later with no Floor link and
417
+ // no trace of why.
418
+ join(spoolDir(home), "hook-errors.log")
419
+ );
420
+ return void 0;
421
+ }
422
+ function syncHealthContext(home) {
423
+ if (!existsSync(join(home, ".outerlayer", "config.json"))) return null;
424
+ const health = assessSyncHealth(readSyncStatus(home));
425
+ if (health.level !== "error") return null;
426
+ return [`OuterLayer sync is failing on this machine: ${health.headline}`, ...health.details.map((d) => ` - ${d}`)].join("\n");
427
+ }
428
+ var UNTRUSTED_STALE_CLI_CONTEXT = "The installed OuterLayer CLI appears stale relative to a checkout near your working directory; run `outerlayer doctor` for details.";
429
+ function installedCliStalenessContext(home, cwd) {
430
+ const result = checkInstalledCliStaleness({ home, cwd });
431
+ if (result === null || !result.stale) return null;
432
+ if (!result.workspaceRootTrusted) return UNTRUSTED_STALE_CLI_CONTEXT;
433
+ const installedLabel = sanitizeForContext(result.installedId);
434
+ const workspaceLabel = sanitizeForContext(result.workspaceId);
435
+ const command = refreshCommand(result.workspaceDistDir, result.workspaceRootTrusted);
436
+ const howToRefresh = command ? `Refresh it: ${command}` : `Workspace build found at ${sanitizeForContext(workspaceRootOf(result.workspaceDistDir))}. Rebuild it (yarn workspace @outerlayer/cli build) and copy packages/cli/dist to ~/.outerlayer/cli/dist to refresh the installed copy.`;
437
+ return `The installed OuterLayer CLI (~/.outerlayer/cli) is behind the workspace build: installed ${installedLabel}, workspace ${workspaceLabel}.
438
+ ${howToRefresh}`;
439
+ }
440
+ function writeSessionStartContext(event, home, cwd, write, extraParts = []) {
441
+ try {
442
+ if (event !== "SessionStart") return;
443
+ const parts = [syncHealthContext(home), installedCliStalenessContext(home, cwd), ...extraParts].filter(
444
+ (s) => s !== null
445
+ );
446
+ if (parts.length === 0) return;
447
+ write(
448
+ JSON.stringify({
449
+ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: parts.join("\n\n") }
450
+ })
451
+ );
452
+ } catch {
453
+ }
454
+ }
455
+ var HEAL_RETRY_INTERVAL_MS = 6 * 60 * 60 * 1e3;
456
+ function healFailureMarkerPath(home, repoRoot) {
457
+ const key = createHash("sha256").update(repoRoot).digest("hex").slice(0, 16);
458
+ return join(spoolDir(home), `dead-hooks-heal-failed-${key}`);
459
+ }
460
+ function recentHealFailure(home, repoRoot) {
461
+ try {
462
+ const ageMs = Date.now() - statSync(healFailureMarkerPath(home, repoRoot)).mtimeMs;
463
+ return ageMs < HEAL_RETRY_INTERVAL_MS;
464
+ } catch {
465
+ return false;
466
+ }
467
+ }
468
+ function markHealFailure(home, repoRoot) {
469
+ try {
470
+ mkdirSync(spoolDir(home), { recursive: true });
471
+ writeFileSync(healFailureMarkerPath(home, repoRoot), (/* @__PURE__ */ new Date()).toISOString());
472
+ } catch {
473
+ }
474
+ }
475
+ function clearHealFailureMarker(home, repoRoot) {
476
+ try {
477
+ unlinkSync(healFailureMarkerPath(home, repoRoot));
478
+ } catch {
479
+ }
480
+ }
481
+ function deadHooksAutoHealContext(cwd, home) {
482
+ if (findGitRoot(cwd) === void 0) return null;
483
+ const state = deadHooksState(cwd);
484
+ if (!state.dead || state.repoRoot === void 0 || state.hooksDir === void 0) return null;
485
+ const hooksDirLabel = sanitizeForContext(state.hooksDir);
486
+ const script = huskyPrepareScript(state.repoRoot);
487
+ if (script === null) {
488
+ return `Git hooks are dead in this checkout (${hooksDirLabel} is missing or empty, via core.hooksPath) and package.json declares no husky prepare script to run automatically. Run \`outerlayer doctor\` for the fix.`;
489
+ }
490
+ if (recentHealFailure(home, state.repoRoot)) {
491
+ return `Git hooks are still dead in this checkout (${hooksDirLabel}) \u2014 a previous auto-heal attempt failed recently, so it will not retry yet. Run \`outerlayer doctor\` for the fix.`;
492
+ }
493
+ const command = preparePackageManagerCommand(state.repoRoot);
494
+ const [bin, ...args] = command.split(" ");
495
+ try {
496
+ execFileSync(bin, args, { cwd: state.repoRoot, stdio: "ignore", timeout: 3e4 });
497
+ } catch (err) {
498
+ markHealFailure(home, state.repoRoot);
499
+ return `OuterLayer tried to regenerate dead git hooks by running \`${command}\`, but it failed: ${sanitizeForContext(err instanceof Error ? err.message : String(err))}`;
500
+ }
501
+ const after = deadHooksState(state.repoRoot);
502
+ if (after.dead) {
503
+ markHealFailure(home, state.repoRoot);
504
+ return `OuterLayer ran \`${command}\` to regenerate dead git hooks, but ${hooksDirLabel} is still missing or empty afterward. Run \`outerlayer doctor\` for the fix.`;
505
+ }
506
+ clearHealFailureMarker(home, state.repoRoot);
507
+ return null;
508
+ }
509
+ var PIN_FILENAME = ".outerlayer-pin.json";
510
+ function sourceCachePath(gitCommonDir) {
511
+ return join(gitCommonDir, "outerlayer", "source.json");
512
+ }
513
+ function findGitRoot(cwd) {
514
+ let dir = cwd;
515
+ for (; ; ) {
516
+ const gitPath = join(dir, ".git");
517
+ let stat;
518
+ try {
519
+ stat = statSync(gitPath);
520
+ } catch {
521
+ stat = void 0;
522
+ }
523
+ if (stat?.isDirectory()) return { repoRoot: dir, gitCommonDir: gitPath };
524
+ if (stat?.isFile()) {
525
+ try {
526
+ const content = readFileSync(gitPath, "utf8").trim();
527
+ const match = /^gitdir:\s*(.+)$/.exec(content);
528
+ if (match) {
529
+ const gitdir = isAbsolute(match[1]) ? match[1] : join(dir, match[1]);
530
+ try {
531
+ const commondir = readFileSync(join(gitdir, "commondir"), "utf8").trim();
532
+ return { repoRoot: dir, gitCommonDir: isAbsolute(commondir) ? commondir : join(gitdir, commondir) };
533
+ } catch {
534
+ return { repoRoot: dir, gitCommonDir: gitdir };
535
+ }
536
+ }
537
+ } catch {
538
+ }
539
+ }
540
+ const parent = dirname(dir);
541
+ if (parent === dir) return void 0;
542
+ dir = parent;
543
+ }
544
+ }
545
+ function contextPossiblyGoverned(cwd, home = homeFromEnv()) {
546
+ try {
547
+ const found = findGitRoot(cwd);
548
+ if (found === void 0) return false;
549
+ const { repoRoot, gitCommonDir } = found;
550
+ if (existsSync(join(repoRoot, PIN_FILENAME))) return true;
551
+ if (!existsSync(join(home, ".outerlayer", "config.json"))) return false;
552
+ let cache;
553
+ try {
554
+ cache = JSON.parse(readFileSync(sourceCachePath(gitCommonDir), "utf8"));
555
+ } catch {
556
+ cache = void 0;
557
+ }
558
+ if (!cache) return true;
559
+ if (cache.status === "governed") return true;
560
+ if (typeof cache.checkedAt !== "string" || typeof cache.ttlMs !== "number") return true;
561
+ const fresh = Date.now() - Date.parse(cache.checkedAt) < cache.ttlMs;
562
+ return !fresh;
563
+ } catch {
564
+ return false;
565
+ }
566
+ }
567
+ async function runHookFast(event, home = homeFromEnv(), read = readStdin, env = process.env, spawnImpl = spawnDetached) {
568
+ try {
569
+ const dir = spoolDir(home);
570
+ mkdirSync(dir, { recursive: true });
571
+ const stdin = read();
572
+ let payload = {};
573
+ try {
574
+ payload = stdin ? JSON.parse(stdin) : {};
575
+ } catch {
576
+ payload = {};
577
+ }
578
+ const resolvedEvent = String(event ?? payload.hook_event_name ?? "unknown");
579
+ const record = {
580
+ t: (/* @__PURE__ */ new Date()).toISOString(),
581
+ event: resolvedEvent,
582
+ sessionId: payload.session_id ?? null,
583
+ transcriptPath: payload.transcript_path ?? null,
584
+ cwd: payload.cwd ?? null
585
+ };
586
+ appendFileSync(join(dir, "events.jsonl"), JSON.stringify(record) + "\n");
587
+ markHookFired(resolvedEvent, home);
588
+ maybeAutoSync(resolvedEvent, home, spawnImpl);
589
+ const sessionCwd = typeof payload.cwd === "string" ? payload.cwd : process.cwd();
590
+ let materializeMessage;
591
+ let deadHooksMessage;
592
+ let workLaunchMessage;
593
+ let hookErrorsMessage;
594
+ if (resolvedEvent === "SessionStart") {
595
+ const hookErrorsBytes = fileSizeBytes(join(dir, "hook-errors.log"));
596
+ const hookErrorsSeen = hookErrorsSeenBytes(home);
597
+ recordHookErrorsSeen(home, hookErrorsBytes);
598
+ if (contextPossiblyGoverned(sessionCwd, home)) {
599
+ try {
600
+ const { materializeContext: materialize } = await import('./context-materialize-J6CRQ7O7.js');
601
+ const sessionId = typeof payload.session_id === "string" ? payload.session_id : "unknown";
602
+ const result = await materialize({ cwd: sessionCwd, sessionId, home });
603
+ materializeMessage = result.message;
604
+ } catch (err) {
605
+ logHookError(home, err);
606
+ }
607
+ }
608
+ try {
609
+ deadHooksMessage = deadHooksAutoHealContext(sessionCwd, home) ?? void 0;
610
+ } catch (err) {
611
+ logHookError(home, err);
612
+ }
613
+ try {
614
+ const sessionId = typeof payload.session_id === "string" ? payload.session_id : void 0;
615
+ workLaunchMessage = await handleWorkLaunch(sessionId, home, env, spawnImpl);
616
+ } catch (err) {
617
+ logHookError(home, err);
618
+ }
619
+ if (workLaunchMessage === void 0) {
620
+ hookErrorsMessage = hookErrorsContext(home, hookErrorsBytes, hookErrorsSeen) ?? void 0;
621
+ }
622
+ }
623
+ writeSessionStartContext(
624
+ resolvedEvent,
625
+ home,
626
+ sessionCwd,
627
+ (out) => process.stdout.write(out),
628
+ [materializeMessage, deadHooksMessage, workLaunchMessage, hookErrorsMessage].filter(
629
+ (s) => s !== void 0
630
+ )
631
+ );
632
+ } catch (err) {
633
+ logHookError(home, err);
634
+ }
635
+ }
636
+ function logHookError(home, err) {
637
+ try {
638
+ const dir = spoolDir(home);
639
+ mkdirSync(dir, { recursive: true });
640
+ writeFileSync(
641
+ join(dir, "hook-errors.log"),
642
+ `${(/* @__PURE__ */ new Date()).toISOString()} ${err instanceof Error ? err.message : String(err)}
643
+ `,
644
+ { flag: "a" }
645
+ );
646
+ } catch {
647
+ }
648
+ }
649
+
650
+ export { AUTO_SYNC_DEBOUNCE_MS, SYNC_STALE_AFTER_MS, WORK_ADD_RETRIES, assessSyncHealth, checkInstalledCliStaleness, contextPossiblyGoverned, formatAgo, handleWorkLaunch, hookFiredMarkerPath, maybeAutoSync, readSyncStatus, refreshCommand, runHookFast, sanitizeForContext, spoolDir, workspaceRootOf, writeSyncStatus };
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { isSameProcess } from './chunk-OZ7C3XUE.js';
4
+ import { RUNNER_DIR_MODE, RUNNER_FILE_MODE } from './chunk-JJP7YLMN.js';
5
+ import { existsSync, readFileSync, rmSync, mkdirSync, writeFileSync } from 'fs';
6
+ import { dirname } from 'path';
7
+
8
+ createRequire(import.meta.url);
9
+ function readPidFile(path) {
10
+ if (!existsSync(path)) return null;
11
+ try {
12
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
13
+ if (typeof parsed.pid !== "number" || typeof parsed.start !== "string") return null;
14
+ return { pid: parsed.pid, start: parsed.start };
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ function writePidFile(path, entry) {
20
+ mkdirSync(dirname(path), { recursive: true, mode: RUNNER_DIR_MODE });
21
+ writeFileSync(path, JSON.stringify(entry), { mode: RUNNER_FILE_MODE });
22
+ }
23
+ function releasePidFile(path, self) {
24
+ const existing = readPidFile(path);
25
+ if (!existing || existing.pid !== self.pid || existing.start !== self.start) return;
26
+ rmSync(path, { force: true });
27
+ }
28
+ var RunnerAlreadyRunningError = class extends Error {
29
+ constructor(pid) {
30
+ super(`a runner is already running on this config (pid ${pid})`);
31
+ this.pid = pid;
32
+ }
33
+ };
34
+ function claimPidFile(path, self, isSameProcessFn = isSameProcess) {
35
+ const existing = readPidFile(path);
36
+ if (existing && isSameProcessFn(existing.pid, existing.start)) {
37
+ throw new RunnerAlreadyRunningError(existing.pid);
38
+ }
39
+ writePidFile(path, self);
40
+ }
41
+
42
+ export { RunnerAlreadyRunningError, claimPidFile, readPidFile, releasePidFile };