heyquilt 0.3.121 → 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.
- package/RELEASES.md +292 -17
- package/bin/quilt.js +72 -5
- package/desktop/main.js +16 -2
- package/package.json +3 -1
- 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 +21 -10
- package/src/api/notify.js +134 -0
- package/src/api/routes/browsers.js +29 -0
- package/src/api/routes/github.js +0 -13
- package/src/api/routes/relay.js +26 -3
- package/src/api/routes/seam-connect.js +103 -0
- package/src/api/routes/session-migrate.js +39 -0
- package/src/api/routes/sessions.js +73 -1
- package/src/api/server.js +46 -12
- package/src/api/supabase-store.js +53 -4
- package/src/autowork.js +182 -16
- package/src/connection.js +39 -9
- package/src/control.js +139 -13
- package/src/force-date.js +27 -0
- package/src/fsutil.js +13 -3
- package/src/gitstate.js +38 -2
- package/src/history.js +25 -1
- package/src/mcp.js +93 -7
- 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-commit.js +5 -0
- package/src/relay-lease.js +6 -2
- package/src/relay-mcp.js +172 -8
- package/src/relay-outbound.js +1 -1
- package/src/relay-prune.js +4 -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 +1767 -33
- package/src/session-follow.js +515 -0
- package/src/session-layout3.js +466 -0
- package/src/session-migrate.js +123 -0
- package/src/session-quilt.js +145 -0
- package/src/session-seam.js +284 -0
- package/src/session.js +899 -46
- package/src/source-live/bring-in.js +283 -0
- package/src/source-live/live-block.js +507 -35
- package/src/source-live/live-square.js +17 -6
- package/src/status.js +5 -0
- 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/chat.js +9 -3
- package/src/ui/cloud-ai-model.js +13 -0
- package/src/ui/cloud-ai.js +10 -2
- package/src/ui/common.js +6 -2
- package/src/ui/fileview.js +20 -7
- package/src/ui/force-words.js +19 -0
- package/src/ui/git-rule.js +9 -0
- package/src/ui/home.js +0 -147
- 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 +413 -31
- 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-server.js +80 -28
- package/src/update-check.js +25 -1
- package/src/voice.js +13 -6
- package/src/git.js +0 -147
- package/src/quilts/AGENTS.md +0 -1
- package/src/quilts/CLAUDE.md +0 -1
- 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/claims.js +0 -144
- package/src/quilts/cli.js +0 -449
- package/src/quilts/client.js +0 -128
- package/src/quilts/conflict-format.js +0 -190
- package/src/quilts/describe.js +0 -266
- package/src/quilts/dogfood.js +0 -82
- package/src/quilts/errors.js +0 -8
- package/src/quilts/events.js +0 -160
- package/src/quilts/export-git.js +0 -130
- 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-git.js +0 -291
- package/src/quilts/import.js +0 -134
- package/src/quilts/index.js +0 -72
- 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 -486
- 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 -195
- package/src/quilts/routes.js +0 -126
- package/src/quilts/service.js +0 -923
- package/src/quilts/setup.js +0 -34
- package/src/quilts/signing.js +0 -149
- package/src/quilts/squares.js +0 -239
- package/src/quilts/store-memory.js +0 -683
- package/src/quilts/store-rpc.js +0 -79
- 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 -375
- package/src/quilts/tree.js +0 -351
- package/src/quilts/webhook.js +0 -116
- package/src/quilts/window.js +0 -119
- package/src/quilts/xdiff.js +0 -435
- package/src/ui/branches.js +0 -111
package/src/ui-server.js
CHANGED
|
@@ -15,7 +15,6 @@ import { MAX_SHARED_FILE_BYTES } from './protocol.js'
|
|
|
15
15
|
import { getSettings, saveSettings, unsupportedRelay, relayUrl } from './settings.js'
|
|
16
16
|
import { PICKUP_MODES, pickupMode } from './tasks.js'
|
|
17
17
|
import { AGENT_COMMITS } from './github-commit.js'
|
|
18
|
-
import * as gitops from './git.js'
|
|
19
18
|
import { execFile } from 'node:child_process'
|
|
20
19
|
import { installedEditors, openIn, copyToClipboard } from './editors.js'
|
|
21
20
|
import { autowork, autoConfig, publicConfig, AUTO_MODES } from './autowork.js'
|
|
@@ -28,6 +27,7 @@ import { effectiveAccess, builtinType } from './session-access.js'
|
|
|
28
27
|
import { cleanSessionName, BAD_SESSION_NAME, SESSION_NAME_MAX } from './session-name.js'
|
|
29
28
|
import { personPasses, personApiAuth } from './pass-source.js'
|
|
30
29
|
import { INVALID_INVITE } from './ui/invite.js'
|
|
30
|
+
import { seamResultWords } from './ui/seam-model.js'
|
|
31
31
|
import { loadIdentity } from './identity.js'
|
|
32
32
|
import { currentVersion, localReleases, latestRelease, compareVersions, downloadUrl, seenVersion, markSeen, hiddenUpdate, hideUpdate } from './releases.js'
|
|
33
33
|
import { createReporter } from './report.js'
|
|
@@ -45,6 +45,9 @@ const THEMES = ['light', 'dark', 'system']
|
|
|
45
45
|
const SAVE_FAILED = "Quilt couldn't save your sign-in on this computer."
|
|
46
46
|
const SIGNED_OUT_MESSAGE = 'This computer was signed out. Sign in again.'
|
|
47
47
|
const LOCAL_RELAY_GONE = "This session ran on your computer's own relay, which Quilt no longer supports. Your files are untouched."
|
|
48
|
+
// 16's R2: no New session from GitHub. Said to an older window that still offers it.
|
|
49
|
+
export const GITHUB_GONE = 'Starting a session from GitHub is gone: start one from a folder (a new session\'s code is committed in Seam from the start), and bring a repository\'s history into Seam with seam github import.'
|
|
50
|
+
const STATUS_PUSH_MS = 500 // a session's status goes to the window at most this often when the session changes it
|
|
48
51
|
// Until this computer is signed in, only these answer.
|
|
49
52
|
const OPEN_ROUTES = new Set(['GET /api/account', 'POST /api/account/start', 'POST /api/account/cancel', 'POST /api/account/signout', 'GET /api/events', 'POST /api/shutdown', 'POST /api/report'])
|
|
50
53
|
|
|
@@ -58,7 +61,7 @@ function profile () {
|
|
|
58
61
|
return {
|
|
59
62
|
name: account ? account.account.name : os.userInfo().username,
|
|
60
63
|
tool: s.tool || guessTool(),
|
|
61
|
-
// No tool picked in Settings: the shown one is a guess,
|
|
64
|
+
// No tool picked in Settings: the shown one is a guess, which saving a colour doesn't pin.
|
|
62
65
|
toolGuessed: !s.tool,
|
|
63
66
|
color: s.color || null,
|
|
64
67
|
joinDir: s.joinDir || '~/quilt',
|
|
@@ -120,6 +123,11 @@ function updateProfile (b) {
|
|
|
120
123
|
patch.autoModel = m || undefined
|
|
121
124
|
}
|
|
122
125
|
if ('autoFiles' in b) patch.autoFiles = b.autoFiles ? true : undefined
|
|
126
|
+
// Whether a run of the person's AI may merge a square under its line's policy (Seam, 12): never unless they say so.
|
|
127
|
+
if ('autoMerge' in b) {
|
|
128
|
+
if (!['never', 'policy'].includes(b.autoMerge)) throw httpError(400, 'Pick Never, or When the line\'s rules allow it.')
|
|
129
|
+
patch.autoMerge = b.autoMerge === 'policy' ? 'policy' : undefined
|
|
130
|
+
}
|
|
123
131
|
if ('autoCap' in b) {
|
|
124
132
|
const n = b.autoCap === '' || b.autoCap === null ? 0 : Number(b.autoCap)
|
|
125
133
|
if (!Number.isInteger(n) || n < 0 || n > 1e9) throw httpError(400, 'The daily limit is a whole number of tokens (0 for none).')
|
|
@@ -181,6 +189,7 @@ export const STATIC = {
|
|
|
181
189
|
'/changes.js': ['changes.js', 'text/javascript; charset=utf-8'],
|
|
182
190
|
'/changes-seen.js': ['changes-seen.js', 'text/javascript; charset=utf-8'],
|
|
183
191
|
'/diffview.js': ['diffview.js', 'text/javascript; charset=utf-8'],
|
|
192
|
+
'/quiltview.js': ['quiltview.js', 'text/javascript; charset=utf-8'],
|
|
184
193
|
'/boxes.js': ['boxes.js', 'text/javascript; charset=utf-8'],
|
|
185
194
|
'/releases.js': ['releases.js', 'text/javascript; charset=utf-8'],
|
|
186
195
|
'/feed-convs.js': ['feed-convs.js', 'text/javascript; charset=utf-8'],
|
|
@@ -193,7 +202,6 @@ export const STATIC = {
|
|
|
193
202
|
'/loom.css': ['loom.css', 'text/css; charset=utf-8'],
|
|
194
203
|
'/board.js': ['board.js', 'text/javascript; charset=utf-8'],
|
|
195
204
|
'/schedule.js': ['schedule.js', 'text/javascript; charset=utf-8'],
|
|
196
|
-
'/branches.js': ['branches.js', 'text/javascript; charset=utf-8'],
|
|
197
205
|
'/workspaces.js': ['workspaces.js', 'text/javascript; charset=utf-8'],
|
|
198
206
|
'/invites.js': ['invites.js', 'text/javascript; charset=utf-8'],
|
|
199
207
|
'/agent-place.js': ['agent-place.js', 'text/javascript; charset=utf-8'],
|
|
@@ -205,6 +213,8 @@ export const STATIC = {
|
|
|
205
213
|
'/preview-model.js': ['preview-model.js', 'text/javascript; charset=utf-8'],
|
|
206
214
|
'/sounds.js': ['sounds.js', 'text/javascript; charset=utf-8'],
|
|
207
215
|
'/git-rule.js': ['git-rule.js', 'text/javascript; charset=utf-8'],
|
|
216
|
+
'/seam-model.js': ['seam-model.js', 'text/javascript; charset=utf-8'],
|
|
217
|
+
'/force-words.js': ['force-words.js', 'text/javascript; charset=utf-8'],
|
|
208
218
|
'/cloud-ai.js': ['cloud-ai.js', 'text/javascript; charset=utf-8'],
|
|
209
219
|
'/cloud-ai-model.js': ['cloud-ai-model.js', 'text/javascript; charset=utf-8'],
|
|
210
220
|
'/sandbox-keys.js': ['sandbox-keys.js', 'text/javascript; charset=utf-8'],
|
|
@@ -397,8 +407,20 @@ export async function startUi ({ port = 7420, onShutdown, preview = false, repor
|
|
|
397
407
|
return { id, dir: r.run.dir, invite: r.run.invite, viewInvite: r.run.viewInvite, status: r.run.session.status(), logs: r.logs.slice(-80), workspace: readConfig(r.run.dir)?.workspace || '' }
|
|
398
408
|
}
|
|
399
409
|
const pushStatus = (id) => runs.has(id) && broadcast('session', summary(id))
|
|
410
|
+
// What the session itself changes (presence, status, access) is sent at most every STATUS_PUSH_MS,
|
|
411
|
+
// and only to a window that listens: presence alone changes several times a second in a busy room,
|
|
412
|
+
// and each full status (every task, comment and change) made hundreds of MB a minute of garbage.
|
|
413
|
+
const pushTimers = new Map()
|
|
414
|
+
const pushSoon = (id) => {
|
|
415
|
+
if (pushTimers.has(id) || !runs.has(id)) return
|
|
416
|
+
const t = setTimeout(() => { pushTimers.delete(id); if (clients.size) pushStatus(id) }, STATUS_PUSH_MS)
|
|
417
|
+
t.unref?.()
|
|
418
|
+
pushTimers.set(id, t)
|
|
419
|
+
}
|
|
400
420
|
|
|
401
|
-
async function start ({ mode, dir, tool, invite, prefer,
|
|
421
|
+
async function start ({ mode, dir, tool, invite, prefer, workspace }) {
|
|
422
|
+
// New session from GitHub went in 16's R2: new sessions are quilts (quilt-default.js), and a repository's history comes into Seam by its import.
|
|
423
|
+
if (mode === 'github') throw httpError(410, GITHUB_GONE)
|
|
402
424
|
const me = profile()
|
|
403
425
|
// Every session signs in to the relay as this computer's account.
|
|
404
426
|
const sessionPasses = accountPasses()
|
|
@@ -407,16 +429,6 @@ export async function startUi ({ port = 7420, onShutdown, preview = false, repor
|
|
|
407
429
|
const outlived = () => readAccount()
|
|
408
430
|
? httpError(409, 'Your sign-in changed while this session was starting. Start it again.')
|
|
409
431
|
: Object.assign(httpError(401, 'Sign in to Quilt first.'), { signedOut: true })
|
|
410
|
-
if (mode === 'github') {
|
|
411
|
-
// Clone first, then start a normal session on the clone.
|
|
412
|
-
const repoName = String(repo || '').split('/').pop()
|
|
413
|
-
dir = path.resolve(expandHome(dir || underJoinDir(me.joinDir, repoName || 'repo', 'That repository name is not valid.')))
|
|
414
|
-
if (runs.has(idFor(dir))) throw httpError(400, 'A session is already running in that folder.')
|
|
415
|
-
await gitops.cloneRepo({ repo, dir, branch, newBranch, base })
|
|
416
|
-
if (!current()) throw outlived()
|
|
417
|
-
mode = 'create'
|
|
418
|
-
}
|
|
419
|
-
const toolGuessed = !tool && me.toolGuessed
|
|
420
432
|
tool = tool || me.tool
|
|
421
433
|
prefer = prefer || (me.preferLocal ? 'local' : 'remote')
|
|
422
434
|
if (mode === 'join' && !dir) {
|
|
@@ -457,7 +469,6 @@ export async function startUi ({ port = 7420, onShutdown, preview = false, repor
|
|
|
457
469
|
// The account's name until the first pass names us (the same name, from the API).
|
|
458
470
|
name: me.name,
|
|
459
471
|
tool,
|
|
460
|
-
toolGuessed,
|
|
461
472
|
color: me.color,
|
|
462
473
|
shareByDefault: me.shareAgent,
|
|
463
474
|
summarizeByDefault: me.summarize,
|
|
@@ -471,11 +482,14 @@ export async function startUi ({ port = 7420, onShutdown, preview = false, repor
|
|
|
471
482
|
startName: mode === 'create' ? cleanSessionName([...path.basename(dir)].slice(0, SESSION_NAME_MAX).join('')) || '' : '',
|
|
472
483
|
// Your default for what agents may commit, set on the new room once the relay makes you its owner.
|
|
473
484
|
startAgentCommits: mode === 'create' ? me.agentCommits : '',
|
|
485
|
+
// A new session is a quilt from the start when the API says so (quilt-default.js).
|
|
486
|
+
newSession: mode === 'create',
|
|
474
487
|
workspace: workspace || '',
|
|
475
488
|
onLog: log,
|
|
476
489
|
onFatal: async (err) => {
|
|
477
490
|
log(`stopped: ${err.message}`)
|
|
478
|
-
|
|
491
|
+
// Said in the window too (app.js): the relay sent this app away (to update, ended, not let in), or it couldn't go on.
|
|
492
|
+
await stop(id, err.message)
|
|
479
493
|
// Only this sign-in's passes may sign it out: a stale session's 401 is about a token already gone.
|
|
480
494
|
if (err.signedOut && current()) await signedOut('revoked')
|
|
481
495
|
}
|
|
@@ -495,13 +509,15 @@ export async function startUi ({ port = 7420, onShutdown, preview = false, repor
|
|
|
495
509
|
}
|
|
496
510
|
runs.set(id, entry)
|
|
497
511
|
const s = entry.run.session
|
|
498
|
-
s.on('status-changed', () =>
|
|
499
|
-
s.on('access', () =>
|
|
512
|
+
s.on('status-changed', () => pushSoon(id))
|
|
513
|
+
s.on('access', () => pushSoon(id))
|
|
514
|
+
s.on('quilt', () => pushSoon(id))
|
|
515
|
+
s.on('quilt-shadow', () => pushSoon(id))
|
|
500
516
|
s.on('message', (m) => broadcast('message', { id, message: m }))
|
|
501
517
|
s.on('agent-feed', (entries) => broadcast('feed', { id, entries }))
|
|
502
518
|
s.on('file-changed', (e) => broadcast('file-changed', { id, ...e }))
|
|
503
519
|
// Presence changes (e.g. focus, recently edited files) also refresh the view.
|
|
504
|
-
s.conn.awareness.on('change', () =>
|
|
520
|
+
s.conn.awareness.on('change', () => pushSoon(id))
|
|
505
521
|
// The call (voice.js): who is in it and their call setup, sent only when it changed.
|
|
506
522
|
let voiceWas = ''
|
|
507
523
|
const pushVoice = () => {
|
|
@@ -518,7 +534,7 @@ export async function startUi ({ port = 7420, onShutdown, preview = false, repor
|
|
|
518
534
|
if (workspace && mode === 'create') {
|
|
519
535
|
// Put the new room in its workspace before anyone else connects. A failure is logged, never
|
|
520
536
|
// fatal, and the session is outside any workspace here too, as it is on the API.
|
|
521
|
-
//
|
|
537
|
+
// A new session from a folder (New session from GitHub went in 16's R2).
|
|
522
538
|
const linked = await asAccount((token) => setSessionWorkspace({ token, id: workspace, room: conn.room })).then(() => true, (err) => {
|
|
523
539
|
log(`could not add this session to its workspace: ${err.message}`)
|
|
524
540
|
forgetWorkspace(entry.run.dir)
|
|
@@ -537,12 +553,13 @@ export async function startUi ({ port = 7420, onShutdown, preview = false, repor
|
|
|
537
553
|
return summary(id)
|
|
538
554
|
}
|
|
539
555
|
|
|
540
|
-
|
|
556
|
+
/** `why`: the session stopped by itself (Session 'fatal'), not because someone left it: the window says why. */
|
|
557
|
+
async function stop (id, why = '') {
|
|
541
558
|
const r = runs.get(id)
|
|
542
559
|
if (!r) return
|
|
543
560
|
runs.delete(id)
|
|
544
561
|
await r.run.stop()
|
|
545
|
-
broadcast('stopped', { id })
|
|
562
|
+
broadcast('stopped', { id, ...(why ? { why: String(why).slice(0, 500) } : {}) })
|
|
546
563
|
}
|
|
547
564
|
|
|
548
565
|
const get = (id) => {
|
|
@@ -1032,7 +1049,7 @@ export async function startUi ({ port = 7420, onShutdown, preview = false, repor
|
|
|
1032
1049
|
'POST /api/sessions/:id/focus': (b, id) => { get(id).setFocus(b.text); return { ok: true } },
|
|
1033
1050
|
// Voice (voice.js): the call as this app sees it, and our part in it, from the app window, which does the audio.
|
|
1034
1051
|
'GET /api/sessions/:id/voice': (b, id) => get(id).voice(),
|
|
1035
|
-
'POST /api/sessions/:id/voice': (b, id) => get(id).setVoice(b.in === false ? null : { muted: b.muted === true, sig: b.sig, transcribing: b.transcribing === true }),
|
|
1052
|
+
'POST /api/sessions/:id/voice': (b, id) => get(id).setVoice(b.in === false ? null : { muted: b.muted === true, sig: b.sig, transcribing: b.transcribing === true, sharing: b.sharing === true }),
|
|
1036
1053
|
'POST /api/sessions/:id/voice/transcribe': (b, id) => get(id).setVoiceTranscribe(b.on === true),
|
|
1037
1054
|
'POST /api/sessions/:id/voice/clip': (b, id) => voiceClip(get(id), b),
|
|
1038
1055
|
'GET /api/sessions/:id/voice/transcript': (b, id, url) => get(id).voiceTranscript({ call: url.searchParams.get('call') || '' }),
|
|
@@ -1085,12 +1102,22 @@ export async function startUi ({ port = 7420, onShutdown, preview = false, repor
|
|
|
1085
1102
|
'GET /api/sessions/:id/changes': (b, id) => get(id).changes(),
|
|
1086
1103
|
'POST /api/sessions/:id/catch-up/dismiss': (b, id) => { const r = get(id).dismissCatchUp(); pushStatus(id); return r },
|
|
1087
1104
|
// One file's changes, newest first, each with its diff (the Changes panel's "What changed").
|
|
1088
|
-
|
|
1105
|
+
// In a session linked to a quilt, its History: the live changes, then the patches from Seam's log.
|
|
1106
|
+
'GET /api/sessions/:id/history': async (b, id, url) => {
|
|
1089
1107
|
const file = String(url.searchParams.get('path') || '')
|
|
1090
|
-
const
|
|
1108
|
+
const s = get(id)
|
|
1109
|
+
const linked = s.quiltLink() ? await s.fileHistory(file) : null
|
|
1110
|
+
if (linked) return { entries: linked.live, patches: linked.patches, quilt: linked.quilt, ...(linked.note ? { note: linked.note } : {}) }
|
|
1111
|
+
const entries = s.history.entries().filter((e) => e.path === file)
|
|
1091
1112
|
return { entries: queryHistory(entries, { limit: 100 }).reverse() }
|
|
1092
1113
|
},
|
|
1114
|
+
// A linked session's Blame of a file, one patch of its History, and code search (the relay reads Seam).
|
|
1115
|
+
'GET /api/sessions/:id/blame': (b, id, url) => get(id).quiltBlame(String(url.searchParams.get('path') || '')),
|
|
1116
|
+
'GET /api/sessions/:id/patch': (b, id, url) => get(id).quiltShow(String(url.searchParams.get('commit') || ''), String(url.searchParams.get('path') || '')),
|
|
1117
|
+
'GET /api/sessions/:id/search': (b, id, url) => get(id).quiltSearch(String(url.searchParams.get('q') || '')),
|
|
1093
1118
|
// Commits pushed or merged elsewhere: fetch and bring them in now (they also come in by themselves).
|
|
1119
|
+
// In a session that moved into Seam: the square panel's Commit and Merge onto main, as this member (control.js seamTool).
|
|
1120
|
+
'POST /api/sessions/:id/seam': (b, id) => squareTool(get(id), b),
|
|
1094
1121
|
'POST /api/sessions/:id/branches/sync': async (b, id) => { const r = await get(id).syncBranchNow(); pushStatus(id); return r },
|
|
1095
1122
|
'GET /api/sessions/:id/file': (b, id, url) => {
|
|
1096
1123
|
const f = get(id).readShared(url.searchParams.get('path'))
|
|
@@ -1170,14 +1197,17 @@ export async function startUi ({ port = 7420, onShutdown, preview = false, repor
|
|
|
1170
1197
|
reporter.record({ kind: 'error', name: String(b.name || 'renderer').slice(0, 80), outcome: 'error', message: String(b.message || '').slice(0, 500), context: typeof b.context === 'object' && b.context ? b.context : {} })
|
|
1171
1198
|
return { ok: true }
|
|
1172
1199
|
},
|
|
1173
|
-
'
|
|
1174
|
-
'GET /api/github/
|
|
1175
|
-
'GET /api/github/
|
|
1200
|
+
// New session from GitHub's lists, gone in 16's R2: an older window that asks is told why.
|
|
1201
|
+
'GET /api/github/status': () => { throw httpError(410, GITHUB_GONE) },
|
|
1202
|
+
'GET /api/github/repos': () => { throw httpError(410, GITHUB_GONE) },
|
|
1203
|
+
'GET /api/github/branches': () => { throw httpError(410, GITHUB_GONE) },
|
|
1176
1204
|
'POST /api/sessions/:id/commit-request': (b, id) => { const r = get(id).requestCommit(b.message, { files: Array.isArray(b.files) ? b.files : null, task: b.task || null, by: b.by ? String(b.by).slice(0, 80) : null }); pushStatus(id); return r },
|
|
1177
1205
|
// Commits a request in this folder (commit.js), and what is not committed here yet, by who made it.
|
|
1178
1206
|
'POST /api/sessions/:id/commit-session': async (b, id) => { try { return await get(id).commitSessionWork(String(b.message || ''), { mine: b.mine === true }) } finally { pushStatus(id) } },
|
|
1179
1207
|
'POST /api/sessions/:id/commit-request/commit': async (b, id) => { try { return await get(id).commitRequest(String(b.id || '')) } finally { pushStatus(id) } },
|
|
1180
1208
|
'POST /api/sessions/:id/agent-commits': async (b, id) => { try { return await get(id).setAgentCommits(b.mode) } finally { pushStatus(id) } },
|
|
1209
|
+
// "Make this a quilt" (Seam's plans 16, flag 9): the owner's button. The relay makes the session a quilt and moves it into its squares, as the owner.
|
|
1210
|
+
'POST /api/sessions/:id/quilt-make': async (b, id) => { try { return await get(id).quiltMake(process.env.SEAM_TOKEN || (readAccount() || {}).token) } finally { pushStatus(id) } },
|
|
1181
1211
|
'POST /api/sessions/:id/github-sync': async (b, id) => { try { return await get(id).setGithubSync(b.on !== false) } finally { pushStatus(id) } },
|
|
1182
1212
|
'POST /api/sessions/:id/large-to-relay': async (b, id) => { try { return await get(id).setLargeToRelay(b.on === true) } finally { pushStatus(id) } },
|
|
1183
1213
|
'POST /api/sessions/:id/github/recheck': async (b, id) => { try { return { github: await get(id).githubRecheck() } } finally { pushStatus(id) } },
|
|
@@ -1503,6 +1533,28 @@ export function underJoinDir (joinDir, name, message = INVALID_INVITE) {
|
|
|
1503
1533
|
return dir
|
|
1504
1534
|
}
|
|
1505
1535
|
|
|
1536
|
+
/**
|
|
1537
|
+
* The square panel's buttons (layout 3): seam_commit with a message, or seam_merge (a message for what it
|
|
1538
|
+
* commits first), as this member through Seam's own tools; or open_square, for a folder whose branch has
|
|
1539
|
+
* none (Session.openSquareHere). The answer as { text }, a refusal as a 400.
|
|
1540
|
+
*/
|
|
1541
|
+
export async function squareTool (s, b) {
|
|
1542
|
+
const tool = String((b && b.tool) || '')
|
|
1543
|
+
if (tool === 'open_square') {
|
|
1544
|
+
if (!s.openSquareHere) throw httpError(409, 'This session\'s code is not in Seam.')
|
|
1545
|
+
try { return { text: (await s.openSquareHere()).text } } catch (err) { throw httpError(400, err.message) }
|
|
1546
|
+
}
|
|
1547
|
+
if (tool !== 'seam_commit' && tool !== 'seam_merge') throw httpError(400, 'The app commits or merges the square, or opens one: seam_commit, seam_merge or open_square.')
|
|
1548
|
+
if (!s.seamTool) throw httpError(409, 'This session\'s code is not in Seam.')
|
|
1549
|
+
const message = b.args && typeof b.args.message === 'string' ? b.args.message.trim().slice(0, 2000) : ''
|
|
1550
|
+
if (tool === 'seam_commit' && !message) throw httpError(400, 'Say what the commit is.')
|
|
1551
|
+
const r = await s.seamTool({ tool, args: message ? { message } : {} })
|
|
1552
|
+
const text = ((r && r.content) || []).map((c) => c.text || '').join('\n').trim()
|
|
1553
|
+
if (r && r.isError) throw httpError(400, text || 'Seam refused.')
|
|
1554
|
+
// Plain words for a person (seam-model.js), not the tool's text an AI reads.
|
|
1555
|
+
return { text: seamResultWords(tool, { structuredContent: r && r.structuredContent, text }) || 'Done.' }
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1506
1558
|
function httpError (status, message) {
|
|
1507
1559
|
return Object.assign(new Error(message), { status })
|
|
1508
1560
|
}
|
package/src/update-check.js
CHANGED
|
@@ -3,7 +3,31 @@
|
|
|
3
3
|
// quilt_check_update answers for whatever image the agent names.
|
|
4
4
|
import { currentVersion, latestRelease, compareVersions } from './releases.js'
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// How to update each kind of install. Not npm: the heyquilt package there is behind the releases.
|
|
7
|
+
export const UPDATE_STEPS = 'In the desktop app: Settings → About → Update Quilt. On Linux without a desktop: curl -fsSL https://heyquilt.com/install.sh | sh'
|
|
8
|
+
export const UPDATE_HOW = 'Update Quilt (the desktop app: Settings → About → Update Quilt; on Linux without a desktop, run curl -fsSL https://heyquilt.com/install.sh | sh), then rejoin the session.'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What the relay tells an app or CLI too old for a session: only what to do. `version` is the oldest
|
|
12
|
+
* Quilt the session takes, when there is one. It fits a WebSocket close frame (123 bytes).
|
|
13
|
+
*/
|
|
14
|
+
export const needsUpdateWords = (version = '') => `This session needs ${version ? `Quilt ${version} or later` : 'a newer version of Quilt'}. Update Quilt, then join again.`
|
|
15
|
+
|
|
16
|
+
const UPDATE_REFUSAL = /\bUpdate Quilt, then join again\./
|
|
17
|
+
|
|
18
|
+
/** True when a relay's refusal says to update Quilt (needsUpdateWords). */
|
|
19
|
+
export const isUpdateRefusal = (reason) => UPDATE_REFUSAL.test(String(reason || ''))
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A refusal to update with how to, for each kind of install. `ascii`: for an HTTP status line, which
|
|
23
|
+
* older apps read as Latin-1, so an arrow would come out garbled.
|
|
24
|
+
*/
|
|
25
|
+
export function withUpdateSteps (reason, { ascii = false } = {}) {
|
|
26
|
+
const r = String(reason || '')
|
|
27
|
+
const head = isUpdateRefusal(r) ? r.slice(0, r.search(UPDATE_REFUSAL)) + 'Update Quilt, then join again.' : r
|
|
28
|
+
const out = `${head} ${UPDATE_STEPS}`
|
|
29
|
+
return ascii ? out.replace(/→/g, '>') : out
|
|
30
|
+
}
|
|
7
31
|
|
|
8
32
|
/** True when `image` looks like a Quilt version. */
|
|
9
33
|
export const validImage = (image) => /^v?\d+\.\d+\.\d+$/.test(String(image || '').trim())
|
package/src/voice.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// Voice in a session (plans/voice.md). Who is in the call, muted or not, and the call setup
|
|
1
|
+
// Voice in a session (plans/voice.md). Who is in the call, muted or sharing their screen or not, and the call setup
|
|
2
2
|
// (each pair's WebRTC offer and answer) travel in presence, so the relay needs nothing new.
|
|
3
|
-
// The audio
|
|
3
|
+
// The audio (and a shared screen) goes from app window to app window, never through the relay or this
|
|
4
4
|
// process. What people say, when the owner turned transcripts on and they agreed, is kept as
|
|
5
5
|
// lines in the session's document for everyone, agents included, to read.
|
|
6
6
|
import crypto from 'node:crypto'
|
|
@@ -50,6 +50,8 @@ export function cleanVoice (raw) {
|
|
|
50
50
|
call: typeof raw.call === 'string' && CALL.test(raw.call) ? raw.call : '',
|
|
51
51
|
// This person's app sends their speech to the transcript (they agreed, and the owner turned it on).
|
|
52
52
|
...(raw.transcribing === true ? { transcribing: true } : {}),
|
|
53
|
+
// This person shares their screen with the call.
|
|
54
|
+
...(raw.sharing === true ? { sharing: true } : {}),
|
|
53
55
|
sig
|
|
54
56
|
}
|
|
55
57
|
}
|
|
@@ -133,11 +135,14 @@ export function renderLines ({ lines, more, calls }, { tz } = {}) {
|
|
|
133
135
|
export function voiceView ({ awareness, clientID, members = [], doc }) {
|
|
134
136
|
const me = cleanVoice(awareness.getLocalState()?.voice)
|
|
135
137
|
const peers = []
|
|
138
|
+
// Everyone in the session now, in voice or not: someone who left voice but not the session hung up.
|
|
139
|
+
const here = []
|
|
136
140
|
for (const [id, s] of awareness.getStates()) {
|
|
137
141
|
if (id === clientID || !s || !s.name) continue
|
|
142
|
+
here.push(id)
|
|
138
143
|
const v = cleanVoice(s.voice)
|
|
139
144
|
if (!v) continue
|
|
140
|
-
peers.push({ id, name: s.name, color: s.color || '', muted: v.muted, since: v.since, call: v.call, ...(v.transcribing ? { transcribing: true } : {}), signal: v.sig[String(clientID)] || null })
|
|
145
|
+
peers.push({ id, name: s.name, color: s.color || '', muted: v.muted, since: v.since, call: v.call, ...(v.transcribing ? { transcribing: true } : {}), ...(v.sharing ? { sharing: true } : {}), signal: v.sig[String(clientID)] || null })
|
|
141
146
|
}
|
|
142
147
|
peers.sort((a, b) => a.since - b.since || a.id - b.id)
|
|
143
148
|
const t = doc ? transcribeSetting(doc, members) : { on: false }
|
|
@@ -149,10 +154,12 @@ export function voiceView ({ awareness, clientID, members = [], doc }) {
|
|
|
149
154
|
me: clientID,
|
|
150
155
|
in: !!me,
|
|
151
156
|
muted: !!(me && me.muted),
|
|
157
|
+
sharing: !!(me && me.sharing),
|
|
152
158
|
call,
|
|
153
159
|
since: starts.length ? Math.min(...starts) : 0,
|
|
154
160
|
transcribe: t.on,
|
|
155
161
|
peers,
|
|
162
|
+
here,
|
|
156
163
|
// The latest lines of the call going on now, as each is written down.
|
|
157
164
|
recent: doc && call ? readLines(doc, { call, limit: RECENT }).lines : [],
|
|
158
165
|
// Every call that has a transcript: when, who spoke, how many lines.
|
|
@@ -160,12 +167,12 @@ export function voiceView ({ awareness, clientID, members = [], doc }) {
|
|
|
160
167
|
}
|
|
161
168
|
}
|
|
162
169
|
|
|
163
|
-
/** Who is in voice, for status: name -> { muted, transcribing }. */
|
|
170
|
+
/** Who is in voice, for status: name -> { muted, transcribing, sharing }. */
|
|
164
171
|
export function inVoice (awareness, clientID) {
|
|
165
172
|
const out = new Map()
|
|
166
173
|
for (const [id, s] of awareness.getStates()) {
|
|
167
174
|
const v = s && s.name ? cleanVoice(s.voice) : null
|
|
168
|
-
if (v) out.set(s.name, { muted: v.muted, ...(v.transcribing ? { transcribing: true } : {}), ...(id === clientID ? { me: true } : {}) })
|
|
175
|
+
if (v) out.set(s.name, { muted: v.muted, ...(v.transcribing ? { transcribing: true } : {}), ...(v.sharing ? { sharing: true } : {}), ...(id === clientID ? { me: true } : {}) })
|
|
169
176
|
}
|
|
170
177
|
return out
|
|
171
178
|
}
|
|
@@ -179,6 +186,6 @@ export function voiceStatusLines (people, transcribe) {
|
|
|
179
186
|
if (!people.length) return []
|
|
180
187
|
return [
|
|
181
188
|
`## In a voice call${transcribe ? ' (transcribed: quilt_voice_transcript reads it)' : ' (not transcribed: ask in chat what was decided)'}`,
|
|
182
|
-
...people.map((p) => `- ${p.name}${p.muted ? ' (muted)' : ''}${transcribe && !p.transcribing ? ' (not transcribed)' : ''}`)
|
|
189
|
+
...people.map((p) => `- ${p.name}${p.muted ? ' (muted)' : ''}${p.sharing ? ' (sharing their screen)' : ''}${transcribe && !p.transcribing ? ' (not transcribed)' : ''}`)
|
|
183
190
|
]
|
|
184
191
|
}
|
package/src/git.js
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
// Git for starting a session from GitHub: checking names, listing repos and
|
|
2
|
-
// branches, cloning. Quilt never commits, pulls or pushes. Wraps the `git`
|
|
3
|
-
// and `gh` CLIs; arguments are always passed as a list, never through a shell.
|
|
4
|
-
import fs from 'node:fs'
|
|
5
|
-
import path from 'node:path'
|
|
6
|
-
import { execFile } from 'node:child_process'
|
|
7
|
-
|
|
8
|
-
// QUILT_GH lets tests stand in a fake `gh`.
|
|
9
|
-
const GH = () => process.env.QUILT_GH || 'gh'
|
|
10
|
-
const REPO_RE = /^[\w.-]+\/[\w.-]+$/
|
|
11
|
-
// Never stop to ask for a password or open an editor.
|
|
12
|
-
const ENV = { GIT_TERMINAL_PROMPT: '0', GH_PROMPT_DISABLED: '1', GIT_EDITOR: 'true', GH_NO_UPDATE_NOTIFIER: '1' }
|
|
13
|
-
|
|
14
|
-
/** Runs a command; resolves to stdout, or rejects with a short message from stderr. */
|
|
15
|
-
function run (cmd, args, { cwd, allowFail = false } = {}) {
|
|
16
|
-
return new Promise((resolve, reject) => {
|
|
17
|
-
execFile(cmd, args, { cwd, env: { ...process.env, ...ENV }, maxBuffer: 32 * 1024 * 1024, timeout: 5 * 60 * 1000 }, (err, stdout, stderr) => {
|
|
18
|
-
if (!err) return resolve(stdout)
|
|
19
|
-
if (allowFail) return resolve(null)
|
|
20
|
-
if (err.code === 'ENOENT') return reject(new Error(cmd === GH() ? 'The GitHub CLI (gh) is not installed.' : 'git is not installed.'))
|
|
21
|
-
const e = new Error(shortError(stderr || stdout || err.message))
|
|
22
|
-
e.stderr = String(stderr || '')
|
|
23
|
-
e.exitCode = err.code
|
|
24
|
-
reject(e)
|
|
25
|
-
})
|
|
26
|
-
})
|
|
27
|
-
}
|
|
28
|
-
const git = (dir, args, opts) => run('git', args, { cwd: dir, ...opts })
|
|
29
|
-
const gh = (args, opts) => run(GH(), args, opts)
|
|
30
|
-
|
|
31
|
-
/** The first useful line of a CLI error, without "fatal:"/"error:" noise. */
|
|
32
|
-
function shortError (text) {
|
|
33
|
-
const lines = String(text).split('\n').map((l) => l.trim()).filter(Boolean)
|
|
34
|
-
const line = lines.find((l) => /^(fatal|error|remote|GraphQL|HTTP)/i.test(l)) || lines[0] || 'Something went wrong.'
|
|
35
|
-
return line.replace(/^(fatal|error):\s*/i, '').replace(/^remote:\s*/i, '').slice(0, 300)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Throws unless `name` is a valid branch name. */
|
|
39
|
-
export async function checkBranchName (name) {
|
|
40
|
-
name = String(name || '').trim()
|
|
41
|
-
if (!name) throw new Error('Enter a branch name.')
|
|
42
|
-
// Not `--branch`: it expands @{-N} using the current folder's repo, so the
|
|
43
|
-
// answer would depend on where quilt happens to run.
|
|
44
|
-
if (name.startsWith('-') || name === 'HEAD' || (await run('git', ['check-ref-format', `refs/heads/${name}`], { allowFail: true })) === null) {
|
|
45
|
-
throw new Error(`"${name}" isn't a valid branch name.`)
|
|
46
|
-
}
|
|
47
|
-
return name
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function checkRepo (repo) {
|
|
51
|
-
repo = String(repo || '').trim()
|
|
52
|
-
if (!REPO_RE.test(repo) || repo.split('/').some((p) => /^\.+$/.test(p))) throw new Error('Pick a repository (owner/name).')
|
|
53
|
-
return repo
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// --------------------------------------------------------------- GitHub --
|
|
57
|
-
|
|
58
|
-
/** Is gh installed and signed in? { installed, authenticated, user, message } */
|
|
59
|
-
export async function ghStatus () {
|
|
60
|
-
const probe = (args) => new Promise((resolve) => {
|
|
61
|
-
execFile(GH(), args, { env: { ...process.env, ...ENV }, timeout: 20000 }, (err, stdout, stderr) => resolve({ err, text: `${stdout}\n${stderr}` }))
|
|
62
|
-
})
|
|
63
|
-
const version = await probe(['--version'])
|
|
64
|
-
if (version.err && version.err.code === 'ENOENT') return { installed: false, authenticated: false, user: null, message: 'Quilt can\'t list your GitHub repositories on this computer: start from a folder (a clone) instead.' }
|
|
65
|
-
const out = await probe(['auth', 'status', '--hostname', 'github.com'])
|
|
66
|
-
if (out.err) return { installed: true, authenticated: false, user: null, message: 'Quilt can\'t list your GitHub repositories on this computer: start from a folder (a clone) instead.' }
|
|
67
|
-
const user = (out.text.match(/account (\S+)/) || out.text.match(/as (\S+)/) || [])[1] || null
|
|
68
|
-
return { installed: true, authenticated: true, user, message: null }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Your repos, plus your organizations' repos, most recently updated first. */
|
|
72
|
-
export async function listRepos ({ limit = 100 } = {}) {
|
|
73
|
-
limit = Math.max(1, Math.min(1000, Number(limit) || 100))
|
|
74
|
-
const fields = ['--json', 'nameWithOwner,description,updatedAt,defaultBranchRef']
|
|
75
|
-
const parse = (out) => { try { return JSON.parse(out || '[]') } catch { return [] } }
|
|
76
|
-
const mine = parse(await gh(['repo', 'list', ...fields, '--limit', String(limit)]))
|
|
77
|
-
const orgs = String(await gh(['api', 'user/orgs', '--jq', '.[].login'], { allowFail: true }) || '').split('\n').filter(Boolean).slice(0, 10)
|
|
78
|
-
const theirs = await Promise.all(orgs.map((o) => gh(['repo', 'list', o, ...fields, '--limit', String(Math.min(limit, 100))], { allowFail: true }).then(parse)))
|
|
79
|
-
const seen = new Set()
|
|
80
|
-
return [...mine, ...theirs.flat()]
|
|
81
|
-
.filter((r) => r && r.nameWithOwner && !seen.has(r.nameWithOwner) && seen.add(r.nameWithOwner))
|
|
82
|
-
.sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')))
|
|
83
|
-
.map((r) => ({ name: r.nameWithOwner, description: r.description || '', updatedAt: r.updatedAt || null, defaultBranch: r.defaultBranchRef?.name || null }))
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Branch names of a GitHub repo, and which one is the default. */
|
|
87
|
-
export async function listBranches (repo) {
|
|
88
|
-
repo = checkRepo(repo)
|
|
89
|
-
const [names, def] = await Promise.all([
|
|
90
|
-
gh(['api', `repos/${repo}/branches`, '--paginate', '--jq', '.[].name']),
|
|
91
|
-
gh(['api', `repos/${repo}`, '--jq', '.default_branch'])
|
|
92
|
-
])
|
|
93
|
-
const defaultBranch = def.trim() || null
|
|
94
|
-
const branches = names.split('\n').map((s) => s.trim()).filter(Boolean)
|
|
95
|
-
// Default branch first, then alphabetical.
|
|
96
|
-
branches.sort((a, b) => (a === defaultBranch ? -1 : b === defaultBranch ? 1 : a.localeCompare(b)))
|
|
97
|
-
return { branches, defaultBranch }
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function defaultBranchOf (dir) {
|
|
101
|
-
const head = await git(dir, ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], { allowFail: true })
|
|
102
|
-
if (head && head.trim()) return head.trim().replace(/^origin\//, '')
|
|
103
|
-
for (const b of ['main', 'master']) {
|
|
104
|
-
if ((await git(dir, ['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${b}`], { allowFail: true })) !== null) return b
|
|
105
|
-
}
|
|
106
|
-
return null
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Clones `repo` into `dir` (which must be new or empty), then checks out
|
|
111
|
-
* `branch`, or creates `newBranch` from origin/`base` (default: the default branch).
|
|
112
|
-
*/
|
|
113
|
-
export async function cloneRepo ({ repo, dir, branch, newBranch, base }) {
|
|
114
|
-
repo = checkRepo(repo)
|
|
115
|
-
if (!dir) throw new Error('Choose a folder to clone into.')
|
|
116
|
-
dir = path.resolve(dir)
|
|
117
|
-
if (branch) branch = await checkBranchName(branch)
|
|
118
|
-
if (newBranch) newBranch = await checkBranchName(newBranch)
|
|
119
|
-
if (base) base = await checkBranchName(base)
|
|
120
|
-
const existed = fs.existsSync(dir)
|
|
121
|
-
if (existed && fs.readdirSync(dir).some((f) => f !== '.DS_Store')) {
|
|
122
|
-
throw new Error(`${path.basename(dir)} already has files in it. Pick an empty or new folder.`)
|
|
123
|
-
}
|
|
124
|
-
fs.mkdirSync(path.dirname(dir), { recursive: true })
|
|
125
|
-
try {
|
|
126
|
-
await gh(['repo', 'clone', repo, dir])
|
|
127
|
-
const has = async (b) => (await git(dir, ['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${b}`], { allowFail: true })) !== null
|
|
128
|
-
if (newBranch) {
|
|
129
|
-
base = base || await defaultBranchOf(dir)
|
|
130
|
-
if (!base || !(await has(base))) throw new Error(`There's no branch named ${base} on GitHub to start from.`)
|
|
131
|
-
if (await has(newBranch)) throw new Error(`A branch named ${newBranch} already exists. Use it instead, or pick another name.`)
|
|
132
|
-
await git(dir, ['checkout', '-q', '--no-track', '-b', newBranch, `origin/${base}`])
|
|
133
|
-
} else if (branch) {
|
|
134
|
-
const current = (await git(dir, ['branch', '--show-current'])).trim()
|
|
135
|
-
if (branch !== current) {
|
|
136
|
-
if (!(await has(branch))) throw new Error(`There's no branch named ${branch} on GitHub.`)
|
|
137
|
-
await git(dir, ['checkout', '-q', '-b', branch, '--track', `origin/${branch}`])
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return { dir, branch: (await git(dir, ['branch', '--show-current'])).trim() }
|
|
141
|
-
} catch (err) {
|
|
142
|
-
// Only clean up what we made.
|
|
143
|
-
if (!existed) fs.rmSync(dir, { recursive: true, force: true })
|
|
144
|
-
else for (const f of fs.readdirSync(dir)) if (f !== '.DS_Store') fs.rmSync(path.join(dir, f), { recursive: true, force: true })
|
|
145
|
-
throw err
|
|
146
|
-
}
|
|
147
|
-
}
|
package/src/quilts/AGENTS.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
The rules for this folder are in `seam/AGENTS.md`: seam is source control for more than Quilt.
|
package/src/quilts/CLAUDE.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@../../seam/AGENTS.md
|