Keep using Photos.app like you always do. Attic quietly backs up your originals and edits to an S3 bucket you control. One-way, append-only.
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

Detect permission errors via exit code 77, bump base timeout to 10 min

- Check ladder exit code 77 instead of string-matching stderr for
permission errors — more robust across message text changes
- Increase base subprocess timeout from 5 to 10 minutes to match
ladder's per-asset AppleScript timeout, preventing attic from
killing ladder while iCloud downloads are still in progress

+11 -8
+5 -5
cli/src/export/exporter-timeout.test.ts
··· 6 6 } from "./exporter.ts"; 7 7 8 8 Deno.test("timeoutForBytes: scales with size", () => { 9 - // Small batch: base timeout (5 min) + 1 min for < 100 MB 10 - assertEquals(timeoutForBytes(50 * 1024 * 1024), 5 * 60_000 + 60_000); 9 + // Small batch: base timeout (10 min) + 1 min for < 100 MB 10 + assertEquals(timeoutForBytes(50 * 1024 * 1024), 10 * 60_000 + 60_000); 11 11 // 500 MB batch: base + 5 min 12 - assertEquals(timeoutForBytes(500 * 1024 * 1024), 5 * 60_000 + 5 * 60_000); 12 + assertEquals(timeoutForBytes(500 * 1024 * 1024), 10 * 60_000 + 5 * 60_000); 13 13 // 0 bytes: just base 14 - assertEquals(timeoutForBytes(0), 5 * 60_000); 14 + assertEquals(timeoutForBytes(0), 10 * 60_000); 15 15 // Negative bytes: treated as 0 16 - assertEquals(timeoutForBytes(-100), 5 * 60_000); 16 + assertEquals(timeoutForBytes(-100), 10 * 60_000); 17 17 }); 18 18 19 19 Deno.test("isTimeoutError: detects LadderTimeoutError", () => {
+6 -3
cli/src/export/exporter.ts
··· 130 130 return slashIndex === -1 ? id : id.substring(0, slashIndex); 131 131 } 132 132 133 - /** Base timeout for the ladder subprocess (5 minutes). */ 134 - const LADDER_BASE_TIMEOUT_MS = 5 * 60 * 1000; 133 + /** Base timeout for the ladder subprocess (10 minutes). 134 + * Matches ladder's per-asset AppleScript timeout so iCloud downloads 135 + * don't get killed while still in progress. */ 136 + const LADDER_BASE_TIMEOUT_MS = 10 * 60 * 1000; 135 137 136 138 /** Extra timeout per 100 MB of estimated batch size (~1 min per 100 MB). */ 137 139 const TIMEOUT_PER_100MB_MS = 60 * 1000; ··· 183 185 184 186 if (result.code !== 0) { 185 187 const err = new TextDecoder().decode(result.stderr).trim(); 186 - if (err.includes("Automation permission")) { 188 + // Exit code 77 = permission error (ladder convention) 189 + if (result.code === 77) { 187 190 throw new LadderPermissionError( 188 191 err.replace(/^ladder:\s*/, ""), 189 192 );