heyquilt 0.3.122 → 0.3.138

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 (136) hide show
  1. package/RELEASES.md +273 -17
  2. package/bin/quilt.js +55 -9
  3. package/desktop/main.js +16 -2
  4. package/package.json +3 -2
  5. package/src/agent-task-workflow.js +2 -2
  6. package/src/agents/common.js +13 -0
  7. package/src/ai-vm.js +9 -1
  8. package/src/api/activity.js +5 -0
  9. package/src/api/memory-store.js +16 -9
  10. package/src/api/notify.js +134 -0
  11. package/src/api/routes/browsers.js +29 -0
  12. package/src/api/routes/github.js +0 -13
  13. package/src/api/routes/relay.js +26 -3
  14. package/src/api/routes/session-migrate.js +7 -4
  15. package/src/api/routes/sessions.js +73 -1
  16. package/src/api/server.js +13 -7
  17. package/src/api/supabase-store.js +43 -4
  18. package/src/autowork.js +182 -16
  19. package/src/connection.js +36 -9
  20. package/src/control.js +129 -15
  21. package/src/force-date.js +27 -0
  22. package/src/fsutil.js +13 -3
  23. package/src/gitstate.js +38 -2
  24. package/src/history.js +13 -3
  25. package/src/mcp.js +87 -12
  26. package/src/merge3.js +11 -3
  27. package/src/now-summary.js +113 -0
  28. package/src/pathrules.js +12 -0
  29. package/src/protocol.js +10 -2
  30. package/src/quilt-default.js +84 -0
  31. package/src/quilt-history.js +106 -0
  32. package/src/quilt-remote.js +78 -0
  33. package/src/relay-commit.js +5 -0
  34. package/src/relay-lease.js +6 -2
  35. package/src/relay-mcp.js +151 -17
  36. package/src/relay-outbound.js +1 -1
  37. package/src/release-words.js +2 -2
  38. package/src/runner.js +20 -7
  39. package/src/seam-access.js +97 -0
  40. package/src/seam-claims.js +53 -0
  41. package/src/seam-url.js +15 -0
  42. package/src/server.js +1517 -86
  43. package/src/session-follow.js +461 -27
  44. package/src/session-layout3.js +410 -4
  45. package/src/session-migrate.js +15 -11
  46. package/src/session-quilt.js +1 -1
  47. package/src/session-seam.js +233 -20
  48. package/src/session.js +848 -67
  49. package/src/source-live/bring-in.js +283 -0
  50. package/src/source-live/live-block.js +504 -33
  51. package/src/source-live/live-square.js +16 -5
  52. package/src/status.js +5 -0
  53. package/src/ui/agent-guide.js +1 -1
  54. package/src/ui/app.css +85 -20
  55. package/src/ui/app.js +4 -1
  56. package/src/ui/chat.js +9 -3
  57. package/src/ui/cloud-ai-model.js +13 -0
  58. package/src/ui/cloud-ai.js +10 -2
  59. package/src/ui/common.js +6 -2
  60. package/src/ui/fileview.js +20 -7
  61. package/src/ui/force-words.js +19 -0
  62. package/src/ui/git-rule.js +9 -0
  63. package/src/ui/home.js +0 -147
  64. package/src/ui/loom-model.js +15 -1
  65. package/src/ui/quiltview.js +213 -0
  66. package/src/ui/seam-model.js +251 -0
  67. package/src/ui/session.js +413 -31
  68. package/src/ui/voice-call.js +83 -14
  69. package/src/ui/voice-model.js +39 -0
  70. package/src/ui/voice.js +167 -17
  71. package/src/ui-server.js +80 -28
  72. package/src/update-check.js +25 -1
  73. package/src/voice.js +13 -6
  74. package/src/git.js +0 -147
  75. package/src/quilts/AGENTS.md +0 -1
  76. package/src/quilts/CLAUDE.md +0 -1
  77. package/src/quilts/accounts.js +0 -136
  78. package/src/quilts/affected.js +0 -132
  79. package/src/quilts/archiver.js +0 -175
  80. package/src/quilts/autorevert.js +0 -49
  81. package/src/quilts/bisect-run.js +0 -31
  82. package/src/quilts/blame.js +0 -103
  83. package/src/quilts/block.js +0 -347
  84. package/src/quilts/bucket.js +0 -25
  85. package/src/quilts/cbor.js +0 -92
  86. package/src/quilts/checks.js +0 -308
  87. package/src/quilts/claims.js +0 -144
  88. package/src/quilts/cli.js +0 -536
  89. package/src/quilts/client.js +0 -137
  90. package/src/quilts/conflict-format.js +0 -190
  91. package/src/quilts/describe.js +0 -296
  92. package/src/quilts/dogfood.js +0 -82
  93. package/src/quilts/errors.js +0 -8
  94. package/src/quilts/events.js +0 -171
  95. package/src/quilts/export-git.js +0 -130
  96. package/src/quilts/export-remote.js +0 -156
  97. package/src/quilts/export.js +0 -81
  98. package/src/quilts/fastmerge.js +0 -41
  99. package/src/quilts/hash.js +0 -44
  100. package/src/quilts/history.js +0 -266
  101. package/src/quilts/ids.js +0 -12
  102. package/src/quilts/import-follow.js +0 -236
  103. package/src/quilts/import-git.js +0 -294
  104. package/src/quilts/import.js +0 -196
  105. package/src/quilts/index.js +0 -94
  106. package/src/quilts/keys.js +0 -186
  107. package/src/quilts/limits.js +0 -67
  108. package/src/quilts/lines.js +0 -49
  109. package/src/quilts/live.js +0 -50
  110. package/src/quilts/merge-smart.js +0 -114
  111. package/src/quilts/merge.js +0 -501
  112. package/src/quilts/oauth.js +0 -202
  113. package/src/quilts/objects.js +0 -144
  114. package/src/quilts/pack.js +0 -118
  115. package/src/quilts/records.js +0 -211
  116. package/src/quilts/resolutions.js +0 -69
  117. package/src/quilts/reviews.js +0 -208
  118. package/src/quilts/routes.js +0 -186
  119. package/src/quilts/runner.js +0 -95
  120. package/src/quilts/server.js +0 -144
  121. package/src/quilts/service.js +0 -963
  122. package/src/quilts/setup.js +0 -41
  123. package/src/quilts/signing.js +0 -149
  124. package/src/quilts/squares.js +0 -239
  125. package/src/quilts/store-memory.js +0 -844
  126. package/src/quilts/store-rpc.js +0 -90
  127. package/src/quilts/store-supabase.js +0 -13
  128. package/src/quilts/tags.js +0 -64
  129. package/src/quilts/textdiff.js +0 -96
  130. package/src/quilts/tokens.js +0 -66
  131. package/src/quilts/tools.js +0 -403
  132. package/src/quilts/tree.js +0 -351
  133. package/src/quilts/webhook.js +0 -116
  134. package/src/quilts/window.js +0 -121
  135. package/src/quilts/xdiff.js +0 -435
  136. package/src/ui/branches.js +0 -111
