@seanyao/roll 4.703.2 → 4.706.1

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.
@@ -0,0 +1,731 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __rollCreateRequire } from 'node:module';
3
+ import { fileURLToPath as __rollFileURLToPath } from 'node:url';
4
+ import { dirname as __rollDirname } from 'node:path';
5
+ const require = __rollCreateRequire(import.meta.url);
6
+ const __filename = __rollFileURLToPath(import.meta.url);
7
+ const __dirname = __rollDirname(__filename);
8
+
9
+ // packages/cli/src/lib/roll-capture-install.ts
10
+ import { spawnSync } from "node:child_process";
11
+ import {
12
+ chmodSync,
13
+ existsSync,
14
+ lstatSync,
15
+ mkdirSync as mkdirSync2,
16
+ mkdtempSync,
17
+ readdirSync,
18
+ readFileSync as readFileSync2,
19
+ renameSync as renameSync2,
20
+ rmSync as rmSync2,
21
+ statSync as statSync2,
22
+ writeFileSync as writeFileSync2
23
+ } from "node:fs";
24
+ import { homedir } from "node:os";
25
+ import { join as join2 } from "node:path";
26
+
27
+ // packages/spec/dist/i18n/index.js
28
+ function classifyLocale(raw) {
29
+ return raw.startsWith("zh") ? "zh" : "en";
30
+ }
31
+ function resolveLang(sources) {
32
+ for (const v of [
33
+ sources.rollLang,
34
+ sources.configLang,
35
+ sources.lcAll,
36
+ sources.lang,
37
+ sources.appleLang
38
+ ]) {
39
+ if (v !== void 0 && v !== "")
40
+ return classifyLocale(v);
41
+ }
42
+ return "en";
43
+ }
44
+
45
+ // packages/spec/dist/types/terminal.js
46
+ var TERMINAL_SCHEMA_EPOCH_SEC = Date.UTC(2026, 5, 10, 17, 30) / 1e3;
47
+
48
+ // packages/spec/dist/types/truth-registry.js
49
+ var RUNNER = "cycle runner append_run (buildRunRow)";
50
+ var BACKFILL = "merge-evidence backfill (runs-backfill, FIX-243)";
51
+ var REBUILD_BACKFILL = "roll loop run-once post-cycle backfill / backfillMergedRuns";
52
+ var TRUTH_FIELD_REGISTRY = [
53
+ // ── goal.yaml (US-GOAL-001) ────────────────────────────────────────────────
54
+ { field: "schema", surface: "goal", anchor: "goal_state", writer: "goal control plane", kind: "authoritative" },
55
+ { field: "scope", surface: "goal", anchor: "goal_state", writer: "goal control plane", kind: "authoritative" },
56
+ { field: "review", surface: "goal", anchor: "goal_state", writer: "goal control plane", kind: "authoritative" },
57
+ { field: "limits", surface: "goal", anchor: "goal_state", writer: "goal control plane", kind: "authoritative" },
58
+ { field: "status", surface: "goal", anchor: "goal_state", writer: "goal control plane; complete only by adjudication", kind: "authoritative" },
59
+ { field: "usage", surface: "goal", anchor: "goal_state", writer: "goal control plane from runs ledger", kind: "derived-cache", rebuild: "sum cycles/cost from scoped runs rows" },
60
+ { field: "createdAt", surface: "goal", anchor: "goal_state", writer: "goal control plane", kind: "authoritative" },
61
+ { field: "updatedAt", surface: "goal", anchor: "goal_state", writer: "goal control plane", kind: "authoritative" },
62
+ { field: "lastDecisionReason", surface: "goal", anchor: "goal_state", writer: "goal control plane / adjudicator", kind: "authoritative" },
63
+ // ── runs.jsonl row (cycle_outcome anchor's primary view) ───────────────────
64
+ { field: "run_id", surface: "runs", anchor: "cycle_outcome", writer: RUNNER, kind: "authoritative" },
65
+ { field: "cycle_id", surface: "runs", anchor: "cycle_outcome", writer: RUNNER, kind: "authoritative" },
66
+ { field: "story_id", surface: "runs", anchor: "cycle_outcome", writer: RUNNER, kind: "authoritative" },
67
+ { field: "status", surface: "runs", anchor: "cycle_outcome", writer: `${RUNNER}; corrected only by ${BACKFILL}`, kind: "authoritative" },
68
+ { field: "outcome", surface: "runs", anchor: "cycle_outcome", writer: `${RUNNER}; corrected only by ${BACKFILL}`, kind: "authoritative" },
69
+ { field: "failure_class", surface: "runs", anchor: "cycle_outcome", writer: RUNNER, kind: "authoritative" },
70
+ { field: "root_cause_key", surface: "runs", anchor: "cycle_outcome", writer: RUNNER, kind: "authoritative" },
71
+ { field: "agent", surface: "runs", anchor: "cycle_outcome", writer: RUNNER, kind: "authoritative" },
72
+ { field: "built", surface: "runs", anchor: "story_delivery", writer: RUNNER, kind: "derived-cache", rebuild: "re-derive from story_id + delivery truth (deriveStoryTruth)" },
73
+ { field: "tcr_count", surface: "runs", anchor: "tcr_evidence", writer: RUNNER, kind: "derived-cache", rebuild: "recount tcr: commits on the cycle branch" },
74
+ { field: "ts", surface: "runs", anchor: "cycle_outcome", writer: RUNNER, kind: "authoritative" },
75
+ { field: "duration_sec", surface: "runs", anchor: "cycle_outcome", writer: RUNNER, kind: "derived-cache", rebuild: "ended_at - started_at from the cycle:terminal twin" },
76
+ { field: "model", surface: "runs", anchor: "usage_cost", writer: RUNNER, kind: "derived-cache", rebuild: "re-extract from agent usage records (usage-recovery / stream parse)" },
77
+ { field: "tokens_in", surface: "runs", anchor: "usage_cost", writer: RUNNER, kind: "derived-cache", rebuild: "re-extract from agent usage records" },
78
+ { field: "tokens_out", surface: "runs", anchor: "usage_cost", writer: RUNNER, kind: "derived-cache", rebuild: "re-extract from agent usage records" },
79
+ { field: "tokens_cache_read", surface: "runs", anchor: "usage_cost", writer: RUNNER, kind: "derived-cache", rebuild: "re-extract from agent usage records" },
80
+ { field: "tokens_cache_write", surface: "runs", anchor: "usage_cost", writer: RUNNER, kind: "derived-cache", rebuild: "re-extract from agent usage records" },
81
+ { field: "cost_usd", surface: "runs", anchor: "usage_cost", writer: RUNNER, kind: "derived-cache", rebuild: "recompute from tokens via the price table (toCycleCost)" },
82
+ { field: "cost_effective_usd", surface: "runs", anchor: "usage_cost", writer: RUNNER, kind: "derived-cache", rebuild: "estimated \xD7 (reverts + 1) \u2014 toCycleCost" },
83
+ { field: "cost_currency", surface: "runs", anchor: "usage_cost", writer: RUNNER, kind: "derived-cache", rebuild: "resolve from model's price-table currency (currencyFor / cycleCurrency)" },
84
+ { field: "pr_number", surface: "runs", anchor: "pr_merge", writer: `${RUNNER}; corrected by ${BACKFILL}`, kind: "authoritative" },
85
+ { field: "pr_url", surface: "runs", anchor: "pr_merge", writer: `${RUNNER}; corrected by ${BACKFILL}`, kind: "authoritative" },
86
+ { field: "merged_at", surface: "runs", anchor: "pr_merge", writer: BACKFILL, kind: "derived-cache", rebuild: REBUILD_BACKFILL },
87
+ { field: "merge_commit", surface: "runs", anchor: "pr_merge", writer: BACKFILL, kind: "derived-cache", rebuild: REBUILD_BACKFILL },
88
+ // ── cycle:terminal event (US-TRUTH-001) ────────────────────────────────────
89
+ { field: "type", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
90
+ { field: "schema", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
91
+ { field: "cycleId", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
92
+ { field: "storyId", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
93
+ { field: "agent", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
94
+ // FIX-294: routed model is a dispatch-time fact (like agent) — authoritative,
95
+ // ALWAYS present even when usage couldn't be parsed; the `usage` fact below
96
+ // still owns the present-or-reasoned token/cost truth.
97
+ { field: "model", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
98
+ { field: "startedAt", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
99
+ { field: "endedAt", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
100
+ { field: "outcome", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
101
+ { field: "failure_class", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
102
+ { field: "root_cause_key", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
103
+ { field: "pr", surface: "event:cycle:terminal", anchor: "pr_merge", writer: "buildTerminalEvent", kind: "derived-cache", rebuild: "gh pr view on the cycle branch" },
104
+ { field: "branch", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
105
+ { field: "commit", surface: "event:cycle:terminal", anchor: "tcr_evidence", writer: "buildTerminalEvent", kind: "derived-cache", rebuild: "git rev-parse on the cycle branch" },
106
+ { field: "tcr", surface: "event:cycle:terminal", anchor: "tcr_evidence", writer: "buildTerminalEvent", kind: "derived-cache", rebuild: "recount tcr: commits on the cycle branch" },
107
+ { field: "attest", surface: "event:cycle:terminal", anchor: "attest_evidence", writer: "buildTerminalEvent", kind: "derived-cache", rebuild: "probe the card archive (report + ac-map existence)" },
108
+ { field: "usage", surface: "event:cycle:terminal", anchor: "usage_cost", writer: "buildTerminalEvent", kind: "derived-cache", rebuild: "re-extract from agent usage records" },
109
+ { field: "cost", surface: "event:cycle:terminal", anchor: "usage_cost", writer: "buildTerminalEvent", kind: "derived-cache", rebuild: "recompute from tokens via the price table" },
110
+ { field: "ts", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
111
+ // ── delivery record (US-TRUTH-013) ───────────────────────────────────────
112
+ { field: "storyId", surface: "delivery", anchor: "story_delivery", writer: "cycle runner buildDeliveryRecord", kind: "authoritative" },
113
+ { field: "cycleId", surface: "delivery", anchor: "cycle_outcome", writer: "cycle runner buildDeliveryRecord", kind: "authoritative" },
114
+ { field: "lifecycleState", surface: "delivery", anchor: "story_delivery", writer: "cycle runner buildDeliveryRecord", kind: "authoritative" },
115
+ { field: "prNumber", surface: "delivery", anchor: "pr_merge", writer: "cycle runner buildDeliveryRecord", kind: "authoritative" },
116
+ { field: "prUrl", surface: "delivery", anchor: "pr_merge", writer: "cycle runner buildDeliveryRecord", kind: "authoritative" },
117
+ { field: "mergedAt", surface: "delivery", anchor: "pr_merge", writer: "cycle runner buildDeliveryRecord", kind: "authoritative" },
118
+ { field: "mergeCommit", surface: "delivery", anchor: "pr_merge", writer: "cycle runner buildDeliveryRecord", kind: "authoritative" },
119
+ { field: "recordedAt", surface: "delivery", anchor: "story_delivery", writer: "cycle runner buildDeliveryRecord", kind: "authoritative" },
120
+ // ── release records (US-TRUTH-005) ────────────────────────────────────────
121
+ { field: "type", surface: "event:release:gate", anchor: "release_verdict", writer: "release ship recordGate", kind: "authoritative" },
122
+ { field: "tag", surface: "event:release:gate", anchor: "release_verdict", writer: "release ship recordGate", kind: "authoritative" },
123
+ { field: "verdict", surface: "event:release:gate", anchor: "release_verdict", writer: "release ship recordGate", kind: "authoritative" },
124
+ { field: "failCount", surface: "event:release:gate", anchor: "release_verdict", writer: "release ship recordGate", kind: "authoritative" },
125
+ { field: "waivedRules", surface: "event:release:gate", anchor: "release_waiver", writer: "release ship recordGate", kind: "derived-cache", rebuild: "re-join gate findings with live waivers (decideReleaseGate)" },
126
+ { field: "ts", surface: "event:release:gate", anchor: "release_verdict", writer: "release ship recordGate", kind: "authoritative" },
127
+ { field: "type", surface: "event:release:waiver", anchor: "release_waiver", writer: "(retired US-REL-007 \u2014 historical release:waiver events only; no writer remains)", kind: "authoritative" },
128
+ { field: "reason", surface: "event:release:waiver", anchor: "release_waiver", writer: "(retired US-REL-007 \u2014 historical release:waiver events only; no writer remains)", kind: "authoritative" },
129
+ { field: "scope", surface: "event:release:waiver", anchor: "release_waiver", writer: "(retired US-REL-007 \u2014 historical release:waiver events only; no writer remains)", kind: "authoritative" },
130
+ { field: "expiresSec", surface: "event:release:waiver", anchor: "release_waiver", writer: "(retired US-REL-007 \u2014 historical release:waiver events only; no writer remains)", kind: "authoritative" },
131
+ { field: "operator", surface: "event:release:waiver", anchor: "release_waiver", writer: "(retired US-REL-007 \u2014 historical release:waiver events only; no writer remains)", kind: "authoritative" },
132
+ { field: "ts", surface: "event:release:waiver", anchor: "release_waiver", writer: "(retired US-REL-007 \u2014 historical release:waiver events only; no writer remains)", kind: "authoritative" }
133
+ ];
134
+
135
+ // packages/spec/dist/types/story.js
136
+ var STATUS_MARKER = {
137
+ todo: "\u{1F4CB} Todo",
138
+ in_progress: "\u{1F528} In Progress",
139
+ done: "\u2705 Done",
140
+ hold: "\u{1F6AB} Hold",
141
+ cut: "\u{1F5D1}\uFE0F Cut"
142
+ };
143
+ var AWAITING_REVIEW_STATUS_MARKER = "\u23F3 \u5F85\u590D\u8BC4";
144
+ var LEGACY_STATUS_MARKERS = [
145
+ { marker: "\u2714\uFE0F Done", status: "done" },
146
+ { marker: "\u{1F6A7} WIP", status: "in_progress" },
147
+ { marker: "\u{1F504} In Progress", status: "in_progress" },
148
+ { marker: "\u23F3 Hold", status: "hold" },
149
+ { marker: "\u{1F512} Blocked", status: "hold" },
150
+ { marker: "\u23F8 Deferred", status: "hold" }
151
+ ];
152
+ var STATUS_MARKER_ALTERNATIVES = [
153
+ // Canonical first (these are what writers emit), then legacy aliases.
154
+ { marker: STATUS_MARKER.done, status: "done" },
155
+ { marker: STATUS_MARKER.in_progress, status: "in_progress" },
156
+ { marker: STATUS_MARKER.hold, status: "hold" },
157
+ { marker: STATUS_MARKER.cut, status: "cut" },
158
+ { marker: STATUS_MARKER.todo, status: "todo" },
159
+ { marker: AWAITING_REVIEW_STATUS_MARKER, status: "todo" },
160
+ ...LEGACY_STATUS_MARKERS
161
+ ];
162
+ function markerToPattern(marker) {
163
+ return marker.split(/\s+/).map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(" *");
164
+ }
165
+ var STATUS_MARKER_RE_SOURCE = `(${STATUS_MARKER_ALTERNATIVES.map((a) => markerToPattern(a.marker)).join("|")})`;
166
+ function statusMarkerRe(global = false) {
167
+ return new RegExp(STATUS_MARKER_RE_SOURCE, global ? "g" : void 0);
168
+ }
169
+ var STATUS_MARKER_RE = statusMarkerRe(false);
170
+
171
+ // packages/spec/dist/types/capture.js
172
+ var ROLL_CAPTURE_PROTOCOL_V1 = "roll.capture.v1";
173
+ var ROLL_CAPTURE_PROVIDER = "roll-capture";
174
+ var ROLL_CAPTURE_PLATFORM = "darwin";
175
+ var ROLL_CAPTURE_HOST_APP_NAME = "Roll Capture.app";
176
+ var ROLL_CAPTURE_HOST_BUNDLE_ID = "com.seanyao.roll.capture";
177
+ var stringSchema = { type: "string" };
178
+ var nonEmptyStringSchema = { type: "string", minLength: 1 };
179
+ var integerSchema = { type: "integer" };
180
+ var booleanSchema = { type: "boolean" };
181
+ var numberSchema = { type: "number" };
182
+ function objectSchema(properties, required = []) {
183
+ return { type: "object", properties, required, additionalProperties: false };
184
+ }
185
+ var captureKindSchema = { type: "string", enum: ["web", "terminal", "physical_terminal", "region", "display"] };
186
+ var captureStatusSchema = { type: "string", enum: ["taken", "skipped", "failed"] };
187
+ var captureTargetSchema = {
188
+ oneOf: [
189
+ objectSchema({ type: { const: "window" }, appName: nonEmptyStringSchema, windowTitle: stringSchema }, ["type", "appName"]),
190
+ objectSchema({ type: { const: "region" }, x: integerSchema, y: integerSchema, width: integerSchema, height: integerSchema }, ["type", "x", "y", "width", "height"]),
191
+ objectSchema({ type: { const: "display" }, displayId: stringSchema }, ["type"])
192
+ ]
193
+ };
194
+ var captureHostSchema = objectSchema({
195
+ appName: { const: ROLL_CAPTURE_HOST_APP_NAME },
196
+ bundleId: { const: ROLL_CAPTURE_HOST_BUNDLE_ID },
197
+ version: nonEmptyStringSchema
198
+ }, ["appName", "bundleId", "version"]);
199
+ var rollCaptureRequestV1Schema = objectSchema({
200
+ protocol: { const: ROLL_CAPTURE_PROTOCOL_V1 },
201
+ requestId: nonEmptyStringSchema,
202
+ storyId: stringSchema,
203
+ runId: stringSchema,
204
+ kind: captureKindSchema,
205
+ target: captureTargetSchema,
206
+ out: nonEmptyStringSchema,
207
+ timeoutMs: integerSchema,
208
+ createdAt: nonEmptyStringSchema
209
+ }, ["protocol", "requestId", "kind", "target", "out", "timeoutMs", "createdAt"]);
210
+ var captureResponseProperties = {
211
+ protocol: { const: ROLL_CAPTURE_PROTOCOL_V1 },
212
+ requestId: nonEmptyStringSchema,
213
+ screenshotPath: stringSchema,
214
+ responsePath: nonEmptyStringSchema,
215
+ reason: stringSchema,
216
+ imageWidth: integerSchema,
217
+ imageHeight: integerSchema,
218
+ host: captureHostSchema,
219
+ startedAt: nonEmptyStringSchema,
220
+ finishedAt: nonEmptyStringSchema
221
+ };
222
+ var captureResponseRequired = ["protocol", "requestId", "status", "responsePath", "host", "startedAt", "finishedAt"];
223
+ var rollCaptureResponseV1Schema = {
224
+ oneOf: [
225
+ objectSchema({ ...captureResponseProperties, status: { const: "taken" } }, [...captureResponseRequired, "screenshotPath"]),
226
+ objectSchema({ ...captureResponseProperties, status: { const: "skipped" } }, [...captureResponseRequired, "reason"]),
227
+ objectSchema({ ...captureResponseProperties, status: { const: "failed" } }, [...captureResponseRequired, "reason"])
228
+ ]
229
+ };
230
+ var captureLedgerEntrySchema = objectSchema({
231
+ requestId: nonEmptyStringSchema,
232
+ storyId: stringSchema,
233
+ runId: stringSchema,
234
+ kind: captureKindSchema,
235
+ status: captureStatusSchema,
236
+ screenshotPath: stringSchema,
237
+ responsePath: nonEmptyStringSchema,
238
+ reportPath: stringSchema,
239
+ attachedToReport: booleanSchema,
240
+ reason: stringSchema,
241
+ startedAt: nonEmptyStringSchema,
242
+ finishedAt: nonEmptyStringSchema
243
+ }, ["requestId", "status", "responsePath", "attachedToReport", "startedAt", "finishedAt"]);
244
+ var rollCaptureToolOutputSchema = toolResultSchema(objectSchema({
245
+ status: { type: "string", enum: ["taken", "skipped", "failed", "timeout"] },
246
+ path: stringSchema,
247
+ reason: stringSchema,
248
+ response: rollCaptureResponseV1Schema
249
+ }, ["status"]));
250
+ var physicalScreenshotToolInputSchema = {
251
+ title: "Roll Capture request",
252
+ oneOf: [rollCaptureRequestV1Schema]
253
+ };
254
+ var PHYSICAL_SCREENSHOT_TOOL_CONTRACT = {
255
+ id: "physical.screenshot",
256
+ kind: "physical",
257
+ title: "Physical Screenshot",
258
+ description: "Request real physical screen pixels through Roll Capture.app.",
259
+ provider: ROLL_CAPTURE_PROVIDER,
260
+ protocol: ROLL_CAPTURE_PROTOCOL_V1,
261
+ platform: ROLL_CAPTURE_PLATFORM,
262
+ defaults: {
263
+ enabled: true,
264
+ timeoutMs: 6e4,
265
+ sandbox: { maxOutputBytes: 2 * 1024 * 1024 }
266
+ },
267
+ requirements: [{ kind: "service", name: "roll-capture-app", optional: false }],
268
+ inputSchema: physicalScreenshotToolInputSchema,
269
+ outputSchema: rollCaptureToolOutputSchema
270
+ };
271
+ function toolResultSchema(output) {
272
+ const meta = objectSchema({
273
+ invocationId: stringSchema,
274
+ toolId: stringSchema,
275
+ caller: { type: "object", additionalProperties: true },
276
+ startedAt: numberSchema,
277
+ endedAt: numberSchema,
278
+ durationMs: numberSchema,
279
+ attempt: integerSchema
280
+ }, ["invocationId", "toolId", "caller", "startedAt", "endedAt", "durationMs"]);
281
+ const error = objectSchema({
282
+ code: { type: "string", enum: ["not_found", "init_failed", "policy_denied", "budget_exhausted", "sandbox_denied", "timeout", "adapter_error", "invalid_input", "unknown"] },
283
+ message: stringSchema,
284
+ retryable: booleanSchema,
285
+ detail: true
286
+ }, ["code", "message", "retryable"]);
287
+ return {
288
+ oneOf: [
289
+ objectSchema({ ok: { const: true }, output, meta, warnings: { type: "array", items: stringSchema } }, ["ok", "output", "meta"]),
290
+ objectSchema({ ok: { const: false }, error, meta, warnings: { type: "array", items: stringSchema } }, ["ok", "error", "meta"])
291
+ ]
292
+ };
293
+ }
294
+
295
+ // packages/cli/src/lib/roll-capture-readiness.ts
296
+ import { mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
297
+ import { dirname, join } from "node:path";
298
+ var APP_NAME = "Roll Capture.app";
299
+ var ROLL_CAPTURE_POSITIVE_READINESS_TTL_MS = 30 * 60 * 1e3;
300
+ var ROLL_CAPTURE_NEGATIVE_READINESS_TTL_MS = 60 * 1e3;
301
+ function detectCanonicalRollCaptureInstall(deps) {
302
+ for (const candidate of [join(deps.home, "Applications", APP_NAME), join("/Applications", APP_NAME)]) {
303
+ if (deps.exists(candidate)) return { status: "installed", path: candidate };
304
+ }
305
+ return { status: "missing" };
306
+ }
307
+ function rollCaptureReadinessCachePath(deps) {
308
+ const rollHome = (deps.env["ROLL_HOME"] ?? "").trim() || join(deps.home, ".roll");
309
+ return join(rollHome, "cache", "roll-capture-readiness.json");
310
+ }
311
+ function invalidateRollCaptureReadinessCache(deps) {
312
+ rmSync(rollCaptureReadinessCachePath(deps), { force: true });
313
+ }
314
+
315
+ // packages/cli/src/lib/roll-capture-install.ts
316
+ var RELEASE_API = "https://api.github.com/repos/seanyao/roll-capture/releases/latest";
317
+ var ASSET_NAME = "Roll-Capture.app.zip";
318
+ var APP_NAME2 = "Roll Capture.app";
319
+ var DOWNLOAD_TIMEOUT_MS = 6e4;
320
+ var UPDATE_HINT_TIMEOUT_MS = 1500;
321
+ var GH_AUTH_TOKEN_TIMEOUT_MS = 5e3;
322
+ var ROOT_INSTALL_SKIP_REASON = "run roll setup as a regular user to install Roll Capture.app";
323
+ function defaultRollCaptureInstallDeps() {
324
+ const platform = externalPlatformOverride(process.env["_ROLL_EXTERNAL_TOOLS_PLATFORM"]) ?? process.platform;
325
+ const execFile = (cmd, args, opts) => {
326
+ const r = spawnSync(cmd, [...args], {
327
+ cwd: opts?.cwd,
328
+ encoding: "utf8",
329
+ stdio: ["ignore", "pipe", "pipe"],
330
+ timeout: opts?.timeoutMs ?? 15e3
331
+ });
332
+ return { code: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
333
+ };
334
+ return {
335
+ platform,
336
+ env: process.env,
337
+ home: homedir(),
338
+ uid: typeof process.getuid === "function" ? process.getuid() : void 0,
339
+ ...platform === "darwin" && !isCi(process.env) && process.env["ROLL_SKIP_CAPTURE_INSTALL"] !== "1" ? { hasAquaGUI: macosHasAquaGUI(execFile) } : {},
340
+ exists: existsSync,
341
+ renamePath: renameSync2,
342
+ execFile,
343
+ fetchLatestRelease: (timeoutMs) => defaultFetchLatestRelease(timeoutMs, process.env, execFile),
344
+ downloadAsset: (asset, timeoutMs) => defaultDownloadAsset(asset, timeoutMs, process.env, execFile),
345
+ extractZip: async (zipPath, destination, timeoutMs) => extractZip(zipPath, destination, execFile, timeoutMs)
346
+ };
347
+ }
348
+ async function installRollCapture(deps = defaultRollCaptureInstallDeps()) {
349
+ const gate = installGate(deps);
350
+ if (gate !== null) return gate;
351
+ const installed = detectCanonicalRollCaptureInstall(deps);
352
+ if (installed.status === "installed") {
353
+ const appPath = installed.path;
354
+ invalidateRollCaptureReadinessCache(deps);
355
+ const installedVersion = appPath === void 0 ? void 0 : readInstalledVersion(appPath, deps);
356
+ const update = await latestReleaseHint(deps, installedVersion);
357
+ return {
358
+ status: "already-installed",
359
+ reason: "installed",
360
+ ...appPath !== void 0 ? { appPath } : {},
361
+ ...installedVersion !== void 0 ? { installedVersion } : {},
362
+ ...update.releaseTag !== void 0 ? { releaseTag: update.releaseTag } : {},
363
+ updateAvailable: update.updateAvailable
364
+ };
365
+ }
366
+ const deadlineMs = Date.now() + DOWNLOAD_TIMEOUT_MS;
367
+ const remainingMs = () => Math.max(1, deadlineMs - Date.now());
368
+ let release;
369
+ try {
370
+ release = await deps.fetchLatestRelease(remainingMs());
371
+ } catch (error) {
372
+ return manual(`release lookup failed: ${errorMessage(error)}`);
373
+ }
374
+ const asset = release.assets.find((candidate) => candidate.name === ASSET_NAME);
375
+ if (asset === void 0) return manual(`latest release has no ${ASSET_NAME} asset`);
376
+ let bytes;
377
+ try {
378
+ bytes = await deps.downloadAsset(asset, remainingMs());
379
+ } catch (error) {
380
+ return manual(`download failed: ${errorMessage(error)}`, release.tagName);
381
+ }
382
+ if (bytes.byteLength !== asset.size) {
383
+ return manual(`download size mismatch: expected ${asset.size} bytes, got ${bytes.byteLength}`, release.tagName);
384
+ }
385
+ const applicationsDir = join2(deps.home, "Applications");
386
+ const targetApp = join2(applicationsDir, APP_NAME2);
387
+ mkdirSync2(applicationsDir, { recursive: true });
388
+ const workDir = mkdtempSync(join2(applicationsDir, ".roll-capture-install-"));
389
+ const zipPath = join2(workDir, ASSET_NAME);
390
+ const extractDir = join2(workDir, "extract");
391
+ try {
392
+ mkdirSync2(extractDir, { recursive: true });
393
+ writeFileSync2(zipPath, bytes);
394
+ const extracted = await deps.extractZip(zipPath, extractDir, remainingMs());
395
+ if (!extracted.ok) return manual(extracted.detail, release.tagName);
396
+ const app = findExtractedApp(extractDir);
397
+ if (app === null) return manual("zip did not contain Roll Capture.app", release.tagName);
398
+ const executable = findBundleExecutable(app);
399
+ if (executable === null) return manual("Roll Capture.app has no executable in Contents/MacOS", release.tagName);
400
+ chmodSync(executable, statSync2(executable).mode | 73);
401
+ replaceAppAtomically(app, targetApp, deps);
402
+ invalidateRollCaptureReadinessCache(deps);
403
+ return { status: "installed", reason: "installed", appPath: targetApp, releaseTag: release.tagName };
404
+ } catch (error) {
405
+ return manual(`install failed: ${errorMessage(error)}`, release.tagName);
406
+ } finally {
407
+ rmSync2(workDir, { recursive: true, force: true });
408
+ }
409
+ }
410
+ function renderRollCaptureInstallResult(result, lang) {
411
+ if (lang === "zh") return renderZh(result);
412
+ return renderEn(result);
413
+ }
414
+ async function runRollCapturePostinstall(opts) {
415
+ const writeLine = opts?.writeLine ?? ((line) => process.stdout.write(`${line}
416
+ `));
417
+ const lang = opts?.lang ?? resolveLang({ rollLang: process.env["ROLL_LANG"], lcAll: process.env["LC_ALL"], lang: process.env["LANG"] });
418
+ try {
419
+ const result = await installRollCapture(opts?.deps);
420
+ writeLine(renderRollCaptureInstallResult(result, lang));
421
+ } catch (error) {
422
+ writeLine(lang === "zh" ? `Roll Capture.app \u5B89\u88C5\u8DF3\u8FC7\uFF1A${errorMessage(error)}` : `Roll Capture.app install skipped: ${errorMessage(error)}`);
423
+ }
424
+ return 0;
425
+ }
426
+ async function defaultFetchLatestRelease(timeoutMs, env, execFile, fetchImpl = fetch, createProxyDispatcher = defaultProxyDispatcher) {
427
+ const response = await fetchImpl(
428
+ RELEASE_API,
429
+ await withProxyDispatcher(
430
+ {
431
+ headers: { Accept: "application/vnd.github+json", "User-Agent": "@seanyao/roll" },
432
+ signal: AbortSignal.timeout(timeoutMs)
433
+ },
434
+ env,
435
+ createProxyDispatcher
436
+ )
437
+ );
438
+ if (response.ok) return parseRelease(await response.json());
439
+ if (!isAuthRetryableStatus(response.status)) throw new Error(`GitHub release API returned ${response.status}`);
440
+ for (const credential of credentialFallbacks(env, execFile)) {
441
+ const authed = await fetchImpl(
442
+ RELEASE_API,
443
+ await withProxyDispatcher(
444
+ {
445
+ headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${credential.token}`, "User-Agent": "@seanyao/roll" },
446
+ signal: AbortSignal.timeout(timeoutMs)
447
+ },
448
+ env,
449
+ createProxyDispatcher
450
+ )
451
+ );
452
+ if (authed.ok) return parseRelease(await authed.json());
453
+ if (!isAuthRetryableStatus(authed.status)) throw new Error(`GitHub release API returned ${authed.status}`);
454
+ }
455
+ throw new Error(`GitHub release API returned ${response.status}`);
456
+ }
457
+ async function defaultDownloadAsset(asset, timeoutMs, env, execFile, fetchImpl = fetch, createProxyDispatcher = defaultProxyDispatcher) {
458
+ const response = await fetchImpl(
459
+ asset.browserDownloadUrl,
460
+ await withProxyDispatcher(
461
+ {
462
+ headers: { "User-Agent": "@seanyao/roll" },
463
+ redirect: "follow",
464
+ signal: AbortSignal.timeout(timeoutMs)
465
+ },
466
+ env,
467
+ createProxyDispatcher
468
+ )
469
+ );
470
+ if (response.ok) return new Uint8Array(await response.arrayBuffer());
471
+ if (!isAuthRetryableStatus(response.status)) throw new Error(`asset download returned ${response.status}`);
472
+ if (asset.apiUrl === void 0 || asset.apiUrl === "") throw new Error("asset API URL missing for authenticated download");
473
+ for (const credential of credentialFallbacks(env, execFile)) {
474
+ const authed = await fetchImpl(
475
+ asset.apiUrl,
476
+ await withProxyDispatcher(
477
+ {
478
+ headers: { Accept: "application/octet-stream", Authorization: `Bearer ${credential.token}`, "User-Agent": "@seanyao/roll" },
479
+ redirect: "follow",
480
+ signal: AbortSignal.timeout(timeoutMs)
481
+ },
482
+ env,
483
+ createProxyDispatcher
484
+ )
485
+ );
486
+ if (authed.ok) return new Uint8Array(await authed.arrayBuffer());
487
+ if (!isAuthRetryableStatus(authed.status)) throw new Error(`asset download returned ${authed.status}`);
488
+ }
489
+ throw new Error(`asset download returned ${response.status}`);
490
+ }
491
+ function parseRelease(value) {
492
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("release JSON is not an object");
493
+ const row = value;
494
+ const tagName = typeof row["tag_name"] === "string" ? row["tag_name"] : "";
495
+ const rawAssets = Array.isArray(row["assets"]) ? row["assets"] : [];
496
+ const assets = rawAssets.flatMap((raw) => {
497
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return [];
498
+ const asset = raw;
499
+ const name = typeof asset["name"] === "string" ? asset["name"] : "";
500
+ const apiUrl = typeof asset["url"] === "string" ? asset["url"] : "";
501
+ const browserDownloadUrl = typeof asset["browser_download_url"] === "string" ? asset["browser_download_url"] : "";
502
+ const size = typeof asset["size"] === "number" ? asset["size"] : Number.NaN;
503
+ return name !== "" && browserDownloadUrl !== "" && Number.isFinite(size) ? [{ name, browserDownloadUrl, size, ...apiUrl !== "" ? { apiUrl } : {} }] : [];
504
+ });
505
+ return { tagName, assets };
506
+ }
507
+ function isAuthRetryableStatus(status) {
508
+ return status === 401 || status === 404;
509
+ }
510
+ function* credentialFallbacks(env, execFile) {
511
+ const seen = /* @__PURE__ */ new Set();
512
+ for (const key of ["GITHUB_TOKEN", "GH_TOKEN"]) {
513
+ const token = (env[key] ?? "").trim();
514
+ if (token !== "" && !seen.has(token)) {
515
+ seen.add(token);
516
+ yield { token };
517
+ }
518
+ }
519
+ const gh = ghAuthToken(execFile);
520
+ if (gh !== null && !seen.has(gh)) yield { token: gh };
521
+ }
522
+ function ghAuthToken(execFile) {
523
+ try {
524
+ const result = execFile("gh", ["auth", "token"], { timeoutMs: GH_AUTH_TOKEN_TIMEOUT_MS });
525
+ const token = result.code === 0 ? result.stdout.trim() : "";
526
+ return isUsableGhAuthToken(token) ? token : null;
527
+ } catch {
528
+ return null;
529
+ }
530
+ }
531
+ function isUsableGhAuthToken(token) {
532
+ return token.length >= 20 && token.length <= 512 && !/\s/.test(token);
533
+ }
534
+ async function extractZip(zipPath, destination, execFile, timeoutMs) {
535
+ const ditto = execFile("ditto", ["-x", "-k", zipPath, destination], { timeoutMs });
536
+ if (ditto.code === 0) return { ok: true };
537
+ const unzip = execFile("unzip", ["-q", zipPath, "-d", destination], { timeoutMs });
538
+ if (unzip.code === 0) return { ok: true };
539
+ const detail = [ditto.stderr.trim(), unzip.stderr.trim()].filter((line) => line !== "").join("; ");
540
+ return { ok: false, detail: detail === "" ? "zip extraction failed" : detail };
541
+ }
542
+ function installGate(deps) {
543
+ if (deps.platform !== "darwin") return { status: "skipped", reason: "non-darwin" };
544
+ if (skipInstallEnv(deps.env)) return { status: "skipped", reason: "disabled" };
545
+ if (deps.uid === 0 || (deps.env["SUDO_USER"] ?? "").trim() !== "") {
546
+ return { status: "skipped", reason: ROOT_INSTALL_SKIP_REASON };
547
+ }
548
+ if (isCi(deps.env)) return { status: "skipped", reason: "ci" };
549
+ if (deps.hasAquaGUI === false) return { status: "skipped", reason: "headless" };
550
+ return null;
551
+ }
552
+ function skipInstallEnv(env) {
553
+ return env["ROLL_SKIP_CAPTURE_INSTALL"] === "1" || env["npm_config_roll_skip_capture_install"] === "1" || env["ROLL_NO_SCREENCAP"] === "1";
554
+ }
555
+ function isCi(env) {
556
+ return (env["CI"] ?? "").trim() !== "" || (env["GITHUB_ACTIONS"] ?? "").trim() !== "" || (env["GITLAB_CI"] ?? "").trim() !== "" || (env["JENKINS_HOME"] ?? "").trim() !== "";
557
+ }
558
+ function macosHasAquaGUI(execFile) {
559
+ const r = execFile("launchctl", ["managername"]);
560
+ return r.code === 0 && r.stdout.includes("Aqua");
561
+ }
562
+ function findExtractedApp(dir) {
563
+ try {
564
+ for (const name of readdirSync(dir)) {
565
+ const candidate = join2(dir, name);
566
+ const st = lstatSync(candidate);
567
+ if (st.isSymbolicLink()) continue;
568
+ if (st.isDirectory() && name === APP_NAME2) return candidate;
569
+ if (st.isDirectory()) {
570
+ const nested = findExtractedApp(candidate);
571
+ if (nested !== null) return nested;
572
+ }
573
+ }
574
+ } catch {
575
+ return null;
576
+ }
577
+ return null;
578
+ }
579
+ function findBundleExecutable(appPath) {
580
+ const macosDir = join2(appPath, "Contents", "MacOS");
581
+ try {
582
+ for (const name of readdirSync(macosDir)) {
583
+ const candidate = join2(macosDir, name);
584
+ const st = lstatSync(candidate);
585
+ if (st.isSymbolicLink()) continue;
586
+ if (st.isFile()) return candidate;
587
+ }
588
+ } catch {
589
+ return null;
590
+ }
591
+ return null;
592
+ }
593
+ function replaceAppAtomically(app, targetApp, deps) {
594
+ const renamePath = deps.renamePath ?? renameSync2;
595
+ const backupApp = `${targetApp}.bak`;
596
+ const hadExisting = lstatExists(targetApp);
597
+ rmSync2(backupApp, { recursive: true, force: true });
598
+ if (!hadExisting) {
599
+ renamePath(app, targetApp);
600
+ return;
601
+ }
602
+ let backupCreated = false;
603
+ let targetReplaced = false;
604
+ try {
605
+ renamePath(targetApp, backupApp);
606
+ backupCreated = true;
607
+ renamePath(app, targetApp);
608
+ targetReplaced = true;
609
+ rmSync2(backupApp, { recursive: true, force: true });
610
+ } catch (error) {
611
+ if (targetReplaced) rmSync2(targetApp, { recursive: true, force: true });
612
+ if (backupCreated) {
613
+ try {
614
+ renamePath(backupApp, targetApp);
615
+ } catch {
616
+ }
617
+ }
618
+ throw error;
619
+ }
620
+ }
621
+ function lstatExists(path) {
622
+ try {
623
+ lstatSync(path);
624
+ return true;
625
+ } catch {
626
+ return false;
627
+ }
628
+ }
629
+ async function withProxyDispatcher(init, env, createProxyDispatcher) {
630
+ if (!hasProxyEnv(env)) return init;
631
+ const dispatcher = await createProxyDispatcher(env);
632
+ return dispatcher === void 0 || dispatcher === null ? init : { ...init, dispatcher };
633
+ }
634
+ function hasProxyEnv(env) {
635
+ return ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"].some((key) => (env[key] ?? "").trim() !== "");
636
+ }
637
+ async function defaultProxyDispatcher(_env) {
638
+ try {
639
+ const imported = await optionalImport("undici");
640
+ if (typeof imported.EnvHttpProxyAgent === "function") return new imported.EnvHttpProxyAgent();
641
+ enableNodeGlobalProxyFromEnv(_env);
642
+ return void 0;
643
+ } catch {
644
+ enableNodeGlobalProxyFromEnv(_env);
645
+ return void 0;
646
+ }
647
+ }
648
+ var optionalImport = new Function("specifier", "return import(specifier)");
649
+ function enableNodeGlobalProxyFromEnv(env) {
650
+ const getBuiltinModule = process.getBuiltinModule;
651
+ const http = getBuiltinModule?.("node:http") ?? getBuiltinModule?.("http");
652
+ const setGlobalProxyFromEnv = isRecord(http) ? http["setGlobalProxyFromEnv"] : void 0;
653
+ if (typeof setGlobalProxyFromEnv !== "function") return;
654
+ try {
655
+ setGlobalProxyFromEnv(env);
656
+ } catch {
657
+ }
658
+ }
659
+ function readInstalledVersion(appPath, deps) {
660
+ const plistPath = join2(appPath, "Contents", "Info.plist");
661
+ const plutil = deps.execFile("plutil", ["-extract", "CFBundleShortVersionString", "raw", "-o", "-", plistPath]);
662
+ const raw = plutil.code === 0 ? plutil.stdout.trim() : "";
663
+ if (raw !== "") return raw;
664
+ try {
665
+ const text = readFileSync2(plistPath, "utf8");
666
+ const match = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]+)<\/string>/.exec(text);
667
+ return match?.[1];
668
+ } catch {
669
+ return void 0;
670
+ }
671
+ }
672
+ async function latestReleaseHint(deps, installedVersion) {
673
+ try {
674
+ const release = await deps.fetchLatestRelease(UPDATE_HINT_TIMEOUT_MS);
675
+ const latest = normalizeVersion(release.tagName);
676
+ const installed = normalizeVersion(installedVersion ?? "");
677
+ return {
678
+ releaseTag: release.tagName,
679
+ updateAvailable: latest !== "" && installed !== "" && latest !== installed
680
+ };
681
+ } catch {
682
+ return {};
683
+ }
684
+ }
685
+ function normalizeVersion(value) {
686
+ return value.trim().replace(/^v/i, "");
687
+ }
688
+ function manual(reason, releaseTag) {
689
+ return { status: "manual", reason, ...releaseTag !== void 0 ? { releaseTag } : {} };
690
+ }
691
+ function renderEn(result) {
692
+ if (result.status === "installed") {
693
+ return `Roll Capture.app installed at ${result.appPath ?? join2("~", "Applications", APP_NAME2)}; open it once and grant Screen Recording permission.`;
694
+ }
695
+ if (result.status === "already-installed") {
696
+ const update = result.updateAvailable === true && result.releaseTag !== void 0 ? `; newer release ${result.releaseTag} is available` : "";
697
+ return `Roll Capture.app already installed at ${result.appPath ?? "(detected)"}${update}.`;
698
+ }
699
+ if (result.status === "skipped") return `Roll Capture.app install skipped (${result.reason}).`;
700
+ return `Roll Capture.app automatic install failed (${result.reason}); install it manually, then open it once and grant Screen Recording permission.`;
701
+ }
702
+ function renderZh(result) {
703
+ if (result.status === "installed") {
704
+ return `Roll Capture.app \u5DF2\u5B89\u88C5\u5230 ${result.appPath ?? join2("~", "Applications", APP_NAME2)}\uFF1B\u8BF7\u6253\u5F00\u4E00\u6B21\u5E76\u6388\u4E88\u5C4F\u5E55\u5F55\u5236\u6743\u9650\u3002`;
705
+ }
706
+ if (result.status === "already-installed") {
707
+ const update = result.updateAvailable === true && result.releaseTag !== void 0 ? `\uFF1B\u6709\u65B0\u7248 ${result.releaseTag} \u53EF\u7528` : "";
708
+ return `Roll Capture.app \u5DF2\u5B89\u88C5\uFF1A${result.appPath ?? "\u5DF2\u68C0\u6D4B\u5230"}${update}\u3002`;
709
+ }
710
+ if (result.status === "skipped") return `Roll Capture.app \u5B89\u88C5\u5DF2\u8DF3\u8FC7\uFF08${renderZhReason(result.reason)}\uFF09\u3002`;
711
+ return `Roll Capture.app \u81EA\u52A8\u5B89\u88C5\u5931\u8D25\uFF08${result.reason}\uFF09\uFF1B\u8BF7\u624B\u52A8\u5B89\u88C5\uFF0C\u6253\u5F00\u4E00\u6B21\u5E76\u6388\u4E88\u5C4F\u5E55\u5F55\u5236\u6743\u9650\u3002`;
712
+ }
713
+ function renderZhReason(reason) {
714
+ if (reason === ROOT_INSTALL_SKIP_REASON) return "\u8BF7\u4EE5\u666E\u901A\u7528\u6237\u8FD0\u884C roll setup \u5B89\u88C5";
715
+ return reason;
716
+ }
717
+ function errorMessage(error) {
718
+ return error instanceof Error ? error.message : String(error);
719
+ }
720
+ function isRecord(value) {
721
+ return typeof value === "object" && value !== null && !Array.isArray(value);
722
+ }
723
+ function externalPlatformOverride(raw) {
724
+ if (raw === "aix" || raw === "android" || raw === "darwin" || raw === "freebsd" || raw === "haiku" || raw === "linux" || raw === "openbsd" || raw === "sunos" || raw === "win32" || raw === "cygwin" || raw === "netbsd") {
725
+ return raw;
726
+ }
727
+ return void 0;
728
+ }
729
+
730
+ // packages/cli/bin/postinstall.js
731
+ await runRollCapturePostinstall();