heyquilt 0.3.122 → 0.3.140
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.
- package/RELEASES.md +279 -17
- package/bin/quilt.js +62 -152
- package/desktop/main.js +16 -2
- package/package.json +3 -2
- package/src/account.js +0 -29
- package/src/agent-task-workflow.js +2 -2
- package/src/agents/common.js +13 -0
- package/src/ai-vm.js +9 -1
- package/src/api/activity.js +5 -0
- package/src/api/memory-store.js +16 -38
- package/src/api/notify.js +134 -0
- package/src/api/routes/browsers.js +29 -0
- package/src/api/routes/relay.js +26 -3
- package/src/api/routes/session-migrate.js +7 -4
- package/src/api/routes/sessions.js +73 -1
- package/src/api/routes/teams.js +0 -23
- package/src/api/server.js +17 -25
- package/src/api/supabase-store.js +43 -56
- package/src/autowork.js +182 -16
- package/src/branch-view.js +6 -131
- package/src/connection.js +36 -9
- package/src/control.js +131 -48
- package/src/duties.js +1 -1
- package/src/force-date.js +27 -0
- package/src/fsutil.js +13 -3
- package/src/github-commit.js +3 -315
- package/src/history.js +13 -3
- package/src/mcp.js +93 -167
- package/src/merge3.js +11 -3
- package/src/now-summary.js +113 -0
- package/src/pathrules.js +12 -0
- package/src/protocol.js +10 -2
- package/src/quilt-default.js +84 -0
- package/src/quilt-history.js +106 -0
- package/src/quilt-remote.js +78 -0
- package/src/relay-ask.js +3 -66
- package/src/relay-cluster.js +3 -5
- package/src/relay-lease.js +6 -2
- package/src/relay-mcp.js +154 -432
- package/src/relay-prune.js +6 -145
- package/src/relay-turn-sync.js +1 -1
- package/src/release-words.js +2 -2
- package/src/runner.js +20 -7
- package/src/seam-access.js +97 -0
- package/src/seam-claims.js +53 -0
- package/src/seam-url.js +15 -0
- package/src/server.js +1516 -1383
- package/src/session-follow.js +461 -27
- package/src/session-layout3.js +410 -4
- package/src/session-migrate.js +15 -11
- package/src/session-quilt.js +1 -1
- package/src/session-seam.js +233 -20
- package/src/session.js +745 -3438
- package/src/source-live/bring-in.js +283 -0
- package/src/source-live/live-block.js +504 -33
- package/src/source-live/live-square.js +16 -5
- package/src/status.js +7 -58
- package/src/turn-sync.js +15 -2
- package/src/ui/agent-guide.js +1 -1
- package/src/ui/app.css +85 -20
- package/src/ui/app.js +4 -1
- package/src/ui/changes.js +5 -14
- package/src/ui/chat.js +9 -4
- package/src/ui/cloud-ai-model.js +13 -0
- package/src/ui/cloud-ai.js +10 -2
- package/src/ui/common.js +6 -4
- package/src/ui/fileview.js +20 -7
- package/src/ui/force-words.js +19 -0
- package/src/ui/git-rule.js +9 -28
- package/src/ui/home.js +3 -223
- package/src/ui/loom-model.js +15 -1
- package/src/ui/quiltview.js +213 -0
- package/src/ui/seam-model.js +251 -0
- package/src/ui/session.js +377 -489
- package/src/ui/tree.js +2 -8
- package/src/ui/voice-call.js +83 -14
- package/src/ui/voice-model.js +39 -0
- package/src/ui/voice.js +167 -17
- package/src/ui/workspaces.js +2 -122
- package/src/ui-server.js +81 -66
- package/src/update-check.js +25 -1
- package/src/voice.js +13 -6
- package/src/api/routes/github.js +0 -393
- package/src/branches.js +0 -194
- package/src/clash.js +0 -128
- package/src/commit-request.js +0 -59
- package/src/commit.js +0 -213
- package/src/git-hook.js +0 -48
- package/src/git.js +0 -147
- package/src/gitignore.js +0 -49
- package/src/gitstate.js +0 -1033
- package/src/quilts/AGENTS.md +0 -1
- package/src/quilts/CLAUDE.md +0 -1
- package/src/quilts/accounts.js +0 -136
- package/src/quilts/affected.js +0 -132
- package/src/quilts/archiver.js +0 -175
- package/src/quilts/autorevert.js +0 -49
- package/src/quilts/bisect-run.js +0 -31
- package/src/quilts/blame.js +0 -103
- package/src/quilts/block.js +0 -347
- package/src/quilts/bucket.js +0 -25
- package/src/quilts/cbor.js +0 -92
- package/src/quilts/checks.js +0 -308
- package/src/quilts/claims.js +0 -144
- package/src/quilts/cli.js +0 -536
- package/src/quilts/client.js +0 -137
- package/src/quilts/conflict-format.js +0 -190
- package/src/quilts/describe.js +0 -296
- package/src/quilts/dogfood.js +0 -82
- package/src/quilts/errors.js +0 -8
- package/src/quilts/events.js +0 -171
- package/src/quilts/export-git.js +0 -130
- package/src/quilts/export-remote.js +0 -156
- package/src/quilts/export.js +0 -81
- package/src/quilts/fastmerge.js +0 -41
- package/src/quilts/hash.js +0 -44
- package/src/quilts/history.js +0 -266
- package/src/quilts/ids.js +0 -12
- package/src/quilts/import-follow.js +0 -236
- package/src/quilts/import-git.js +0 -294
- package/src/quilts/import.js +0 -196
- package/src/quilts/index.js +0 -94
- package/src/quilts/keys.js +0 -186
- package/src/quilts/limits.js +0 -67
- package/src/quilts/lines.js +0 -49
- package/src/quilts/live.js +0 -50
- package/src/quilts/merge-smart.js +0 -114
- package/src/quilts/merge.js +0 -501
- package/src/quilts/oauth.js +0 -202
- package/src/quilts/objects.js +0 -144
- package/src/quilts/pack.js +0 -118
- package/src/quilts/records.js +0 -211
- package/src/quilts/resolutions.js +0 -69
- package/src/quilts/reviews.js +0 -208
- package/src/quilts/routes.js +0 -186
- package/src/quilts/runner.js +0 -95
- package/src/quilts/server.js +0 -144
- package/src/quilts/service.js +0 -963
- package/src/quilts/setup.js +0 -41
- package/src/quilts/signing.js +0 -149
- package/src/quilts/squares.js +0 -239
- package/src/quilts/store-memory.js +0 -844
- package/src/quilts/store-rpc.js +0 -90
- package/src/quilts/store-supabase.js +0 -13
- package/src/quilts/tags.js +0 -64
- package/src/quilts/textdiff.js +0 -96
- package/src/quilts/tokens.js +0 -66
- package/src/quilts/tools.js +0 -403
- package/src/quilts/tree.js +0 -351
- package/src/quilts/webhook.js +0 -116
- package/src/quilts/window.js +0 -121
- package/src/quilts/xdiff.js +0 -435
- package/src/relay-ci.js +0 -111
- package/src/relay-clash.js +0 -214
- package/src/relay-commit.js +0 -918
- package/src/relay-filekey.js +0 -46
- package/src/relay-git.js +0 -274
- package/src/relay-githttp.js +0 -166
- package/src/relay-github.js +0 -109
- package/src/relay-outbound.js +0 -1295
- package/src/relay-repo-moves.js +0 -43
- package/src/relay-sync.js +0 -795
- package/src/relay-upstream.js +0 -889
- package/src/sync-words.js +0 -42
- package/src/ui/branches.js +0 -111
- package/src/upstream.js +0 -192
package/src/control.js
CHANGED
|
@@ -11,6 +11,14 @@ import { createRun, waitRun, endRun, secretNames, apiCall } from './secrets-clie
|
|
|
11
11
|
import { runMailTool, MAIL_TOOLS } from './mail-tools.js'
|
|
12
12
|
import { runWakeTool } from './wake-tool.js'
|
|
13
13
|
import { wakeCheck } from './wake.js'
|
|
14
|
+
import { seamAccess, sessionCredential } from './seam-access.js'
|
|
15
|
+
import { runSessionSeamTool } from './session-seam.js'
|
|
16
|
+
import { appSeamUrl } from './seam-url.js'
|
|
17
|
+
import { squareFor } from './session-follow.js'
|
|
18
|
+
import { shareWithPeople } from './session-layout3.js'
|
|
19
|
+
import { QUILT_MOVE_TIMEOUT_MS } from './connection.js'
|
|
20
|
+
import { FEATURES } from './protocol.js'
|
|
21
|
+
import { currentVersion } from './releases.js'
|
|
14
22
|
|
|
15
23
|
export async function startControl (session, extras = {}) {
|
|
16
24
|
const token = crypto.randomBytes(16).toString('hex')
|
|
@@ -22,10 +30,45 @@ export async function startControl (session, extras = {}) {
|
|
|
22
30
|
// Step 2 of joining (wake.js): an agent that hasn't chosen how Quilt wakes it is reminded with each answer.
|
|
23
31
|
const wake = wakeCheck({
|
|
24
32
|
fetchWake: async () => {
|
|
25
|
-
|
|
33
|
+
// A person's cloud AI (aiOf) is woken by its machine (autowork.js), never asked how.
|
|
34
|
+
if (session.kind !== 'agent' || session.aiOf || !extras.apiAuth) return { method: 'n/a' }
|
|
26
35
|
return (await apiCall(await secretsApi(), 'GET', '/v1/agents/me/wake')).wake
|
|
27
36
|
}
|
|
28
37
|
})
|
|
38
|
+
// Seam's tools for this folder's AI sessions, in a session that is a quilt (session-seam.js), as
|
|
39
|
+
// this session's member: its person, or its agent on a cloud machine (seam-access.js).
|
|
40
|
+
// Kept on the session, so its AI's runs (autowork.js) read Seam as the same member.
|
|
41
|
+
if (extras.apiAuth && !session.apiAuth) session.apiAuth = extras.apiAuth
|
|
42
|
+
const seam = session.seamMember ? session.seamMember() : (session.seam || (session.seam = seamAccess({ api: () => appSeamUrl(), credential: sessionCredential({ apiAuth: extras.apiAuth, account: readAccount }) })))
|
|
43
|
+
const shared = new Set() // squares this person holds and has shared with the session's other people
|
|
44
|
+
const seamTool = async (b) => {
|
|
45
|
+
const q = session.quilt
|
|
46
|
+
if (!q || !q.id) return { content: [{ type: 'text', text: 'This session is not a quilt, so it has no Seam tools.' }], isError: true }
|
|
47
|
+
const branch = session.squareBranchKey ? session.squareBranchKey() : session.branchKey()
|
|
48
|
+
const at = squareFor(q, branch)
|
|
49
|
+
const square = at ? at.square : ((q.squares || {})[branch] || null)
|
|
50
|
+
const fail = (text) => ({ content: [{ type: 'text', text }], isError: true })
|
|
51
|
+
// The folder's live block on that square: the folder itself, following it in place, or a
|
|
52
|
+
// separate one (session-follow.js). Its commit is `quilt commit`'s: what the relay holds.
|
|
53
|
+
const f = session.squareFollower
|
|
54
|
+
const here = f && f.here && at && f.here.square === at.square ? f.here : null
|
|
55
|
+
const sep = f && f.following && at && f.following.square === at.square ? f.following : null
|
|
56
|
+
const live = (here && here.block) || (sep && sep.block) || null
|
|
57
|
+
if (here && !here.block && (b.tool === 'seam_commit' || b.tool === 'seam_merge')) return fail(here.problem ? `This folder can't follow its square yet (${here.problem}); Quilt tries again by itself.` : 'This folder is still starting to follow its square; try again in a moment.')
|
|
58
|
+
let c
|
|
59
|
+
try { c = await seam.client(q.id, square ? [square] : []) } catch (err) { return fail(err.message) }
|
|
60
|
+
// The folder's live work is read and committed as this member.
|
|
61
|
+
if (live) live.client = c.client
|
|
62
|
+
if (c.person && square && !shared.has(square)) {
|
|
63
|
+
shared.add(square)
|
|
64
|
+
// The square's holder shares it with the session's other people, so their AIs (working for them) write there too.
|
|
65
|
+
shareWithPeople(c.client, q.id, square, session.members || []).catch(() => shared.delete(square))
|
|
66
|
+
}
|
|
67
|
+
const next = at ? async () => session.conn.branchRequest({ op: 'quilt-next', token: c.token, branch }, { timeoutMs: QUILT_MOVE_TIMEOUT_MS }) : null
|
|
68
|
+
return runSessionSeamTool(String(b.tool || ''), b.args || {}, { client: c.client, quilt: q.id, square, live, next, branch })
|
|
69
|
+
}
|
|
70
|
+
// The app's square panel (ui-server.js) commits and merges through the same tools, as the same member.
|
|
71
|
+
session.seamTool = seamTool
|
|
29
72
|
const routes = {
|
|
30
73
|
'GET /status': (b) => {
|
|
31
74
|
// To one of the AI sessions working through this app, "you" is that session (persona.js).
|
|
@@ -94,47 +137,33 @@ export async function startControl (session, extras = {}) {
|
|
|
94
137
|
'POST /chat-link': (b) => session.createChatLink({ name: b.name, minutes: b.minutes }),
|
|
95
138
|
// Owner only: how long a chat link still works. { who: name or key, minutes } -> { name, expiresAt }
|
|
96
139
|
'POST /chat-link/extend': (b) => session.extendChatLink(String(b.who || ''), b.minutes),
|
|
97
|
-
// Owner only: the relay's read-only GitHub token, for commits while everyone's offline. { token } ('' clears) -> { githubToken }
|
|
98
|
-
'POST /github-token': (b) => session.setGithubToken(b.token),
|
|
99
140
|
'POST /agent': (b) => { session.addAgent(b.client); return { ok: true } },
|
|
100
141
|
'POST /feed': (b) => ({ entries: session.agentFeedFor(b.who, { limit: Math.min(Number(b.limit) || 40, 300) }) }),
|
|
101
142
|
// The chronology: { path, by, since, task, limit } (see Session.historyQuery).
|
|
102
|
-
|
|
143
|
+
// In a session linked to a quilt (Seam), the quilt's log too (Session.historyLinked): { entries, note? }.
|
|
144
|
+
'POST /history': (b) => session.historyLinked(b),
|
|
103
145
|
'GET /tree': () => session.tree(),
|
|
104
146
|
'POST /sharing': (b) => ({ on: session.setAgentSharing(b.on !== false) }),
|
|
105
|
-
'GET /commits': () => session.commitStatus({ includeMe: false }),
|
|
106
|
-
'GET /branches': () => ({ branches: session.status().branches, git: session.status().git }),
|
|
107
|
-
// The relay's own list, with where each branch stands with GitHub (quilt_sync_status).
|
|
108
|
-
'GET /sync-status': () => ({ branches: session.branchList || [], mine: session.branchKey ? session.branchKey() : '' }),
|
|
109
|
-
'POST /branches/sync': () => session.syncBranchNow(),
|
|
110
147
|
// `quilt session migrate --dry-run`: what making the session a quilt would do (the relay's dry run).
|
|
111
148
|
'POST /quilt-plan': () => session.quiltPlan(),
|
|
112
149
|
// The quilt this session is (Seam), as the relay last said; null while it is not (session-seam.js).
|
|
113
150
|
'GET /quilt': () => ({ quilt: session.quilt || null }),
|
|
151
|
+
// One of Seam's tools (session-seam.js SEAM_SESSION_TOOLS), { tool, args }: an MCP tool result.
|
|
152
|
+
'POST /seam': (b) => seamTool(b),
|
|
114
153
|
// `quilt session migrate`: the relay makes the session a quilt, as its owner (their sign-in).
|
|
115
154
|
'POST /quilt-migrate': () => session.quiltMigrate(process.env.SEAM_TOKEN || (readAccount() || {}).token),
|
|
155
|
+
// "Make this a quilt" (`quilt session migrate`, the app's button): made a quilt and moved into its squares in one go.
|
|
156
|
+
'POST /quilt-make': () => session.quiltMake(process.env.SEAM_TOKEN || (readAccount() || {}).token),
|
|
116
157
|
// `quilt session migrate --switch` / `--back`: the session's files into its squares (layout 3), or back.
|
|
117
158
|
'POST /quilt-switch': () => session.quiltSwitch(process.env.SEAM_TOKEN || (readAccount() || {}).token),
|
|
118
|
-
'POST /quilt-unswitch': () => session.quiltUnswitch(),
|
|
159
|
+
'POST /quilt-unswitch': (b) => session.quiltUnswitch(process.env.SEAM_TOKEN || (readAccount() || {}).token, Array.isArray(b.carried) ? b.carried : []),
|
|
160
|
+
// `quilt session migrate --shadow <quilt>`: link the session to the quilt an import of its git remote fills (16 step 1).
|
|
161
|
+
'POST /quilt-shadow': (b) => session.quiltShadowLink(process.env.SEAM_TOKEN || (readAccount() || {}).token, b.quilt ?? null),
|
|
119
162
|
// `quilt session follow <folder>`: at layout 3, that folder follows this branch's square, live.
|
|
120
|
-
'POST /quilt-follow': (b) => session.quiltFollow(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
'POST /agent-commits': (b) => session.setAgentCommits(b.mode),
|
|
124
|
-
// GitHub as a node: the owner's switch, and a person adopting an agent's branch (relay-outbound.js).
|
|
125
|
-
'POST /github-sync': async (b) => (await session.setGithubSync(b.on !== false)).githubSync === false ? { githubSync: false } : { githubSync: true },
|
|
126
|
-
'POST /adopt': (b) => session.adoptBranch(String(b.from || '')),
|
|
127
|
-
// The branch's pull requests on GitHub, and merging one, through the relay (relay-commit.js).
|
|
128
|
-
'POST /github/recheck': () => session.githubRecheck(),
|
|
129
|
-
'POST /unpublished': (b) => session.unpublished({ files: Array.isArray(b.files) ? b.files : null, task: b.task || null, column: b.column || '' }),
|
|
130
|
-
// git in an agent's folder: what it signs in to GitHub with (`quilt git-credential`), and whether a push may go (`quilt git-pre-push`).
|
|
131
|
-
'POST /git/credential': (b) => session.gitCredential({ host: b.host || '' }),
|
|
132
|
-
'POST /git/push-check': (b) => session.pushCheck({ remote: b.remote || 'origin', refs: Array.isArray(b.refs) ? b.refs : [] }),
|
|
163
|
+
'POST /quilt-follow': (b) => session.quiltFollow(null, b.dir),
|
|
164
|
+
// `quilt commit <message>`: at layout 3, the square this folder follows is committed, as this person.
|
|
165
|
+
'POST /square-commit': (b) => session.squareCommit(String(b.message || '')),
|
|
133
166
|
|
|
134
|
-
// A commit request committed in this folder, which has git (commit.js).
|
|
135
|
-
'POST /commit-request/commit': (b) => session.commitRequest(String(b.id || '')),
|
|
136
|
-
'GET /uncommitted': () => session.uncommittedWork(),
|
|
137
|
-
'POST /commit-request/done': (b) => ({ done: session.resolveCommitRequests({ ids: b.id ? [String(b.id)] : null }) }),
|
|
138
167
|
'POST /work': (b) => {
|
|
139
168
|
if (b.state === 'working') session.personaSays(b.via, b.note)
|
|
140
169
|
const work = session.setWork(b.state, b.note)
|
|
@@ -160,17 +189,13 @@ export async function startControl (session, extras = {}) {
|
|
|
160
189
|
session.noteTaskChange(null, task, b.via)
|
|
161
190
|
return { task, tasks: session.taskList() }
|
|
162
191
|
},
|
|
163
|
-
'POST /tasks/update':
|
|
192
|
+
'POST /tasks/update': (b) => {
|
|
164
193
|
const before = session.taskList().find((t) => t.id === b.id) || null
|
|
165
|
-
// Done merges the task's pull request first (relay-commit.js): refused with the reason when it can't be.
|
|
166
|
-
const finished = b.column === 'done' && before && before.column !== 'done' ? await session.finishTask(b.id) : null
|
|
167
194
|
const task = session.updateTask(b)
|
|
168
195
|
session.noteTaskChange(before, task, b.via)
|
|
169
196
|
if (b.column === 'doing' && task) session.personaSays(b.via, task.title) // taking a task can name an AI session
|
|
170
197
|
if (task && (b.column === 'doing' || b.column === 'done')) session.stitchClock(b.via, b.column === 'doing')
|
|
171
|
-
|
|
172
|
-
const branch = b.column === 'doing' && task && before && before.column !== 'doing' ? await session.branchForTask(task).catch(() => null) : null
|
|
173
|
-
return { task, tasks: session.taskList(), ...(finished && finished.merged ? { merged: finished.merged } : {}), ...(branch ? { branch } : {}) }
|
|
198
|
+
return { task, tasks: session.taskList() }
|
|
174
199
|
},
|
|
175
200
|
// What an agent gets when it picks a task up: history for its files, claims, the project's checks.
|
|
176
201
|
'POST /tasks/brief': (b) => session.taskBrief(b.id),
|
|
@@ -181,10 +206,6 @@ export async function startControl (session, extras = {}) {
|
|
|
181
206
|
return r
|
|
182
207
|
},
|
|
183
208
|
'POST /tasks/delete': (b) => { session.deleteTask(b.id); return { tasks: session.taskList() } },
|
|
184
|
-
// Merges Quilt made on its own (session.js autoSettle), and files waiting for their holder to let go.
|
|
185
|
-
'GET /merges': () => ({ merges: session.mergeList() }),
|
|
186
|
-
// quilt_clash: a file held because the session and GitHub changed the same lines (relay-clash.js).
|
|
187
|
-
'POST /clash': (b) => session.clash({ action: String(b.action || ''), path: String(b.path || ''), content: typeof b.content === 'string' ? b.content : null, checks: b.checks && typeof b.checks === 'object' ? b.checks : null, force: b.force === true }),
|
|
188
209
|
'GET /rules': () => ({ text: session.projectRulesText() }),
|
|
189
210
|
// Secrets (`quilt run`, quilt_secret_names): asked of the API as this session's own identity
|
|
190
211
|
// (its person, or the agent it runs as). Run tokens and names only: never a value.
|
|
@@ -221,7 +242,8 @@ export async function startControl (session, extras = {}) {
|
|
|
221
242
|
const auth = await secretsApi()
|
|
222
243
|
return { text: await runMailTool(b.tool, b.args && typeof b.args === 'object' ? b.args : {}, { call: (method, route, body) => apiCall(auth, method, route, body), room: session.room, me: session.actorName ? session.actorName(b.via) : session.name }) }
|
|
223
244
|
},
|
|
224
|
-
|
|
245
|
+
// `version` and `features` (what this app tells the relay it can do): the CLI checks them before a move (bin/quilt.js).
|
|
246
|
+
'GET /info': (b) => ({ room: session.room, dir: session.root, name: session.actorName(b.via), person: session.name, kind: session.kind, invite: extras.invite || null, viewInvite: extras.viewInvite || null, access: session.access, pid: process.pid, version: appVersion(), features: session.features || FEATURES })
|
|
225
247
|
}
|
|
226
248
|
const server = http.createServer(async (req, res) => {
|
|
227
249
|
const reply = (code, body) => {
|
|
@@ -299,15 +321,76 @@ export function findDaemon (start = process.env.QUILT_DIR || process.cwd()) {
|
|
|
299
321
|
}
|
|
300
322
|
}
|
|
301
323
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
324
|
+
const appVersion = () => { try { return currentVersion() } catch { return '' } }
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* `timeoutMs`: give up (throw) when the session hasn't answered by then (git's helpers: git must never wait on Quilt).
|
|
328
|
+
* Without it, it waits as long as the session takes (a move into Seam can take minutes): node:http, not fetch, whose
|
|
329
|
+
* own limit gave up after 5 minutes with nothing but "fetch failed". A session that can't be reached, or that
|
|
330
|
+
* stopped while answering, throws with what happened and what to do (`unreachable`).
|
|
331
|
+
*/
|
|
332
|
+
export function call (daemon, method, route, body, { timeoutMs = 0 } = {}) {
|
|
333
|
+
const data = body ? JSON.stringify(body) : null
|
|
334
|
+
return new Promise((resolve, reject) => {
|
|
335
|
+
let timer = null
|
|
336
|
+
let late = null // the timeout's error, once it fired: whatever the socket says after it
|
|
337
|
+
const failed = (err) => { clearTimeout(timer); reject(late || (err.name === 'TimeoutError' ? err : unreachable(daemon, err))) }
|
|
338
|
+
const req = http.request({
|
|
339
|
+
host: '127.0.0.1',
|
|
340
|
+
port: daemon.port,
|
|
341
|
+
path: route,
|
|
342
|
+
method,
|
|
343
|
+
// A connection each: a kept-alive one outlives an app that restarted, and the next call would find it cut.
|
|
344
|
+
agent: false,
|
|
345
|
+
headers: { authorization: `Bearer ${daemon.token}`, 'content-type': 'application/json', ...(data ? { 'content-length': Buffer.byteLength(data) } : {}) }
|
|
346
|
+
}, (res) => {
|
|
347
|
+
let raw = ''
|
|
348
|
+
res.setEncoding('utf8')
|
|
349
|
+
res.on('data', (c) => { raw += c })
|
|
350
|
+
res.on('error', failed)
|
|
351
|
+
res.on('end', () => {
|
|
352
|
+
clearTimeout(timer)
|
|
353
|
+
let json
|
|
354
|
+
try { json = JSON.parse(raw) } catch { return reject(new Error(`Quilt's app sent something that isn't an answer (HTTP ${res.statusCode})`)) }
|
|
355
|
+
if (res.statusCode < 200 || res.statusCode >= 300) return reject(Object.assign(new Error(json.error || `HTTP ${res.statusCode}`), { status: res.statusCode }))
|
|
356
|
+
resolve(json)
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
req.on('error', failed)
|
|
360
|
+
if (timeoutMs) {
|
|
361
|
+
timer = setTimeout(() => {
|
|
362
|
+
late = Object.assign(new Error(`The session in ${daemon.dir || 'this folder'} did not answer within ${Math.round(timeoutMs / 1000)} s`), { name: 'TimeoutError' })
|
|
363
|
+
req.destroy(late)
|
|
364
|
+
}, timeoutMs)
|
|
365
|
+
}
|
|
366
|
+
req.end(data || undefined)
|
|
309
367
|
})
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* The control server couldn't be reached (nothing listens: the app closed this session or restarted), or
|
|
372
|
+
* it closed the connection before answering (the session stopped meanwhile: left, quit, or sent away by
|
|
373
|
+
* the relay). `cut` tells the two apart for callers that say more (a move into Seam goes on without the app).
|
|
374
|
+
*/
|
|
375
|
+
export function unreachable (daemon, err) {
|
|
376
|
+
const code = (err && err.code) || ''
|
|
377
|
+
const where = daemon && daemon.dir ? ` for ${daemon.dir}` : ''
|
|
378
|
+
const cut = code !== 'ECONNREFUSED'
|
|
379
|
+
const msg = cut
|
|
380
|
+
? `Quilt's app${where} stopped answering in the middle of this (${code || err.message}): the session there stopped (left, Quilt quit or restarted, or the relay sent this app away to update). Open the session in Quilt again, then check before trying again.`
|
|
381
|
+
: `Quilt's app${where} isn't answering: the session there is not running any more (Quilt quit, restarted, or left it). Open it in Quilt (or run quilt join here), then try again.`
|
|
382
|
+
return Object.assign(new Error(msg), { unreachable: true, cut, code })
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Why `quilt session migrate` (or --switch, --back) must not start through this app, from its GET /info,
|
|
387
|
+
* or null. The move ends by sending away every app that can't follow a square (no `quilt-folders`): the
|
|
388
|
+
* owner's own app too, cutting this command's answer off mid-way. An app older than this check says
|
|
389
|
+
* neither its features nor its version; `cliVersion` is the quilt command's own.
|
|
390
|
+
*/
|
|
391
|
+
export function moveRefusal (info, cliVersion = '') {
|
|
392
|
+
const features = info && typeof info.features === 'string' ? info.features.split(',') : null
|
|
393
|
+
if (features && features.includes('quilt-folders')) return null
|
|
394
|
+
const was = info && info.version ? ` (${info.version})` : ''
|
|
395
|
+
return `Quilt's app for this folder${was} is older than this quilt command${cliVersion ? ` (${cliVersion})` : ''}: the move sends it away to update at the end, and this command would lose its answer. Update Quilt, or restart it so it runs the new version, then run this again.`
|
|
313
396
|
}
|
package/src/duties.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
//
|
|
16
16
|
// Messages are chat entries ({ id, by, to, text, ts }) the reader can see.
|
|
17
17
|
import { mentionsMe, mentioned, ALL_AGENTS } from './inbox.js'
|
|
18
|
-
import { RELAY_BY } from './
|
|
18
|
+
import { RELAY_NAME as RELAY_BY } from './ui/chat.js'
|
|
19
19
|
import { RELAY_OLD_NAMES } from './ui/chat.js'
|
|
20
20
|
|
|
21
21
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The forced date (Seam's plans/quilt-source-control/16, flag 9): QUILT_SOURCE_FORCE_AT=<date> on the
|
|
2
|
+
// relay. Unset (or not a date), nothing happens. From the date on, a session still in git mode is
|
|
3
|
+
// moved on without anyone losing work: one with no git remote becomes a quilt without history on its
|
|
4
|
+
// owner's next open; one with a git remote that has not cut over keeps chat, tasks, the agent feed and
|
|
5
|
+
// the audit trail, and its live file sync stops for good (git, its remote, clones and CI untouched).
|
|
6
|
+
// Pure: no I/O (test/unit/force-date.test.js). The words are in ui/force-words.js, which the app loads too.
|
|
7
|
+
export { syncStoppedWords, forceAheadWords } from './ui/force-words.js'
|
|
8
|
+
|
|
9
|
+
/** The date as ms, or null when unset or not a date. */
|
|
10
|
+
export function forceAtMs (env = process.env) {
|
|
11
|
+
const v = String(env.QUILT_SOURCE_FORCE_AT || '').trim()
|
|
12
|
+
if (!v) return null
|
|
13
|
+
const ms = Date.parse(v)
|
|
14
|
+
return Number.isFinite(ms) ? ms : null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* What the date means for a session now: null (no date, not yet, or already a quilt in its squares),
|
|
19
|
+
* 'stop' (a git remote, not cut over: live sync stops), or 'migrate' (no git remote: its owner's next
|
|
20
|
+
* open makes it a quilt without history).
|
|
21
|
+
*/
|
|
22
|
+
export function forcedAction ({ at, now = Date.now(), layout = 2, remote = false, stopped = false }) {
|
|
23
|
+
if (stopped) return 'stop'
|
|
24
|
+
if (at == null || now < at || layout === 3) return null
|
|
25
|
+
return remote ? 'stop' : 'migrate'
|
|
26
|
+
}
|
|
27
|
+
|
package/src/fsutil.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import crypto from 'node:crypto'
|
|
4
|
-
import {
|
|
4
|
+
import { isUtf8 } from 'node:buffer'
|
|
5
|
+
import { makeIgnore, isIgnored, scopeIgnore, submodulePathsIn, IGNORE_FILES } from './pathrules.js'
|
|
5
6
|
|
|
6
7
|
export { IGNORE_FILES } from './pathrules.js'
|
|
7
8
|
export { MAX_TEXT_BYTES, MAX_BINARY_BYTES, LARGE_FILE_BYTES, MAX_STORED_BINARY_BYTES, CHAT_FILES, isIgnored, isSafeRelPath, globMatcher, patternsOverlap } from './pathrules.js'
|
|
@@ -27,6 +28,9 @@ export function closeWatcher (w) {
|
|
|
27
28
|
*/
|
|
28
29
|
export function loadIgnore (root) {
|
|
29
30
|
const ig = makeIgnore()
|
|
31
|
+
// A submodule (.gitmodules) is a repository of its own that git keeps as one commit id: its files
|
|
32
|
+
// are its own git's, never the session's (nor a Seam square's, which holds it as a gitlink).
|
|
33
|
+
for (const p of submodulePaths(root)) ig.add(`/${p}`)
|
|
30
34
|
const visit = (dirRel) => {
|
|
31
35
|
const dir = path.join(root, dirRel)
|
|
32
36
|
for (const file of IGNORE_FILES) {
|
|
@@ -44,6 +48,11 @@ export function loadIgnore (root) {
|
|
|
44
48
|
return ig
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
/** The folders .gitmodules names as submodules (`path = …`), as safe relative paths. */
|
|
52
|
+
export function submodulePaths (root) {
|
|
53
|
+
try { return submodulePathsIn(fs.readFileSync(path.join(root, '.gitmodules'), 'utf8')) } catch { return [] }
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
/**
|
|
48
57
|
* Resolves a relative path to an absolute one inside root, making sure no
|
|
49
58
|
* symlinked parent directory points outside of it (or nowhere: a dangling
|
|
@@ -71,8 +80,9 @@ export function resolveInside (root, rel) {
|
|
|
71
80
|
export function looksBinary (buf) {
|
|
72
81
|
const n = Math.min(buf.length, 8000)
|
|
73
82
|
for (let i = 0; i < n; i++) if (buf[i] === 0) return true
|
|
74
|
-
// Invalid UTF-8 is treated as binary so it round-trips byte for byte.
|
|
75
|
-
|
|
83
|
+
// Invalid UTF-8 is treated as binary so it round-trips byte for byte. Checked in place: decoding
|
|
84
|
+
// and encoding every file again to compare made two copies of each one.
|
|
85
|
+
return !isUtf8(buf)
|
|
76
86
|
}
|
|
77
87
|
|
|
78
88
|
export function sha1 (buf) {
|