viewable-one 0.0.0-stage → 0.12.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.
Files changed (89) hide show
  1. package/CHANGELOG.md +1460 -0
  2. package/LICENSE +21 -0
  3. package/README.md +755 -2
  4. package/SPAWN.md +2105 -0
  5. package/glytch.mjs +517 -0
  6. package/lib/acp.mjs +978 -0
  7. package/lib/admission.mjs +105 -0
  8. package/lib/agent-selection.mjs +123 -0
  9. package/lib/agents.mjs +913 -0
  10. package/lib/atomic.mjs +96 -0
  11. package/lib/auth.mjs +1387 -0
  12. package/lib/authenticatedHttp.mjs +68 -0
  13. package/lib/boardside.mjs +225 -0
  14. package/lib/child.mjs +1252 -0
  15. package/lib/cli-output.mjs +176 -0
  16. package/lib/cloudflared-release.json +25 -0
  17. package/lib/cloudflared.mjs +81 -0
  18. package/lib/codex-rpc.mjs +92 -0
  19. package/lib/codex.mjs +747 -0
  20. package/lib/codexApps.mjs +332 -0
  21. package/lib/codexPlanWindow.mjs +84 -0
  22. package/lib/codexToolInputs.mjs +139 -0
  23. package/lib/codingChecks.mjs +55 -0
  24. package/lib/compose.mjs +76 -0
  25. package/lib/computerApps.mjs +110 -0
  26. package/lib/conversationRules.mjs +45 -0
  27. package/lib/detect.mjs +605 -0
  28. package/lib/devserver.mjs +659 -0
  29. package/lib/digest.mjs +290 -0
  30. package/lib/doctor.mjs +464 -0
  31. package/lib/exitClass.mjs +64 -0
  32. package/lib/glytchFolder.mjs +105 -0
  33. package/lib/hosted.mjs +112 -0
  34. package/lib/imageAttachments.mjs +31 -0
  35. package/lib/init-connect.mjs +418 -0
  36. package/lib/kill.mjs +104 -0
  37. package/lib/logindaemon.mjs +976 -0
  38. package/lib/logs.mjs +120 -0
  39. package/lib/mcpjson.mjs +242 -0
  40. package/lib/memberPreview.mjs +165 -0
  41. package/lib/multiwatch.mjs +428 -0
  42. package/lib/pagePictures.mjs +396 -0
  43. package/lib/pageRoutes.mjs +67 -0
  44. package/lib/paths.mjs +9 -0
  45. package/lib/permissionAsk.mjs +418 -0
  46. package/lib/picker.mjs +300 -0
  47. package/lib/platform.mjs +60 -0
  48. package/lib/previewGateway.mjs +129 -0
  49. package/lib/previewTunnel.mjs +85 -0
  50. package/lib/procs.mjs +428 -0
  51. package/lib/project-mcp.mjs +288 -0
  52. package/lib/projectPaths.mjs +31 -0
  53. package/lib/refusal.mjs +70 -0
  54. package/lib/registry.mjs +204 -0
  55. package/lib/repomap.mjs +322 -0
  56. package/lib/restartPass.mjs +151 -0
  57. package/lib/retired-address.mjs +78 -0
  58. package/lib/rowsAnswers.mjs +389 -0
  59. package/lib/rowsEvents.mjs +86 -0
  60. package/lib/run-engine.mjs +16 -0
  61. package/lib/runner-health.mjs +57 -0
  62. package/lib/runner.mjs +1569 -0
  63. package/lib/runnerAdmission.mjs +35 -0
  64. package/lib/runnerApi.mjs +312 -0
  65. package/lib/runnerCompletion.mjs +406 -0
  66. package/lib/runnerLease.mjs +37 -0
  67. package/lib/runnerMaintenance.mjs +27 -0
  68. package/lib/runnerPace.mjs +283 -0
  69. package/lib/runnerPerformance.mjs +26 -0
  70. package/lib/runnerRealtime.mjs +340 -0
  71. package/lib/runnerRegister.mjs +96 -0
  72. package/lib/runnerSettings.mjs +40 -0
  73. package/lib/runnerSettlement.mjs +138 -0
  74. package/lib/runnerWorkspace.mjs +162 -0
  75. package/lib/serverSweep.mjs +90 -0
  76. package/lib/sharedPreview.mjs +292 -0
  77. package/lib/signIn.mjs +47 -0
  78. package/lib/spawn.mjs +330 -0
  79. package/lib/sweep.mjs +161 -0
  80. package/lib/sync.mjs +484 -0
  81. package/lib/uninstall-project.mjs +588 -0
  82. package/lib/uninstall.mjs +457 -0
  83. package/lib/update.mjs +302 -0
  84. package/lib/usageLimit.mjs +108 -0
  85. package/lib/windows-command.mjs +85 -0
  86. package/lib/worktree.mjs +1138 -0
  87. package/lib/writePolicy.mjs +45 -0
  88. package/package.json +58 -4
  89. package/spawn-runner.mjs +167 -0