@@ -1,186 +0,0 @@
1
- // Seam's HTTP door (plans/quilt-source-control/04): routes any server can mount. The host says
2
- // who is calling: identify(req) → { member_id, kind, name, tool?, on_behalf_of? }, or throws its
3
- // own refusal (Quilt's accounts API builds it from its sign-in, src/api/server.js). Every caller
4
- // reaches the same operations as the seam CLI and seam_* tools.
5
- import { QuiltError } from './service.js'
6
- import { classify, tokenKey } from './limits.js'
7
- import { scopeAllows } from './oauth.js'
8
-
9
- const Q = '/v1/quilts/([^/]+)'
10
- const MAX_JSON = 96 * 1024 * 1024
11
- const MAX_PACK = 65 * 1024 * 1024
12
-
13
- const query = (req) => new URL(req.url, 'http://x').searchParams
14
- const list = (v) => (v ? String(v).split(',').map((s) => s.trim()).filter(Boolean) : [])
15
-
16
- export function quiltRoutes ({ quilts, identify }) {
17
- if (!quilts) return []
18
- if (typeof identify !== 'function') throw new Error('quiltRoutes needs identify(req): who is calling')
19
-
20
- /** The caller as a quilt member: a quilt token (tokens.js, `Authorization: Bearer sqt_…`, a key for one quilt), else whoever the host says. */
21
- async function me (req) {
22
- const h = String(req.headers?.authorization || '')
23
- if (h.startsWith('Bearer sqt_')) {
24
- const t = await quilts.tokenCaller(h.slice(7).trim())
25
- if (!t) throw new QuiltError(401, 'bad_token', 'this quilt token is unknown or revoked')
26
- return t
27
- }
28
- return identify(req)
29
- }
30
-
31
- // An app's OAuth token (oauth.js) reaches /v1/me, the quilts list and the quilts its scopes name.
32
- const READ_POSTS = /\/(have|blobs)$/
33
- async function withinScope (m, req) {
34
- if (!m.scopes) return
35
- const p = new URL(req.url, 'http://x').pathname
36
- if (req.method === 'GET' && (p === '/v1/me' || p === '/v1/quilts')) return
37
- const q = new RegExp(`^${Q}(?:/|$)`).exec(p)
38
- const need = req.method === 'GET' || READ_POSTS.test(p) ? 'read' : 'write'
39
- if (q) {
40
- let ref
41
- try { ref = decodeURIComponent(q[1]) } catch { ref = q[1] }
42
- const quilt = await quilts.internals.quiltFor(m, ref, 'read')
43
- if (scopeAllows(m.scopes, quilt.id, need)) return
44
- }
45
- throw new QuiltError(403, 'insufficient_scope', `this app's access does not include ${need} here; ask for a wider scope`)
46
- }
47
- const inScope = (m, list) => m.scopes && !m.scopes.some((s) => s.startsWith('quilts:')) ? list.filter((q) => scopeAllows(m.scopes, q.id, 'read')) : list
48
-
49
- // Every answer is the server's record; a refusal carries its wire code (12's WriteResult).
50
- // Per-token rate limits (04, limits.js), when the quilts were made with them.
51
- const limiter = quilts.rateLimiter || null
52
- const run = (fn) => async (req, body, params) => {
53
- try {
54
- const m = await me(req)
55
- await withinScope(m, req)
56
- if (limiter) limiter.take(tokenKey(req, m), classify(req.method, req.url, body), m.kind === 'person')
57
- return await fn(m, body, params, req)
58
- } catch (err) {
59
- if (err instanceof QuiltError) return [err.status, { ok: false, error: err.message, code: err.code, ...err.extra }]
60
- if (err && /^(bad path|a path|.*: (put|bad|give)|commit message|changes must|too many files)/.test(String(err.message))) return [400, { ok: false, error: err.message, code: 'bad_request' }]
61
- throw err
62
- }
63
- }
64
- const big = { maxBody: MAX_JSON }
65
- // Seam's device flow, before a computer has a sign-in: no caller, a refusal as an answer.
66
- const open = (fn) => async (req, body) => {
67
- try { return await fn(body) } catch (err) {
68
- if (err instanceof QuiltError) return [err.status, { ok: false, error: err.message, code: err.code, ...err.extra }]
69
- throw err
70
- }
71
- }
72
-
73
- return [
74
- ['POST', /^\/v1\/imports$/, run(async (m, b) => [202, await quilts.startImport(m, b)])],
75
- ['GET', /^\/v1\/imports\/([^/]+)$/, run((m, b, [id]) => quilts.importStatus(m, decodeURIComponent(id)))],
76
- ['POST', /^\/v1\/imports\/([^/]+)\/cutover$/, run((m, b, [id]) => quilts.cutover(m, decodeURIComponent(id), b || {}))],
77
- // A poke carries the import's secret (Bearer sip_…) or a member's key.
78
- ['POST', /^\/v1\/imports\/([^/]+)\/poke$/, async (req, body, [id]) => {
79
- const h = String(req.headers?.authorization || '')
80
- if (!h.startsWith('Bearer sip_')) return run(async (m) => [202, await quilts.pokeImport(m, decodeURIComponent(id))])(req, body, [id])
81
- try { return [202, await quilts.pokeImport(null, decodeURIComponent(id), { secret: h.slice(7).trim() })] } catch (err) { if (err instanceof QuiltError) return [err.status, { ok: false, error: err.message, code: err.code }]; throw err }
82
- }],
83
- ['GET', /^\/v1\/me$/, run((m) => quilts.whoami(m))],
84
- ['POST', /^\/v1\/me\/signout$/, run((m) => quilts.signout(m))],
85
- ['POST', /^\/v1\/device\/start$/, open((b) => quilts.deviceStart(b))],
86
- ['POST', /^\/v1\/device\/poll$/, open((b) => quilts.devicePoll(b))],
87
- ['POST', /^\/v1\/device\/resume$/, open((b) => quilts.deviceResume(b))],
88
- ['POST', /^\/v1\/device\/approve$/, run((m, b) => quilts.deviceApprove(m, b))],
89
- ['GET', /^\/v1\/device\/([A-Za-z0-9-]+)$/, run((m, b, [code]) => quilts.deviceLinkInfo(m, code))],
90
- ['GET', /^\/v1\/agents$/, run((m) => quilts.agents(m))],
91
- ['POST', /^\/v1\/agents$/, run(async (m, b) => [201, await quilts.createAgent(m, b)])],
92
- ['POST', /^\/v1\/agents\/([^/]+)\/key$/, run((m, b, [id]) => quilts.refreshAgentKey(m, decodeURIComponent(id)))],
93
- ['DELETE', /^\/v1\/agents\/([^/]+)$/, run((m, b, [id]) => quilts.revokeAgent(m, decodeURIComponent(id)))],
94
- ['GET', /^\/v1\/oauth\/clients$/, run((m) => quilts.oauthClients(m))],
95
- ['POST', /^\/v1\/oauth\/clients$/, run(async (m, b) => [201, await quilts.createOAuthClient(m, b)])],
96
- ['DELETE', /^\/v1\/oauth\/clients\/([^/]+)$/, run((m, b, [id]) => quilts.revokeOAuthClient(m, decodeURIComponent(id)))],
97
- ['GET', /^\/v1\/oauth\/authorize$/, run((m, b, p, req) => quilts.oauthAuthorizeInfo(m, Object.fromEntries(query(req))))],
98
- ['POST', /^\/v1\/oauth\/authorize$/, run((m, b) => quilts.oauthAuthorize(m, b))],
99
- ['POST', /^\/oauth\/token$/, open((b) => quilts.oauthToken(b))],
100
- ['POST', /^\/oauth\/revoke$/, open((b) => quilts.oauthRevoke(b))],
101
- ['GET', /^\/v1\/quilts$/, run(async (m) => ({ quilts: inScope(m, await quilts.quilts(m)) }))],
102
- ['POST', /^\/v1\/quilts$/, run(async (m, b) => [201, await quilts.createQuilt(m, b)])],
103
- ['GET', new RegExp(`^${Q}$`), run((m, b, [q]) => quilts.quiltInfo(m, q))],
104
- ['POST', new RegExp(`^${Q}/members$`), run((m, b, [q]) => quilts.addMember(m, q, b))],
105
- ['GET', new RegExp(`^${Q}/tokens$`), run((m, b, [q]) => quilts.tokens(m, q))],
106
- ['POST', new RegExp(`^${Q}/tokens$`), run(async (m, b, [q]) => [201, await quilts.createToken(m, q, b)])],
107
- ['DELETE', new RegExp(`^${Q}/tokens/([^/]+)$`), run((m, b, [q, id]) => quilts.revokeToken(m, q, decodeURIComponent(id)))],
108
-
109
- ['GET', new RegExp(`^${Q}/squares$`), run(async (m, b, [q], req) => { const s = query(req); return { squares: await quilts.squares(m, q, { state: s.get('state') || undefined, mine: s.get('mine') === '1', task: s.get('task') || undefined }) } })],
110
- ['POST', new RegExp(`^${Q}/squares$`), run(async (m, b, [q]) => [201, await quilts.openSquare(m, q, b)])],
111
- ['GET', new RegExp(`^${Q}/squares/([^/]+)$`), run((m, b, [q, s]) => quilts.square(m, q, s))],
112
- ['POST', new RegExp(`^${Q}/squares/([^/]+)/commit$`), run(async (m, b, [q, s]) => [201, await quilts.commit(m, q, s, b)]), big],
113
- ['POST', new RegExp(`^${Q}/squares/([^/]+)/amend$`), run(async (m, b, [q, s]) => [201, await quilts.amend(m, q, s, b)]), big],
114
- ['GET', new RegExp(`^${Q}/resolutions$`), run((m, b, [q], req) => quilts.resolutions(m, q, { path: query(req).get('path') || undefined }))],
115
- ['DELETE', new RegExp(`^${Q}/resolutions/([^/]+)$`), run((m, b, [q, k]) => quilts.forgetResolution(m, q, decodeURIComponent(k)))],
116
- ['GET', new RegExp(`^${Q}/claims$`), run((m, b, [q], req) => quilts.claims(m, q, { square: query(req).get('square') || undefined }))],
117
- ['POST', new RegExp(`^${Q}/squares/([^/]+)/claims$`), run(async (m, b, [q, s]) => [201, await quilts.claim(m, q, s, b)])],
118
- ['DELETE', new RegExp(`^${Q}/squares/([^/]+)/claims/(.+)$`), run((m, b, [q, s, c]) => quilts.unclaim(m, q, s, decodeURIComponent(c)))],
119
- ['GET', new RegExp(`^${Q}/squares/([^/]+)/reviews$`), run((m, b, [q, s]) => quilts.reviews(m, q, s))],
120
- ['POST', new RegExp(`^${Q}/squares/([^/]+)/reviews$`), run(async (m, b, [q, s]) => [201, await quilts.review(m, q, s, b)])],
121
- ['POST', new RegExp(`^${Q}/squares/([^/]+)/checks$`), run((m, b, [q, s]) => quilts.reportSquareCheck(m, q, s, b))],
122
- ['POST', new RegExp(`^${Q}/squares/([^/]+)/checks/([^/]+)/rerun$`), run(async (m, b, [q, s, c]) => [201, await quilts.rerunCheck(m, q, { ...b, check: decodeURIComponent(c), square: s })])],
123
- ['POST', new RegExp(`^${Q}/lines/([^/]+)/checks/([^/]+)/rerun$`), run(async (m, b, [q, line, c]) => [201, await quilts.rerunCheck(m, q, { ...b, check: decodeURIComponent(c), line: decodeURIComponent(line) })])],
124
- ['GET', new RegExp(`^${Q}/lines/([^/]+)/policy$`), run((m, b, [q, line]) => quilts.linePolicy(m, q, decodeURIComponent(line)))],
125
- ['PUT', new RegExp(`^${Q}/lines/([^/]+)/policy$`), run((m, b, [q, line]) => quilts.setLinePolicy(m, q, decodeURIComponent(line), b))],
126
- ['GET', new RegExp(`^${Q}/squares/([^/]+)/live$`), run((m, b, [q, s]) => quilts.live(m, q, s))],
127
- ['PUT', new RegExp(`^${Q}/squares/([^/]+)/live$`), run((m, b, [q, s]) => quilts.setLive(m, q, s, b)), big],
128
- ['POST', new RegExp(`^${Q}/squares/([^/]+)/handoff$`), run((m, b, [q, s]) => quilts.handoff(m, q, s, b))],
129
- ['POST', new RegExp(`^${Q}/squares/([^/]+)/abandon$`), run((m, b, [q, s]) => quilts.abandon(m, q, s, b))],
130
- ['POST', new RegExp(`^${Q}/squares/([^/]+)/merged$`), run((m, b, [q, s]) => quilts.squareMerged(m, q, s, b))],
131
- ['POST', new RegExp(`^${Q}/squares/([^/]+)/reopen$`), run((m, b, [q, s]) => quilts.reopen(m, q, s, b))],
132
- ['POST', new RegExp(`^${Q}/squares/([^/]+)/merge$`), run(async (m, b, [q, s]) => { const r = await quilts.mergeSquare(m, q, s, b); return r.noop ? r : [201, r] })],
133
- ['POST', new RegExp(`^${Q}/squares/([^/]+)/fold$`), run((m, b, [q, s]) => quilts.fold(m, q, s, b))],
134
- ['POST', new RegExp(`^${Q}/squares/([^/]+)/split$`), run(async (m, b, [q, s]) => [201, await quilts.split(m, q, s, b)])],
135
- ['POST', new RegExp(`^${Q}/squares/([^/]+)/rebase$`), run((m, b, [q, s]) => quilts.rebase(m, q, s, b))],
136
- ['POST', new RegExp(`^${Q}/squares/([^/]+)/cherry-pick$`), run((m, b, [q, s]) => quilts.cherryPick(m, q, s, b))],
137
- ['POST', new RegExp(`^${Q}/squares/([^/]+)/resolve$`), run((m, b, [q, s]) => quilts.resolve(m, q, s, b))],
138
- ['GET', new RegExp(`^${Q}/lines$`), run((m, b, [q]) => quilts.lines(m, q))],
139
- ['POST', new RegExp(`^${Q}/lines$`), run(async (m, b, [q]) => [201, await quilts.newLine(m, q, b)])],
140
- ['POST', new RegExp(`^${Q}/lines/([^/]+)/freeze$`), run((m, b, [q, line]) => quilts.freezeLine(m, q, line))],
141
- ['POST', new RegExp(`^${Q}/lines/([^/]+)/unfreeze$`), run((m, b, [q, line]) => quilts.unfreezeLine(m, q, line))],
142
- ['PUT', new RegExp(`^${Q}/export$`), run((m, b, [q]) => quilts.setExport(m, q, b))],
143
- ['GET', new RegExp(`^${Q}/export$`), run((m, b, [q]) => quilts.exportStatus(m, q))],
144
- ['GET', new RegExp(`^${Q}/tags$`), run((m, b, [q]) => quilts.tags(m, q))],
145
- ['POST', new RegExp(`^${Q}/tags$`), run(async (m, b, [q]) => [201, await quilts.createTag(m, q, b)])],
146
- ['GET', new RegExp(`^${Q}/tags/(.+)$`), run((m, b, [q, name]) => quilts.getTag(m, q, decodeURIComponent(name)))],
147
- ['GET', new RegExp(`^${Q}/releases$`), run((m, b, [q]) => quilts.releases(m, q))],
148
- ['POST', new RegExp(`^${Q}/releases$`), run(async (m, b, [q]) => [201, await quilts.createRelease(m, q, b)])],
149
- ['POST', new RegExp(`^${Q}/lines/([^/]+)/merge$`), run(async (m, b, [q, line]) => { const r = await quilts.mergeIntoLine(m, q, line, b); return r.ok || r.dry_run ? [r.dry_run ? 200 : 201, r] : [422, { ...r, error: r.refused.detail, code: r.refused.code }] })],
150
- ['POST', new RegExp(`^${Q}/lines/([^/]+)/checks$`), run((m, b, [q, line]) => quilts.reportCheck(m, q, line, b))],
151
- ['GET', new RegExp(`^${Q}/lines/([^/]+)/window$`), run((m, b, [q, line]) => quilts.lineWindow(m, q, line))],
152
- ['GET', new RegExp(`^${Q}/lines/([^/]+)/patches$`), run((m, b, [q, line], req) => { const s = query(req); return quilts.linePatches(m, q, line, { before: s.get('before') || undefined, limit: s.get('limit') || 100 }) })],
153
- ['POST', new RegExp(`^${Q}/lines/([^/]+)/window$`), run((m, b, [q, line]) => quilts.setLineLimits(m, q, line, b))],
154
- ['POST', new RegExp(`^${Q}/lines/([^/]+)/checks$`), run((m, b, [q, line]) => quilts.reportCheck(m, q, line, b))],
155
- ['GET', new RegExp(`^${Q}/lines/([^/]+)/window$`), run((m, b, [q, line]) => quilts.lineWindow(m, q, line))],
156
- ['POST', new RegExp(`^${Q}/lines/([^/]+)/window$`), run((m, b, [q, line]) => quilts.setLineLimits(m, q, line, b))],
157
- ['POST', new RegExp(`^${Q}/patches/(\\d+)/revert$`), run(async (m, b, [q, n]) => [201, await quilts.revert(m, q, { ...b, rev: `${b.line || 'main'}:${n}` })])],
158
- ['POST', new RegExp(`^${Q}/revert$`), run(async (m, b, [q]) => [201, await quilts.revert(m, q, b)])],
159
-
160
- ['GET', new RegExp(`^${Q}/checks$`), run((m, b, [q], req) => { const s = query(req); return quilts.checkRuns(m, q, { state: s.get('state') || undefined, name: s.get('name') || undefined, tier: s.get('tier') || undefined, line: s.get('line') || undefined, square: s.get('square') || undefined, limit: s.get('limit') || 100 }) })],
161
- ['GET', new RegExp(`^${Q}/events$`), run((m, b, [q], req) => { const s = query(req); return quilts.events(m, q, { after: s.get('after') || 0, kinds: list(s.get('kinds')), limit: s.get('limit') || 100 }) })],
162
- ['POST', new RegExp(`^${Q}/webhook$`), run((m, b, [q]) => quilts.subscribe(m, q, b))],
163
- ['GET', new RegExp(`^${Q}/show/([^/]+)$`), run((m, b, [q, rev], req) => { const s = query(req); return quilts.show(m, q, rev, { format: s.get('format') || 'unified', paths: list(s.get('paths')) }) })],
164
- ['GET', new RegExp(`^${Q}/export/git$`), run((m, b, [q], req) => { const s = query(req); return quilts.exportPatches(m, q, { line: s.get('line') || 'main', after: s.has('after') ? Number(s.get('after')) : -1, limit: Number(s.get('limit')) || 200 }) })],
165
- ['GET', new RegExp(`^${Q}/log$`), run((m, b, [q], req) => { const s = query(req); return quilts.log(m, q, { rev: s.get('rev') || 'main', paths: list(s.get('paths')), limit: Number(s.get('limit')) || 20, follow: s.get('follow') === '1' || s.get('follow') === 'true', search: s.get('search') ?? undefined, show_signature: s.get('show_signature') === '1' || s.get('show_signature') === 'true' }) })],
166
- ['POST', new RegExp(`^${Q}/commits/([^/]+)/signatures$`), run(async (m, b, [q, rev]) => [201, await quilts.sign(m, q, decodeURIComponent(rev), b)])],
167
- ['GET', /^\/v1\/search$/, run((m, b, p, req) => { const s = query(req); const yes = (k) => s.get(k) === '1' || s.get(k) === 'true'; return quilts.search(m, { pattern: s.get('pattern'), rev: s.get('rev') || 'main', quilts: list(s.get('quilts')), paths: list(s.get('paths')), ignore_case: yes('ignore_case'), fixed: yes('fixed'), limit: Number(s.get('limit')) || 200 }) })],
168
- ['GET', /^\/v1\/signing-keys$/, run((m, b, p, req) => quilts.signingKeys(m, null, { member: query(req).get('member') || undefined }))],
169
- ['POST', /^\/v1\/signing-keys$/, run(async (m, b) => [201, await quilts.addKey(m, null, b)])],
170
- ['DELETE', /^\/v1\/signing-keys\/([^/]+)$/, run((m, b, [key]) => quilts.removeKey(m, null, decodeURIComponent(key)))],
171
- ['GET', new RegExp(`^${Q}/describe$`), run((m, b, [q], req) => quilts.describe(m, q, query(req).get('rev') || 'main'))],
172
- ['GET', new RegExp(`^${Q}/merge-base$`), run((m, b, [q], req) => { const s = query(req); return quilts.mergeBaseOf(m, q, s.get('a'), s.get('b')) })],
173
- ['GET', new RegExp(`^${Q}/grep$`), run((m, b, [q], req) => { const s = query(req); const yes = (k) => s.get(k) === '1' || s.get(k) === 'true'; return quilts.grep(m, q, { pattern: s.get('pattern'), rev: s.get('rev') || 'main', paths: list(s.get('paths')), ignore_case: yes('ignore_case'), fixed: yes('fixed'), limit: Number(s.get('limit')) || 200 }) })],
174
- ['POST', new RegExp(`^${Q}/bisect$`), run((m, b, [q]) => quilts.bisect(m, q, b || {}))],
175
- ['GET', new RegExp(`^${Q}/diff$`), run((m, b, [q], req) => { const s = query(req); return quilts.diff(m, q, { from: s.get('from') || undefined, to: s.get('to') || undefined, paths: list(s.get('paths')), format: s.get('format') || 'unified' }) })],
176
- ['GET', new RegExp(`^${Q}/tree/([^/]+)$`), run((m, b, [q, rev], req) => quilts.tree(m, q, rev, { paths: list(query(req).get('paths')) }))],
177
- ['GET', new RegExp(`^${Q}/blob/([^/]+)/(.+)$`), run((m, b, [q, rev, path], req) => quilts.cat(m, q, rev, path, { format: query(req).get('format') || 'diff' }))],
178
- ['GET', new RegExp(`^${Q}/blame/([^/]+)/(.+)$`), run((m, b, [q, rev, path]) => quilts.blame(m, q, rev, path))],
179
-
180
- ['POST', new RegExp(`^${Q}/have$`), run((m, b, [q]) => quilts.have(m, q, b.ids)), { maxBody: 2 * 1024 * 1024 }],
181
- ['POST', new RegExp(`^${Q}/blobs$`), run((m, b, [q]) => quilts.blobs(m, q, b.ids)), { maxBody: 2 * 1024 * 1024 }],
182
- ['POST', new RegExp(`^${Q}/packs$`), run(async (m, b, [q]) => [201, await quilts.uploadPack(m, q, b)]), { raw: true, maxBody: MAX_PACK }],
183
- ['POST', new RegExp(`^${Q}/import/commits$`), run((m, b, [q]) => quilts.importBatch(m, q, b)), big],
184
- ['POST', new RegExp(`^${Q}/import/known$`), run((m, b, [q]) => quilts.importKnown(m, q, b)), big]
185
- ]
186
- }
@@ -1,95 +0,0 @@
1
- // seam runner (plans/quilt-source-control/13 "Checks"): a ready-made check runner anyone starts on
2
- // their own machine. It polls the runs Seam asked its member to run (GET /checks?state=queued),
3
- // checks each run's commit out into a fresh block folder, runs the command there, and reports the
4
- // verdict (exit 0 green, anything else red) on the run's run_key with a link to the log. A run
5
- // Seam cancels (check.cancelled: a newer head superseded it) is stopped and not reported.
6
- import fs from 'node:fs'
7
- import os from 'node:os'
8
- import path from 'node:path'
9
- import { spawn } from 'node:child_process'
10
- import { checkout } from './block.js'
11
-
12
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
13
- const safe = (key) => key.replace(/[^\w.-]+/g, '_').slice(0, 180)
14
-
15
- /** The log link a verdict carries: --log-url with {run_key} and {file}, else the log file's own address. */
16
- export function logLink (template, runKey, file) {
17
- if (!template) return 'file://' + file
18
- return template.replaceAll('{run_key}', encodeURIComponent(runKey)).replaceAll('{file}', encodeURIComponent(path.basename(file)))
19
- }
20
-
21
- /** The environment a run's command gets: what to run and, when Seam knows, which tests the change reaches. */
22
- export function runEnv (run, base = process.env) {
23
- const a = run.affected || { confidence: 'all' }
24
- return {
25
- ...base,
26
- SEAM_CHECK: run.name, SEAM_TIER: run.tier, SEAM_COMMIT: run.commit, SEAM_RUN_KEY: run.run_key,
27
- SEAM_LINE: run.line || '', SEAM_SQUARE: run.square || '',
28
- SEAM_AFFECTED_CONFIDENCE: a.confidence, SEAM_AFFECTED_TESTS: a.confidence === 'all' ? '' : (a.tests || []).join('\n')
29
- }
30
- }
31
-
32
- /**
33
- * Runs checks for one quilt. { client, quilt, checks: ['ci'], run: 'npm test', dir (where block
34
- * folders and logs go), api, once (handle what is queued now, then return), intervalMs, logUrl,
35
- * say(line) }. Returns the runs it finished: [{ run_key, status | 'cancelled', url }].
36
- */
37
- export async function runChecks ({ client, quilt, checks, run: command, dir = path.join(os.tmpdir(), 'seam-runner'), api, once = false, intervalMs = 5000, logUrl, say = () => {}, keep = false }) {
38
- if (!checks?.length) throw new Error('seam runner needs --check <name>[,<name>]')
39
- if (!command) throw new Error('seam runner needs --run "<command>"')
40
- const logs = path.join(dir, 'logs')
41
- fs.mkdirSync(logs, { recursive: true })
42
- const done = []
43
- const seen = new Set()
44
- for (;;) {
45
- const queued = []
46
- for (const name of checks) queued.push(...(await client.checkRuns(quilt, { state: 'queued', name })).runs)
47
- const todo = queued.filter((r) => !seen.has(r.run_key)).sort((a, b) => a.at - b.at)
48
- for (const r of todo) {
49
- seen.add(r.run_key)
50
- done.push(await runOne(r))
51
- }
52
- if (once) return done
53
- await sleep(intervalMs)
54
- }
55
-
56
- async function runOne (r) {
57
- const work = fs.mkdtempSync(path.join(dir, 'run-'))
58
- const file = path.join(logs, safe(r.run_key) + '.log')
59
- const url = logLink(logUrl, r.run_key, file)
60
- say(`${r.name} (${r.tier}) on ${r.commit.slice(0, 12)}${r.square ? ` in ${r.square}` : ` at ${r.line}`}: running`)
61
- const out = fs.openSync(file, 'w')
62
- let status
63
- try {
64
- fs.writeSync(out, `seam runner: ${r.name} ${r.tier} ${r.commit} run_key ${r.run_key}\n$ ${command}\n`)
65
- await checkout(client, { quilt, rev: r.commit, dir: work, api })
66
- const child = spawn(command, { cwd: work, shell: true, env: runEnv(r), stdio: ['ignore', out, out], detached: process.platform !== 'win32' })
67
- const exited = new Promise((resolve) => child.on('close', (code) => resolve(code)))
68
- let cancelled = false
69
- for (;;) {
70
- const code = await Promise.race([exited, sleep(Math.min(intervalMs, 2000)).then(() => undefined)])
71
- if (code !== undefined) { status = code === 0 ? 'green' : 'red'; break }
72
- const gone = (await client.checkRuns(quilt, { state: 'cancelled', name: r.name, limit: 50 })).runs.some((x) => x.run_key === r.run_key)
73
- if (gone && !cancelled) {
74
- cancelled = true
75
- try { process.kill(process.platform !== 'win32' ? -child.pid : child.pid, 'SIGTERM') } catch { child.kill('SIGTERM') }
76
- }
77
- if (cancelled) { await exited; status = 'cancelled'; break }
78
- }
79
- } catch (err) {
80
- fs.writeSync(out, `\nseam runner: ${err.message}\n`)
81
- status = 'red'
82
- } finally {
83
- fs.writeSync(out, `\nseam runner: ${status}\n`)
84
- fs.closeSync(out)
85
- if (!keep) fs.rmSync(work, { recursive: true, force: true })
86
- }
87
- if (status !== 'cancelled') {
88
- const body = { check: r.name, status, run_key: r.run_key, url }
89
- if (r.tier === 'pre_merge' && r.square) await client.reportSquareCheck(quilt, r.square, { ...body, head: r.commit })
90
- else await client.reportCheck(quilt, r.line, body)
91
- }
92
- say(`${r.name} on ${r.commit.slice(0, 12)}: ${status} (log ${url})`)
93
- return { run_key: r.run_key, status, url }
94
- }
95
- }
@@ -1,144 +0,0 @@
1
- // Seam's own server: the routes in routes.js on an HTTP port, apart from any other product.
2
- // Who is calling comes from an identity service over HTTP (SEAM_IDENTITY_URL): Seam sends it the
3
- // caller's Authorization header and gets back the caller as a quilt member. Quilt's accounts API is
4
- // one such service (GET /v1/seam/me); any other host can be one by answering the same way.
5
- //
6
- // seam serve or node src/quilts/server.js
7
- // SEAM_AUTH_URL Seam's own accounts: its Supabase project's address; Seam checks the
8
- // project's access tokens itself (SEAM_AUTH_JWT_SECRET for a shared secret)
9
- // SEAM_IDENTITY_URL an identity service, for every other token: GET with the caller's
10
- // Authorization → 200 { member_id, kind, name, tool?, on_behalf_of? }, or 401
11
- // (one of the two is required)
12
- // SEAM_PORT / PORT port (default 8080); SEAM_HOST (default 0.0.0.0)
13
- // SEAM_STORE=supabase rows and packs in Seam's own Supabase project: SEAM_SUPABASE_URL,
14
- // SEAM_SUPABASE_KEY (service role), SEAM_BUCKET (default quilt-packs);
15
- // otherwise a local store in SEAM_DATA (default ./seam-data)
16
- import http from 'node:http'
17
- import crypto from 'node:crypto'
18
- import { fileURLToPath } from 'node:url'
19
- import { quiltRoutes } from './routes.js'
20
- import { QuiltError } from './errors.js'
21
- import { quiltsFromEnv } from './setup.js'
22
- import { supabaseVerifier, seamIdentity } from './accounts.js'
23
-
24
- const MAX_BODY = 1024 * 1024
25
- const KINDS = new Set(['person', 'agent'])
26
-
27
- /**
28
- * identify(req) from an identity service over HTTP. A caller it accepts is kept for `ttlMs`
29
- * (keyed by a hash of the Authorization header, never the header itself); a refusal is not kept.
30
- */
31
- export function remoteIdentity ({ url, fetch = globalThis.fetch, ttlMs = 30_000, now = Date.now, max = 10_000, timeoutMs = 5000 }) {
32
- if (!url) throw new Error('remoteIdentity needs the identity service url (SEAM_IDENTITY_URL)')
33
- const cache = new Map() // hash → { member, until }
34
- return async function identify (req) {
35
- const auth = String(req.headers?.authorization || '')
36
- if (!auth) throw new QuiltError(401, 'not_signed_in', 'sign in: send "Authorization: Bearer <token>"')
37
- const key = crypto.createHash('sha256').update(auth).digest('hex')
38
- const hit = cache.get(key)
39
- if (hit && hit.until > now()) return hit.member
40
- let res
41
- try {
42
- res = await fetch(url, { headers: { authorization: auth, accept: 'application/json' }, signal: AbortSignal.timeout(timeoutMs) })
43
- } catch {
44
- throw new QuiltError(503, 'identity_unavailable', 'Seam could not reach its sign-in service; try again')
45
- }
46
- const data = await res.json().catch(() => null)
47
- if (res.status === 401 || res.status === 403) throw new QuiltError(401, 'not_signed_in', (data && data.error) || 'this sign-in is unknown or expired')
48
- const m = res.ok && data && typeof data === 'object' ? data : null
49
- if (!m || typeof m.member_id !== 'string' || !m.member_id || !KINDS.has(m.kind)) throw new QuiltError(503, 'identity_unavailable', 'Seam\'s sign-in service gave an answer it cannot use; try again')
50
- const member = { member_id: m.member_id, kind: m.kind, name: String(m.name || m.kind) }
51
- if (m.tool) member.tool = String(m.tool)
52
- if (m.on_behalf_of) member.on_behalf_of = String(m.on_behalf_of)
53
- if (cache.size >= max) cache.delete(cache.keys().next().value)
54
- cache.set(key, { member, until: now() + ttlMs })
55
- return member
56
- }
57
- }
58
-
59
- function readBody (req, max, raw) {
60
- return new Promise((resolve, reject) => {
61
- let size = 0; const chunks = []
62
- req.on('data', (c) => { size += c.length; if (size > max) { req.destroy(); reject(new QuiltError(413, 'too_large', 'too large')) } else chunks.push(c) })
63
- req.on('end', () => {
64
- const buf = Buffer.concat(chunks)
65
- if (raw) return resolve(buf)
66
- if (!buf.length) return resolve({})
67
- if (/^application\/x-www-form-urlencoded/i.test(String(req.headers['content-type'] || ''))) return resolve(Object.fromEntries(new URLSearchParams(buf.toString('utf8'))))
68
- let body
69
- try { body = JSON.parse(buf.toString('utf8')) } catch { return reject(new QuiltError(400, 'bad_request', 'bad json')) }
70
- resolve(body && typeof body === 'object' && !Array.isArray(body) ? body : {})
71
- })
72
- req.on('error', reject)
73
- })
74
- }
75
-
76
- const decode = (s) => { try { return decodeURIComponent(s) } catch { throw new QuiltError(400, 'bad_request', 'bad path') } }
77
-
78
- /** Seam on a port: { server, url, close }. */
79
- export async function startSeamServer ({ quilts, identify, port = 0, host = '127.0.0.1', log = () => {} }) {
80
- if (!quilts) throw new Error('startSeamServer needs quilts (createQuilts)')
81
- const routes = quiltRoutes({ quilts, identify })
82
- const server = http.createServer(async (req, res) => {
83
- const send = (status, data) => {
84
- res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' })
85
- res.end(JSON.stringify(data))
86
- }
87
- try {
88
- const url = new URL(req.url, 'http://x')
89
- if (req.method === 'GET' && (url.pathname === '/healthz' || url.pathname === '/')) return send(200, { ok: true, service: 'seam' })
90
- const route = routes.find(([m, re]) => m === req.method && re.test(url.pathname))
91
- if (!route) return send(404, { ok: false, error: 'not found', code: 'not_found' })
92
- const opts = route[3] || {}
93
- const body = opts.raw ? await readBody(req, opts.maxBody || MAX_BODY, true) : ['POST', 'PUT', 'PATCH'].includes(req.method) ? await readBody(req, opts.maxBody || MAX_BODY, false) : {}
94
- const out = await route[2](req, body, url.pathname.match(route[1]).slice(1).map(decode))
95
- if (Array.isArray(out)) send(out[0], out[1])
96
- else send(200, out)
97
- } catch (err) {
98
- if (err instanceof QuiltError) return send(err.status, { ok: false, error: err.message, code: err.code, ...err.extra })
99
- if (err && Number.isInteger(err.status) && err.status >= 400 && err.status < 500) return send(err.status, { ok: false, error: err.message })
100
- log(`seam error: ${err?.stack || err?.message || err}`)
101
- send(500, { ok: false, error: 'internal error', code: 'internal' })
102
- }
103
- })
104
- await new Promise((resolve) => server.listen(port, host, resolve))
105
- const at = server.address()
106
- return { server, url: `http://${at.address.includes(':') ? `[${at.address}]` : at.address}:${at.port}`, close: () => new Promise((resolve) => server.close(resolve)) }
107
- }
108
-
109
- /** The SEAM_* settings as the store settings setup.js reads (its QUILT_SOURCE_* names still work). */
110
- export function storeEnv (env) {
111
- const pick = (seam, old) => env[seam] ?? env[old]
112
- const out = { QUILT_SOURCE: '1' }
113
- const map = { QUILT_SOURCE_STORE: pick('SEAM_STORE', 'QUILT_SOURCE_STORE'), QUILT_SOURCE_SUPABASE_URL: pick('SEAM_SUPABASE_URL', 'QUILT_SOURCE_SUPABASE_URL'), QUILT_SOURCE_SUPABASE_KEY: pick('SEAM_SUPABASE_KEY', 'QUILT_SOURCE_SUPABASE_KEY'), QUILT_SOURCE_BUCKET: pick('SEAM_BUCKET', 'QUILT_SOURCE_BUCKET'), QUILT_SOURCE_DATA: env.SEAM_DATA ?? env.QUILT_SOURCE_DATA ?? './seam-data', SUPABASE_URL: env.SUPABASE_URL, SEAM_SITE_URL: env.SEAM_SITE_URL }
114
- for (const [k, v] of Object.entries(map)) if (v !== undefined) out[k] = v
115
- return out
116
- }
117
-
118
- /**
119
- * Who is calling, from the settings: Seam's own sign-in (SEAM_AUTH_URL, Seam's Supabase project,
120
- * with SEAM_AUTH_JWT_SECRET when it still signs with a shared secret) checked here, and/or an
121
- * identity service over HTTP (SEAM_IDENTITY_URL) for every other token.
122
- */
123
- export function identityFromEnv (env, quilts, { fetch = globalThis.fetch } = {}) {
124
- const authUrl = env.SEAM_AUTH_URL || ''
125
- const verify = authUrl ? supabaseVerifier({ authUrl, jwtSecret: env.SEAM_AUTH_JWT_SECRET || null, fetch }) : null
126
- const fallback = env.SEAM_IDENTITY_URL ? remoteIdentity({ url: env.SEAM_IDENTITY_URL, fetch, ...(env.SEAM_IDENTITY_TTL_MS ? { ttlMs: Number(env.SEAM_IDENTITY_TTL_MS) } : {}) }) : null
127
- if (!verify && !fallback) throw new Error('seam serve needs a sign-in: SEAM_AUTH_URL (Seam\'s own accounts, its Supabase project) and/or SEAM_IDENTITY_URL (an identity service, e.g. https://api.heyquilt.com/v1/seam/me)')
128
- return seamIdentity({ quilts, verify, fallback })
129
- }
130
-
131
- /** `seam serve`: the settings from the environment, a port, and a log line. */
132
- export async function serveFromEnv (env = process.env, { log = console.log } = {}) {
133
- if (!env.SEAM_AUTH_URL && !env.SEAM_IDENTITY_URL) identityFromEnv(env, null)
134
- const quilts = await quiltsFromEnv(storeEnv(env), { log })
135
- const identify = identityFromEnv(env, quilts)
136
- const s = await startSeamServer({ quilts, identify, port: Number(env.SEAM_PORT || env.PORT || 8080), host: env.SEAM_HOST || '0.0.0.0', log })
137
- const from = [env.SEAM_AUTH_URL && `Seam accounts (${new URL(env.SEAM_AUTH_URL).host})`, env.SEAM_IDENTITY_URL && new URL(env.SEAM_IDENTITY_URL).host].filter(Boolean).join(' and ')
138
- log(`seam: listening on ${s.url}, sign-in from ${from}`)
139
- return s
140
- }
141
-
142
- if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
143
- serveFromEnv().catch((err) => { console.error(err.message || err); process.exit(1) })
144
- }