package/lib/atomic.mjs ADDED
@@ -0,0 +1,96 @@
1
+ // WRITE-THEN-RENAME, for every file the bridge writes that somebody else
2
+ // polls.
3
+ //
4
+ // `writeFile` is not atomic: it truncates (or creates) the destination and
5
+ // then streams bytes into it, so there is a real window in which a reader
6
+ // sees an empty or half-written file. Nothing in this repo has a lock, and
7
+ // every reader here is a POLLER - the app on its 10s ingestion tick, the
8
+ // runner on its poll loop, a session's own `check_answer` - so that window
9
+ // is not theoretical, it is the thing polling is guaranteed to find
10
+ // eventually.
11
+ //
12
+ // It has already cost twice, on the OTHER side of the same contract:
13
+ //
14
+ // - `lib/ops.mjs` readReceipt returns null rather than `{ok: false}` while
15
+ // the app's own non-atomic `writeFolderFile` is still in flight, because
16
+ // a poll landing in that window read "the app refused startSession",
17
+ // killed the request, and spawned nothing (2026-08-26). That guard reads
18
+ // around the app's window; this file removes the bridge's own.
19
+ // - a spawn request read mid-write parsed as invalid JSON and was CLAIMED
20
+ // with an ERROR receipt, and a claim is terminal - so a human click
21
+ // never ran.
22
+ //
23
+ // The temp file is a sibling (same directory, so the rename cannot cross a
24
+ // filesystem) and carries the pid plus a counter, so two processes writing
25
+ // the same destination never share a scratch name.
26
+
27
+ import { link, rename, writeFile, rm } from "node:fs/promises";
28
+ import { dirname, join, basename } from "node:path";
29
+
30
+ let seq = 0;
31
+
32
+ /** The rename failures that are worth waiting out rather than reporting.
33
+ *
34
+ * On Windows file locking is MANDATORY, not advisory: a rename over a
35
+ * destination another process has open fails EPERM or EBUSY, and every reader
36
+ * on the other side of this contract is a poller that opens these files
37
+ * constantly - plus the antivirus scanner and the search indexer, which open
38
+ * a file the instant it is created. The same rename a moment later succeeds.
39
+ * On a Mac neither code is ever produced here, so the loop below is dead code
40
+ * there and every existing test sees exactly what it saw before. Brief 21,
41
+ * session 3. */
42
+ const RETRYABLE_RENAME = new Set(["EPERM", "EBUSY", "EACCES"]);
43
+
44
+ /** How many times the rename is retried, and how long it waits between tries:
45
+ * 20ms, 40, 80, 120, 160 - about 420ms of patience in total, bounded so a
46
+ * genuinely locked file still fails rather than hanging a poll loop. */
47
+ export const RENAME_RETRIES = 5;
48
+ const backoffMs = (attempt) => Math.min(20 * (attempt + 1), 160);
49
+
50
+ /** Write `data` to `path` so a concurrent reader sees either the old file or
51
+ * the new one, never a partial. Returns nothing; throws what writeFile throws.
52
+ *
53
+ * The temp file is removed on failure, so a failed write never leaves a
54
+ * `.tmp-` sibling behind for the next reader to wonder about.
55
+ *
56
+ * `rename` and `sleep` are injected for the retry test, which needs a rename
57
+ * that fails EBUSY twice and no real half-second of waiting. */
58
+ export async function writeFileAtomic(
59
+ path,
60
+ data,
61
+ encoding = "utf8",
62
+ { rename: renameImpl = rename, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), retries = RENAME_RETRIES, mode } = {},
63
+ ) {
64
+ seq += 1;
65
+ const tmp = join(dirname(path), `.${basename(path)}.tmp-${process.pid}-${seq}`);
66
+ try {
67
+ await writeFile(tmp, data, mode === undefined ? encoding : { encoding, mode });
68
+ for (let attempt = 0; ; attempt += 1) {
69
+ try {
70
+ await renameImpl(tmp, path);
71
+ break;
72
+ } catch (error) {
73
+ if (attempt >= retries || !RETRYABLE_RENAME.has(error?.code)) throw error;
74
+ await sleep(backoffMs(attempt));
75
+ }
76
+ }
77
+ } catch (error) {
78
+ await rm(tmp, { force: true }).catch(() => {});
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ /** Publish a complete file only if the destination does not exist. The link
84
+ * reserves the destination atomically across processes; a crash before it
85
+ * leaves only an ignored temp file, and a crash after it leaves the full file.
86
+ * EEXIST means another publisher already committed this identity. */
87
+ export async function writeFileAtomicExclusive(path, data, encoding = "utf8") {
88
+ seq += 1;
89
+ const tmp = join(dirname(path), `.${basename(path)}.tmp-${process.pid}-${seq}`);
90
+ try {
91
+ await writeFile(tmp, data, encoding);
92
+ await link(tmp, path);
93
+ } finally {
94
+ await rm(tmp, { force: true }).catch(() => {});
95
+ }
96
+ }