@heyquilt/seam 0.27.0 → 0.131.0
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/bin/seam.js +118 -17
- package/lib/quilts/agent-tokens.js +148 -0
- package/lib/quilts/archiver.js +241 -0
- package/lib/quilts/attest.js +212 -0
- package/lib/quilts/bisect-run.js +3 -2
- package/lib/quilts/block.js +497 -55
- package/lib/quilts/cas.js +78 -0
- package/lib/quilts/cbor.js +2 -0
- package/lib/quilts/claims.js +409 -0
- package/lib/quilts/cli.js +612 -69
- package/lib/quilts/client.js +306 -19
- package/lib/quilts/conflict-format.js +266 -0
- package/lib/quilts/daemon.js +174 -0
- package/lib/quilts/describe.js +296 -38
- package/lib/quilts/dict.js +34 -0
- package/lib/quilts/exit-archive.js +184 -0
- package/lib/quilts/export-format.js +52 -0
- package/lib/quilts/export-git.js +27 -3
- package/lib/quilts/export-lfs.js +52 -0
- package/lib/quilts/export-sign.js +49 -0
- package/lib/quilts/fastcdc.js +186 -0
- package/lib/quilts/hash.js +17 -2
- package/lib/quilts/ignored.js +46 -0
- package/lib/quilts/import-git.js +54 -2
- package/lib/quilts/journal.js +105 -0
- package/lib/quilts/limits.js +197 -0
- package/lib/quilts/linepolicy.js +109 -0
- package/lib/quilts/pack.js +146 -18
- package/lib/quilts/persona.js +70 -0
- package/lib/quilts/records.js +7 -3
- package/lib/quilts/recover.js +37 -0
- package/lib/quilts/runner.js +79 -12
- package/lib/quilts/sealed.js +231 -0
- package/lib/quilts/secrets.js +86 -0
- package/lib/quilts/signing.js +7 -3
- package/lib/quilts/tarball.js +107 -0
- package/lib/quilts/textdiff.js +7 -5
- package/lib/quilts/tools.js +404 -83
- package/lib/quilts/tree.js +351 -0
- package/lib/quilts/want.js +51 -0
- package/lib/quilts/webhook.js +116 -0
- package/package.json +3 -2
package/lib/quilts/cli.js
CHANGED
|
@@ -6,15 +6,22 @@ import path from 'node:path'
|
|
|
6
6
|
import fs from 'node:fs'
|
|
7
7
|
import os from 'node:os'
|
|
8
8
|
import { quiltClient } from './client.js'
|
|
9
|
-
import { runChecks } from './runner.js'
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
9
|
+
import { runChecks, readRunnerConfig, listenForWork } from './runner.js'
|
|
10
|
+
import { startDaemon, askDaemon } from './daemon.js'
|
|
11
|
+
import { rebuild as rebuildBlock, conflictStyle } from './block.js'
|
|
12
|
+
import { describeRebuild, describeOpened } from './describe.js'
|
|
13
|
+
import { findBlock, checkout, status as blockStatus, commitFolder, amendFolder, syncTo, localDiff, saveBlock, pull, toLineTip, restore, reset, clean, localChanges, bytesOf, livePatch } from './block.js'
|
|
14
|
+
import { describeLog, describeShow, describeDiff, describeBlame, describeStatus, describeMerge, describeWrite, describeSquare, describeSquareStatus, describeEvents, describeTag, describeRelease, describeImport, describeImportRecord, describeRestore, describeReset, describeClean, describeLive, describeClaims, describeHandoff, describeFinished, describeResolutions, describeLiveSet, describeLiveJoin, describeMergeBase, describeStack, describeStackDiff, describeGrep, describeBisect, describeCheck, describeReviews, describeInterdiff, describeReviewed, describeSquareCheck, describePolicy, describeKeys, describeTokens, describeNewToken, describeBlock, describeBlocks, describeProtections, describeTeams, describeAccess, describeRedacted, describeRedactions, describeMetrics, describeSquarePolicy, describePolicyVersions, describeOrg, describeOrgs, describeOrgLog, describeOrgTeams, describeCheckRuns, describeRerun, describeAncestor, describeContains, describePipeline, describeWatches, describeSearch, describePublicReads } from './describe.js'
|
|
15
|
+
import { verifyQuilt, describeVerify } from './attest.js'
|
|
16
|
+
import { confirmWithLocalKey } from './linepolicy.js'
|
|
17
|
+
import { describeAgentToken, describeAgentTokens } from './agent-tokens.js'
|
|
18
|
+
import { codecFor, catBytes, startSealed, startSealedIfNew, grantSealed, rotateSealed, ensureSealingKey, describeSeal } from './sealed.js'
|
|
12
19
|
import { signWithLocalKey, localKey, makeLocalKey, signingKeyFile } from './signing.js'
|
|
13
20
|
|
|
14
21
|
export const API_URL = 'https://api.seam.heyquilt.com'
|
|
15
22
|
|
|
16
23
|
/** The verbs `seam` answers. */
|
|
17
|
-
export const SEAM_VERBS = new Set(['quilts', 'square', 'checkout', 'status', 'commit', 'log', 'show', 'diff', 'merge', 'rebase', 'revert', 'cherry-pick', 'blame', 'resolve', 'cat', 'ls', 'import', 'export', 'events', 'tag', 'release', 'line', 'restore', 'reset', 'clean', 'live', 'bisect', 'grep', 'describe', 'merge-base', 'claim', 'unclaim', 'check', 'checks', 'resolutions', 'review', 'sign', 'key', 'token', 'runner'])
|
|
24
|
+
export const SEAM_VERBS = new Set(['quilts', 'square', 'checkout', 'status', 'commit', 'log', 'show', 'diff', 'merge', 'rebase', 'revert', 'cherry-pick', 'blame', 'resolve', 'cat', 'ls', 'import', 'export', 'events', 'tag', 'release', 'line', 'restore', 'reset', 'clean', 'live', 'bisect', 'grep', 'describe', 'merge-base', 'stack', 'claim', 'unclaim', 'check', 'checks', 'resolutions', 'review', 'sign', 'key', 'token', 'block', 'runner', 'protect', 'team', 'verify', 'redact', 'metrics', 'seal', 'policy', 'org', 'watch', 'search', 'contains', 'daemon', 'clone', 'pull'])
|
|
18
25
|
|
|
19
26
|
/** `ci=agent:ci,lint=agent:a+agent:b` → { ci: ['agent:ci'], lint: ['agent:a', 'agent:b'] }; `none` clears them. */
|
|
20
27
|
export function parseRunners (text) {
|
|
@@ -41,8 +48,42 @@ export function parseTiers (text) {
|
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
/** `test/**,**\/*.test.js` → a list; `none` clears it. */
|
|
51
|
+
/** --pre-merge-checks on|off */
|
|
52
|
+
export function parseOnOff (text, flag) {
|
|
53
|
+
if (text === 'on' || text === 'true') return true
|
|
54
|
+
if (text === 'off' || text === 'false') return false
|
|
55
|
+
throw new Error(`${flag}: on or off (got ${text})`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `db_branch=supabase:abcd,infra=v2` → { db_branch: 'supabase:abcd', infra: 'v2' } (square open --stack). */
|
|
59
|
+
export function parseStackFlag (text) {
|
|
60
|
+
const out = {}
|
|
61
|
+
for (const part of String(text).split(',').map((s) => s.trim()).filter(Boolean)) {
|
|
62
|
+
const i = part.indexOf('=')
|
|
63
|
+
if (i < 1) throw new Error(`--stack: key=value pairs separated by commas, e.g. db_branch=supabase:abcd (got ${part})`)
|
|
64
|
+
out[part.slice(0, i).trim()] = part.slice(i + 1).trim()
|
|
65
|
+
}
|
|
66
|
+
return out
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** `10/120` → { patches: 10, seconds: 120 }; `none` → null (a batch per merge). */
|
|
70
|
+
export const parseBatch = (text) => {
|
|
71
|
+
if (text === 'none') return null
|
|
72
|
+
const m = /^(\d+)\/(\d+)$/.exec(text)
|
|
73
|
+
if (!m) throw new Error(`--batch: <patches>/<seconds>, e.g. 10/120, or none (got ${text})`)
|
|
74
|
+
return { patches: Number(m[1]), seconds: Number(m[2]) }
|
|
75
|
+
}
|
|
44
76
|
export const parseGlobs = (text) => text === 'none' ? null : text.split(',').map((x) => x.trim()).filter(Boolean)
|
|
45
|
-
|
|
77
|
+
// seam token agent new --checks [true|false] --rate commit_rate=10,merge_rate=5 (11.5, 11.9).
|
|
78
|
+
export function agentTokenExtras (flags) {
|
|
79
|
+
const out = {}
|
|
80
|
+
const c = flags['--checks']
|
|
81
|
+
if (c != null) out.checks = c === true || c === 'true' ? true : c === 'false' ? false : c
|
|
82
|
+
if (flags['--rate']) out.rate = Object.fromEntries(String(flags['--rate']).split(',').map((x) => x.trim()).filter(Boolean).map((x) => { const [k, v] = x.split('='); return [k.trim(), Number(v)] }))
|
|
83
|
+
return out
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const VALUE_FLAGS = new Set(['--authors', '--debounce', '-m', '--installation', '--quilts', '--message', '-n', '--max-count', '--into', '--onto', '--on-conflict', '--quilt', '--square', '--dir', '--line', '--task', '--level', '--title', '--slug', '--format', '--expect-tip', '--side', '--file', '--batch', '--to', '--note', '--after', '--kinds', '--webhook', '--secret', '--branch', '--from', '-S', '--run', '--marks', '--patch', '--on', '--path', '--checks', '--name', '--public-key', '--interval', '--credential', '--as', '--runners', '--run-key', '--state', '--tier', '--tiers', '--tests', '--ci-config', '--commit', '--check', '--log-url', '--at', '--human-only', '--keys', '--reason', '--without', '--allow', '--rules', '--org', '--role', '--agent', '--paths', '--squares', '--hours', '--pre-merge-checks', '--by', '--mode', '--machine', '--add', '--remove', '--exclude', '--max-fetch-bytes', '--expires', '--out', '--auto-combine', '--needs-stack', '--monotonic-stack', '--migrations-in-order', '--rule', '--bisect-width', '--classes', '--min-lines', '-L', '--ignore-rev', '--reverse', '--grep', '-G', '--author', '--on-behalf-of', '--since', '--until', '--group-by', '--cursor', '-U', '--unified', '--renames', '--config', '--only', '--listen', '--idle-exit', '--wait', '--days', '--with', '--stack', '--scope', '--suggest', '--to-line', '--approval-survives', '--stack-defaults', '--stack-keys', '--lfs-url', '--lfs-threshold', '--offset', '--length', '--rate', '--before'])
|
|
46
87
|
|
|
47
88
|
export function parse (argv) {
|
|
48
89
|
const pos = []; const flags = {}; const paths = []
|
|
@@ -53,7 +94,8 @@ export function parse (argv) {
|
|
|
53
94
|
if (a === '--') { rest = true; continue }
|
|
54
95
|
const eq = a.match(/^(--[\w-]+)=(.*)$/)
|
|
55
96
|
if (eq) { flags[eq[1]] = eq[2]; continue }
|
|
56
|
-
|
|
97
|
+
// A value flag never takes the next flag as its value (`protect add … --exclude --quilt q`): it is then on.
|
|
98
|
+
if (VALUE_FLAGS.has(a)) { const v = argv[i + 1]; if (v === undefined || /^--[\w-]/.test(v)) flags[a] = true; else { flags[a] = v; i++ } continue }
|
|
57
99
|
if (/^-n\d+$/.test(a)) { flags['-n'] = a.slice(2); continue }
|
|
58
100
|
if (a.startsWith('-') && a.length > 1) { flags[a] = true; continue }
|
|
59
101
|
pos.push(a)
|
|
@@ -70,11 +112,15 @@ export function connect (block, token) {
|
|
|
70
112
|
}
|
|
71
113
|
|
|
72
114
|
/** Runs one verb; returns the text to print (or JSON with --json). `token` is the signed-in person's, when there is one. */
|
|
73
|
-
export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, out = (s) => process.stdout.write(s.endsWith('\n') ? s : s + '\n') } = {}) {
|
|
115
|
+
export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, out = (s) => process.stdout.write(s.endsWith('\n') ? s : s + '\n'), stop = () => false } = {}) {
|
|
74
116
|
const { pos, flags, paths } = parse(argv)
|
|
75
117
|
const found = findBlock(cwd)
|
|
76
118
|
const block = found?.block
|
|
77
|
-
|
|
119
|
+
// Reading an archive needs no server and no sign-in (14, exit path).
|
|
120
|
+
const offline = cmd === 'export' && (pos[0] === 'check' || pos[0] === 'extract' || (pos[0] === 'git' && !!flags['--from']))
|
|
121
|
+
const { client, api } = offline ? { client: null, api: null } : connect(block, token)
|
|
122
|
+
// A sealed quilt's block opens and seals its files with the key this computer holds (11.11).
|
|
123
|
+
if (!offline && block?.sealed && !block._codec) block._codec = await codecFor(client, block.quilt)
|
|
78
124
|
const quilt = flags['--quilt'] || block?.quilt
|
|
79
125
|
const needQuilt = () => { if (!quilt) throw new Error('Name the quilt with --quilt <slug or id> (or run this inside a block folder).'); return quilt }
|
|
80
126
|
const needSquare = () => { const s = flags['--square'] || block?.square; if (!s) throw new Error('No square here: run this in a block folder, or pass --square sq:<id>.'); return s }
|
|
@@ -84,8 +130,65 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
84
130
|
|
|
85
131
|
switch (cmd) {
|
|
86
132
|
case 'quilts': {
|
|
87
|
-
if (pos[0] === 'new') {
|
|
88
|
-
|
|
133
|
+
if (pos[0] === 'new') {
|
|
134
|
+
const q = await client.createQuilt({ slug: pos[1], name: flags['--title'], ...(flags['--sealed'] ? { sealed: true } : {}), ...(flags['--org'] ? { org: flags['--org'] } : {}) })
|
|
135
|
+
if (!flags['--sealed']) return print(q, `Made quilt ${q.slug} (${q.id}).`)
|
|
136
|
+
// Its key is made here and wrapped for this computer; the server never holds it.
|
|
137
|
+
await startSealed(client, q.id, q.owner)
|
|
138
|
+
return print(q, `Made sealed quilt ${q.slug} (${q.id}). Its key is on this computer only: give it to other members with seam seal grant <member id>.`)
|
|
139
|
+
}
|
|
140
|
+
if (pos[0] === 'agents-make-lines') {
|
|
141
|
+
if (!['on', 'off'].includes(pos[1])) throw new Error('seam quilts agents-make-lines on|off --quilt <q>')
|
|
142
|
+
const r = await client.setAgentsMakeLines(needQuilt(), pos[1] === 'on')
|
|
143
|
+
return print(r, r.agents_make_lines ? `Agents may make lines, tags and releases in ${r.quilt}.` : `Only people make lines, tags and releases in ${r.quilt}.`)
|
|
144
|
+
}
|
|
145
|
+
if (pos[0] === 'move') {
|
|
146
|
+
if (!flags['--org']) throw new Error('seam quilts move --quilt <q> --org <org>')
|
|
147
|
+
const r = await client.setQuiltOwner(needQuilt(), flags['--org'])
|
|
148
|
+
return print(r, `${r.quilt} now belongs to ${r.org}.`)
|
|
149
|
+
}
|
|
150
|
+
if (pos[0] === 'sealed') {
|
|
151
|
+
if (!['on', 'off'].includes(pos[1])) throw new Error('seam quilts sealed on|off --quilt <q>')
|
|
152
|
+
const r = await client.setSealed(needQuilt(), pos[1] === 'on')
|
|
153
|
+
if (!r.sealed) return print(r, `${r.quilt} is not sealed: the server reads its files.`)
|
|
154
|
+
// As for seam quilts new --sealed: the key is made here and wrapped for this computer.
|
|
155
|
+
await startSealedIfNew(client, r.quilt_id)
|
|
156
|
+
return print(r, `${r.quilt} is sealed. Its key is on this computer only: give it to other members with seam seal grant <member id>.`)
|
|
157
|
+
}
|
|
158
|
+
if (pos[0] === 'public') {
|
|
159
|
+
if (!['on', 'off'].includes(pos[1])) throw new Error('seam quilts public on|off --quilt <q>')
|
|
160
|
+
const r = await client.setPublic(needQuilt(), pos[1] === 'on')
|
|
161
|
+
return print(r, r.public ? `${r.quilt} is public: anyone can read it, signed in or not. Share ${r.page || `${api}/v1/quilts/${r.quilt_id}`}` : `${r.quilt} is private again.`)
|
|
162
|
+
}
|
|
163
|
+
if (pos[0] === 'infra-paths') {
|
|
164
|
+
// seam quilts infra-paths <glob,glob> | default | none: what seam stack derives infra from.
|
|
165
|
+
if (!pos[1]) throw new Error('seam quilts infra-paths <glob,glob>|default|none --quilt <q>')
|
|
166
|
+
const r = await client.setInfraPaths(needQuilt(), pos[1] === 'none' ? null : pos[1] === 'default' ? 'default' : parseGlobs(pos[1]))
|
|
167
|
+
return print(r, r.infra_paths ? `${r.quilt} derives infra from ${r.infra_paths.join(', ')}.` : `${r.quilt} derives no infra.`)
|
|
168
|
+
}
|
|
169
|
+
if (pos[0] === 'public-reads') {
|
|
170
|
+
// seam quilts public-reads: what readers with no sign-in fetched, per hashed address and 10 minutes (11.10).
|
|
171
|
+
const r = await client.publicReads(needQuilt())
|
|
172
|
+
return print(r, describePublicReads(r))
|
|
173
|
+
}
|
|
174
|
+
if (pos[0] === 'owner-idle-hours') {
|
|
175
|
+
// seam quilts owner-idle-hours <1..24> | default: how long an owner claim holds after its square's last write there.
|
|
176
|
+
if (!pos[1]) throw new Error('seam quilts owner-idle-hours <1..24>|default --quilt <q>')
|
|
177
|
+
const r = await client.setOwnerIdleHours(needQuilt(), pos[1] === 'default' ? null : Number(pos[1]))
|
|
178
|
+
return print(r, `${r.quilt}'s owner claims let go after ${r.owner_idle_hours ?? 2} quiet hours.`)
|
|
179
|
+
}
|
|
180
|
+
if (pos[0] === 'migrations-dir') {
|
|
181
|
+
// seam quilts migrations-dir <folder> | none | default: where seam stack derives db_migration from.
|
|
182
|
+
if (!pos[1]) throw new Error('seam quilts migrations-dir <folder>|none|default --quilt <q>')
|
|
183
|
+
const r = await client.setMigrationsDir(needQuilt(), pos[1] === 'none' ? '' : pos[1] === 'default' ? null : pos[1])
|
|
184
|
+
return print(r, r.migrations_dir === null ? `${r.quilt} derives db_migration from supabase/migrations.` : r.migrations_dir ? `${r.quilt} derives db_migration from ${r.migrations_dir}.` : `${r.quilt} derives no db_migration.`)
|
|
185
|
+
}
|
|
186
|
+
if (pos[0] === 'secret-scanning') {
|
|
187
|
+
if (!pos[1]) { const q = await client.quilt(needQuilt()); return print({ secret_scanning: q.secret_scanning }, `Secret scanning on ${q.slug}: ${q.secret_scanning}.`) }
|
|
188
|
+
const r = await client.setSecretScanning(needQuilt(), pos[1])
|
|
189
|
+
return print(r, `Secret scanning on ${r.quilt}: ${r.secret_scanning}.`)
|
|
190
|
+
}
|
|
191
|
+
if (pos[0] === 'add') { const r = await client.addMember(needQuilt(), { ...(String(pos[1] || '').includes('@') ? { email: pos[1] } : { member_id: pos[1] }), level: flags['--level'] || 'write' }); return print(r, r.invited ? `Invited ${r.email} (${r.level}): ${r.emailed ? 'Seam emailed them a link' : `send them ${r.sign_up}`}. They are in the quilt from their first sign-in.` : `${r.member_id}: ${r.level || 'removed'}`) }
|
|
89
192
|
if (pos[0] === 'show') { const q = await client.quilt(pos[1] || needQuilt()); return print(q, `${q.slug} (${q.id})\n${q.lines.map((l) => ` ${l.name}: ${l.patch_count} patches, tip ${l.tip ? l.tip.slice(0, 12) : '(empty)'}`).join('\n')}`) }
|
|
90
193
|
const list = await client.quilts()
|
|
91
194
|
return print(list, list.length ? list.map((q) => `${q.slug} ${q.id} (${q.level})`).join('\n') : 'No quilts yet.')
|
|
@@ -114,12 +217,18 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
114
217
|
const tiers = flags['--tiers'] === undefined ? undefined : parseTiers(String(flags['--tiers']))
|
|
115
218
|
const tests = flags['--tests'] === undefined ? undefined : parseGlobs(String(flags['--tests']))
|
|
116
219
|
const ciConfig = flags['--ci-config'] === undefined ? undefined : parseGlobs(String(flags['--ci-config']))
|
|
117
|
-
|
|
220
|
+
// --human-only "supabase/migrations/**": paths agents read and never change on this line
|
|
221
|
+
const humanOnly = flags['--human-only'] === undefined ? undefined : parseGlobs(String(flags['--human-only']))
|
|
222
|
+
// --only deploy=src/**+!src/vault/**: the paths each check is about (it runs only when they change)
|
|
223
|
+
const only = flags['--only'] === undefined ? undefined : parseRunners(String(flags['--only']))
|
|
224
|
+
// --rules <file.json>: { rules, classes?, mode? }, what each kind of change needs (13)
|
|
225
|
+
const ruleFile = flags['--rules'] ? JSON.parse(fs.readFileSync(path.resolve(cwd, flags['--rules']), 'utf8')) : null
|
|
226
|
+
const extra = { ...(tiers !== undefined ? { tiers } : {}), ...(tests !== undefined ? { tests } : {}), ...(ciConfig !== undefined ? { ci_config: ciConfig } : {}), ...(humanOnly !== undefined ? { human_only: humanOnly } : {}), ...(only !== undefined ? { only } : {}), ...(ruleFile ? (Array.isArray(ruleFile) ? { rules: ruleFile } : ruleFile) : {}), ...(flags['--pre-merge-checks'] !== undefined ? { pre_merge_checks: parseOnOff(String(flags['--pre-merge-checks']), '--pre-merge-checks') } : {}), ...(flags['--auto-combine'] !== undefined ? { auto_combine: flags['--auto-combine'] } : {}), ...(flags['--approval-survives'] !== undefined ? { approval_survives: flags['--approval-survives'] === 'default' ? null : String(flags['--approval-survives']) } : {}), ...(flags['--batch'] !== undefined ? { batch: parseBatch(String(flags['--batch'])) } : {}), ...(flags['--bisect-width'] !== undefined ? { bisect_width: Number(flags['--bisect-width']) } : {}), ...(flags['--needs-stack'] !== undefined ? { needs_stack: ['none', true].includes(flags['--needs-stack']) ? null : String(flags['--needs-stack']).split(',').map((s) => s.trim()).filter(Boolean) } : {}), ...(flags['--monotonic-stack'] !== undefined ? { monotonic_stack: ['none', true].includes(flags['--monotonic-stack']) ? null : String(flags['--monotonic-stack']).split(',').map((s) => s.trim()).filter(Boolean) } : {}), ...(flags['--migrations-in-order'] !== undefined ? { migrations_in_order: parseOnOff(String(flags['--migrations-in-order']), '--migrations-in-order') } : {}), ...(flags['--stack-defaults'] !== undefined ? { stack_defaults: ['none', true].includes(flags['--stack-defaults']) ? null : parseStackFlag(flags['--stack-defaults']) } : {}) }
|
|
118
227
|
if (pos[2] === undefined && checks === undefined && runners === undefined && !Object.keys(extra).length) { const p = await client.linePolicy(needQuilt(), pos[1]); return print(p, describePolicy(p)) }
|
|
119
228
|
const p = await client.setLinePolicy(needQuilt(), pos[1], { ...(pos[2] !== undefined ? { approvals: Number(pos[2]) } : {}), ...(checks ? { checks } : {}), ...(runners !== undefined ? { runners } : {}), ...extra })
|
|
120
229
|
return print(p, describePolicy(p))
|
|
121
230
|
}
|
|
122
|
-
throw new Error('seam line [list] | new release/<name> [--from <rev>] | freeze <line> | unfreeze <line> | require <line> [<approvals>] [--checks ci,lint] [--runners ci=<member>] [--tiers e2e=post_merge] [--tests test/**] [--ci-config ops/**]')
|
|
231
|
+
throw new Error('seam line [list] | new release/<name> [--from <rev>] | freeze <line> | unfreeze <line> | require <line> [<approvals>] [--checks ci,lint] [--runners ci=<member>] [--tiers e2e=post_merge] [--tests test/**] [--ci-config ops/**] [--human-only supabase/migrations/**] [--pre-merge-checks on|off] [--approval-survives none|whitespace|comment-only|docs|default] [--auto-combine lines|additive|whitespace] [--batch 10/120] [--bisect-width 8] [--needs-stack db_branch,db_migration|none] [--monotonic-stack db_migration|none] [--migrations-in-order on|off]')
|
|
123
232
|
}
|
|
124
233
|
case 'tag':
|
|
125
234
|
case 'release': {
|
|
@@ -135,39 +244,76 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
135
244
|
return print(x, `Released ${x.name} at ${where(x)}. Read it as tag:${x.name}.`)
|
|
136
245
|
}
|
|
137
246
|
case 'events': {
|
|
138
|
-
// seam events [--after n] [--kinds a,b] | --webhook <url> [--secret s] [--kinds a,b] | --webhook off
|
|
247
|
+
// seam events [--after n] [--kinds a,b] [--wait seconds | --follow] | --webhook <url> [--add | --remove] [--secret s] [--kinds a,b] | --webhook off
|
|
139
248
|
const kinds = flags['--kinds'] ? String(flags['--kinds']).split(',') : undefined
|
|
140
249
|
if (flags['--webhook']) {
|
|
141
250
|
const off = flags['--webhook'] === 'off'
|
|
142
|
-
const r = await client.subscribe(needQuilt(),
|
|
143
|
-
|
|
251
|
+
if (!off && flags['--remove']) { const r = await client.subscribe(needQuilt(), { url: flags['--webhook'], remove: true }); return print(r, `Webhook to ${r.url} removed.${r.hooks.length ? ` Still posting to ${r.hooks.map((h) => h.url).join(', ')}.` : ''}`) }
|
|
252
|
+
const r = await client.subscribe(needQuilt(), off ? { url: null } : { url: flags['--webhook'], secret: flags['--secret'], events: kinds, ...(flags['--add'] ? { add: true } : {}) })
|
|
253
|
+
return print(r, off ? 'Webhooks removed.' : `Events (${r.events.join(', ')}) now POST to ${r.url}${r.hooks?.length > 1 ? ` (and ${r.hooks.length - 1} more webhook(s) of yours)` : ''}${r.secret ? `\nSigning secret (shown once): ${r.secret}` : ''}`)
|
|
144
254
|
}
|
|
145
|
-
|
|
255
|
+
if (flags['--follow']) {
|
|
256
|
+
// Prints each event as it arrives (a loop of 30-second long-polls) until stopped; --json: one line per event.
|
|
257
|
+
let after = flags['--after']
|
|
258
|
+
while (!stop()) {
|
|
259
|
+
const r = await client.events(needQuilt(), { after, kinds, wait: 30 })
|
|
260
|
+
if (r.events.length) out(json ? r.events.map((e) => JSON.stringify(e)).join('\n') : describeEvents(r))
|
|
261
|
+
after = r.after
|
|
262
|
+
}
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
const r = await client.events(needQuilt(), { after: flags['--after'], kinds, ...(flags['--wait'] ? { wait: flags['--wait'] } : {}) })
|
|
146
266
|
return print(r, describeEvents(r))
|
|
147
267
|
}
|
|
148
268
|
case 'square': {
|
|
149
269
|
const sub = pos[0] || 'list'
|
|
150
270
|
if (sub === 'open') {
|
|
151
|
-
const sq = await client.openSquare(needQuilt(), { title: pos.slice(1).join(' ') || flags['--title'] || 'untitled', line: flags['--line'], task_id: flags['--task'], ...(flags['--on'] ? { on: flags['--on'] } : {}) })
|
|
271
|
+
const sq = await client.openSquare(needQuilt(), { title: pos.slice(1).join(' ') || flags['--title'] || 'untitled', line: flags['--line'], task_id: flags['--task'], ...(flags['--on'] ? { on: flags['--on'] } : {}), ...(flags['--another'] ? { another: true } : {}), ...(flags['--stack-keys'] || typeof flags['--stack'] === 'string' ? { stack: parseStackFlag(flags['--stack-keys'] || flags['--stack']) } : {}) })
|
|
152
272
|
let text = describeSquare(sq)
|
|
153
273
|
if (flags['--dir']) {
|
|
154
274
|
const dir = path.resolve(cwd, flags['--dir'])
|
|
155
|
-
await checkout(client, { quilt: needQuilt(), square: sq.id, paths, dir, api })
|
|
275
|
+
await checkout(client, { quilt: needQuilt(), square: sq.id, squareRow: sq, paths, dir, api, codec: await codecFor(client, needQuilt()) })
|
|
156
276
|
text += `\nChecked out into ${dir}.`
|
|
157
277
|
}
|
|
158
278
|
return print(sq, text)
|
|
159
279
|
}
|
|
160
|
-
// seam square handoff [sq] --to <member> [--note text] | abandon [sq] | reopen [sq]
|
|
280
|
+
// seam square handoff [sq] --to <member> [--note text] | abandon [sq] [--cascade] | reopen [sq] | archive [sq] | unarchive [sq] | hide [sq] | unhide [sq] | edit [sq] [--title t] [--task id] [--line name] | share [sq] --with <member> | unshare [sq] --with <member> | reparent [sq] --onto <sq>|--line
|
|
161
281
|
if (sub === 'handoff') { const sq = await client.handoff(needQuilt(), pos[1] || needSquare(), { to: flags['--to'], note: flags['--note'] }); return print(sq, `Handed ${sq.short || sq.id} to ${sq.holder}.`) }
|
|
162
|
-
if (sub === 'abandon') { const sq = await client.abandon(needQuilt(), pos[1] || needSquare()); return print(sq, `Abandoned ${sq.short || sq.id}; seam square reopen brings it back.`) }
|
|
282
|
+
if (sub === 'abandon') { const sq = await client.abandon(needQuilt(), pos[1] || needSquare(), flags['--cascade'] ? { children: 'cascade' } : {}); return print(sq, `Abandoned ${sq.short || sq.id}${sq.cascaded ? ` and the ${sq.cascaded.length} square(s) of yours stacked on it` : ''}; seam square reopen brings it back.`) }
|
|
163
283
|
// seam square merged [sq] --as <git sha>: closes a square whose work went into git by hand (admins, during a follow)
|
|
164
284
|
if (sub === 'merged') {
|
|
165
285
|
if (!flags['--as']) throw new Error('Which git commit holds the work? seam square merged [sq:id] --as <sha>')
|
|
166
286
|
const sq = await client.squareMerged(needQuilt(), pos[1] || needSquare(), { git_sha: flags['--as'] })
|
|
167
287
|
return print(sq, `${sq.short || sq.id} is merged by hand: patch ${sq.merged.patch_no} on ${sq.merged.line} (git ${sq.merged.git_sha.slice(0, 12)}).`)
|
|
168
288
|
}
|
|
289
|
+
if (sub === 'edit') {
|
|
290
|
+
const body = { ...(flags['--title'] !== undefined ? { title: flags['--title'] } : {}), ...(flags['--task'] !== undefined ? { task_id: flags['--task'] === 'none' ? null : flags['--task'] } : {}), ...(flags['--line'] !== undefined ? { line: flags['--line'] } : {}) }
|
|
291
|
+
if (!Object.keys(body).length) throw new Error('What to change? seam square edit [sq:id] --title "…" | --task <id|none> | --line <name>')
|
|
292
|
+
const sq = await client.editSquare(needQuilt(), pos[1] || needSquare(), body)
|
|
293
|
+
return print(sq, `Edited ${describeSquare(sq)}.`)
|
|
294
|
+
}
|
|
295
|
+
if (sub === 'reparent') {
|
|
296
|
+
if (!flags['--onto'] && !flags['--line']) throw new Error('Onto what? seam square reparent [sq:id] --onto <sq:id> | --line')
|
|
297
|
+
const r = await client.reparent(needQuilt(), pos[1] || needSquare(), { to: flags['--line'] ? 'line' : flags['--onto'] })
|
|
298
|
+
return print(r, r.noop ? 'Already there.' : `${r.square.short || r.square.id} now ${r.reparented.to ? `stacks on ${r.reparented.to}` : `sits on ${r.square.line}'s tip`}${r.conflicted ? `, with ${r.conflicted} conflict(s) to settle` : ''}. Run seam checkout ${r.square.short} to bring a folder there.`)
|
|
299
|
+
}
|
|
300
|
+
if (sub === 'share' || sub === 'unshare') {
|
|
301
|
+
if (!flags['--with']) throw new Error(`With whom? seam square ${sub} [sq:id] --with <member>`)
|
|
302
|
+
const sq = await client[sub](needQuilt(), pos[1] || needSquare(), { member: flags['--with'] })
|
|
303
|
+
return print(sq, `${sq.short || sq.id}'s writers: ${(sq.writers || []).join(', ') || 'only its holder'}.`)
|
|
304
|
+
}
|
|
305
|
+
if (sub === 'archive') { const sq = await client.archive(needQuilt(), pos[1] || needSquare()); return print(sq, `Archived ${sq.short || sq.id}: its claims are released and seam squares --archived lists it. A commit to it, or seam square unarchive, wakes it.`) }
|
|
306
|
+
if (sub === 'unarchive') { const sq = await client.unarchive(needQuilt(), pos[1] || needSquare()); return print(sq, `${sq.short || sq.id} is awake again; its claims were not restored.`) }
|
|
307
|
+
if (sub === 'hide') { const sq = await client.hide(needQuilt(), pos[1] || needSquare()); return print(sq, `Hid ${sq.short || sq.id}; it stays open, and seam squares --hidden lists it.`) }
|
|
308
|
+
if (sub === 'unhide') { const sq = await client.unhide(needQuilt(), pos[1] || needSquare()); return print(sq, `${sq.short || sq.id} is listed again.`) }
|
|
169
309
|
if (sub === 'reopen') { const sq = await client.reopen(needQuilt(), pos[1] || needSquare()); return print(sq, `Reopened ${sq.short || sq.id}; you hold it.`) }
|
|
170
|
-
// seam square
|
|
310
|
+
// seam square absorb [sq]: live work into the commit of the square that last changed each file
|
|
311
|
+
if (sub === 'absorb') {
|
|
312
|
+
const r = await client.absorb(needQuilt(), pos[1] || needSquare(), {})
|
|
313
|
+
const moved = r.amended.filter((a) => a.paths.length).map((a) => `${a.paths.join(', ')} into ${a.commit_before.slice(0, 12)}`)
|
|
314
|
+
return print(r, `${moved.length ? `Absorbed ${moved.join('; ')} (${r.amended.length} commit(s) rewritten).` : 'Nothing to absorb.'}${r.left_live ? ` Still live: ${r.left_paths.join(', ')}.` : ''}`)
|
|
315
|
+
}
|
|
316
|
+
// seam square fold [sq] --into sq:<id> | split [sq] <paths,...> <paths,...> | --by claim [--stacked] [--title t]
|
|
171
317
|
if (sub === 'fold') {
|
|
172
318
|
if (!flags['--into']) throw new Error('Fold into which square? seam square fold [sq:from] --into sq:<id>')
|
|
173
319
|
const r = await client.fold(needQuilt(), pos[1] || needSquare(), { into: flags['--into'], message })
|
|
@@ -176,16 +322,36 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
176
322
|
if (sub === 'split') {
|
|
177
323
|
const own = pos[1] && pos[1].startsWith('sq:')
|
|
178
324
|
const groups = pos.slice(own ? 2 : 1).map((g) => g.split(',').filter(Boolean))
|
|
179
|
-
|
|
180
|
-
|
|
325
|
+
const by = flags['--by'] || 'paths'
|
|
326
|
+
if (by === 'paths' && !groups.length) throw new Error('Split by which paths? seam square split [sq] docs,README.md src (one group per new square), or --by claim')
|
|
327
|
+
const r = await client.split(needQuilt(), own ? pos[1] : needSquare(), { by, ...(by === 'paths' ? { groups } : {}), ...(flags['--stacked'] ? { stacked: true } : {}), ...(flags['--title'] ? { titles: [flags['--title']] } : {}) })
|
|
181
328
|
const text = [`Kept in ${r.square.short}: ${r.kept.join(', ') || '(nothing)'}`, ...r.squares.map((q) => `New ${describeSquare(q)}: ${q.paths.join(', ')}`)].join('\n')
|
|
182
329
|
if (found && block.square === r.square.id) { const st = await syncTo(client, found.dir, block, r.square.head, { square: r.square }); return print(r, `${text}\n${st.written} file(s) updated in the folder.`) }
|
|
183
330
|
return print(r, text)
|
|
184
331
|
}
|
|
185
332
|
if (sub === 'show') { const sq = await client.square(needQuilt(), pos[1] || needSquare()); return print(sq, describeSquareStatus(sq)) }
|
|
186
|
-
const list = await client.squares(needQuilt(), { state: flags['--all'] ? undefined : (flags['--state'] || 'open'), mine: !!flags['--mine'] })
|
|
333
|
+
const list = await client.squares(needQuilt(), { state: flags['--all'] ? undefined : (flags['--state'] || 'open'), mine: !!flags['--mine'], hidden: !!flags['--hidden'], archived: !!flags['--archived'] })
|
|
187
334
|
return print(list, list.length ? list.map(describeSquare).join('\n') : 'No open squares.')
|
|
188
335
|
}
|
|
336
|
+
case 'clone': {
|
|
337
|
+
// seam clone <repo> [dir]: main (or --line), ready to work: the first commit opens a square.
|
|
338
|
+
const repo = pos[0] || flags['--quilt']
|
|
339
|
+
if (!repo) throw new Error('seam clone <repo> [folder] [--line <line>]')
|
|
340
|
+
const line = flags['--line'] || 'main'
|
|
341
|
+
const name = String(repo).split('/').pop()
|
|
342
|
+
const dir = path.resolve(cwd, pos[1] || flags['--dir'] || name)
|
|
343
|
+
const r = await checkout(client, { quilt: repo, rev: line, paths, dir, api, codec: await codecFor(client, repo) })
|
|
344
|
+
return print({ dir, files: r.files, block: { ...r.block, files: undefined } }, `Cloned ${repo} (${line}, ${r.files} file(s)) into ${dir}.\nEdit, then seam commit -m "…": the first commit opens a square for it; seam merge lands it, seam pull brings others' patches in.`)
|
|
345
|
+
}
|
|
346
|
+
case 'pull': {
|
|
347
|
+
if (!found) throw new Error('seam pull works in a block folder: run it in a folder made by seam clone or seam checkout.')
|
|
348
|
+
const r = await pull(client, found.dir, block)
|
|
349
|
+
const kept = r.kept?.length ? `\nKept your own version of ${r.kept.join(', ')} (changed on both sides): compare with seam diff.` : ''
|
|
350
|
+
if (r.noop) return print(r, block.square ? `Already up to date: ${block.square_short || block.square} sits on ${block.line}'s tip.` : `Already up to date with ${block.line}.`)
|
|
351
|
+
if (r.left) return print(r, `${r.left.square} was ${r.left.state}; the folder is now on ${r.line} (${r.at.slice(0, 12)}, ${r.written} file(s) updated).` + kept)
|
|
352
|
+
if (r.rebased) return print(r, describeWrite(r.result, 'Rebased') + `\n${r.written} file(s) updated in the folder.` + kept)
|
|
353
|
+
return print(r, `Pulled ${r.line} (now ${r.at.slice(0, 12)}): ${r.written} file(s) updated.` + kept)
|
|
354
|
+
}
|
|
189
355
|
case 'checkout': {
|
|
190
356
|
const target = pos[0]
|
|
191
357
|
if (!target) throw new Error('seam checkout <sq:id | revision> [--dir folder] [-- paths]')
|
|
@@ -201,17 +367,23 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
201
367
|
return print(r, `Switched to ${sq ? `${sq.short} "${sq.title}"` : target} (${r.written} file(s) written).`)
|
|
202
368
|
}
|
|
203
369
|
const dir = path.resolve(cwd, flags['--dir'] || (isSquare ? target.replace(':', '-') : 'seam-checkout'))
|
|
204
|
-
const r = await checkout(client, { quilt: needQuilt(), ...(isSquare ? { square: target } : { rev: target }), paths, dir, api })
|
|
370
|
+
const r = await checkout(client, { quilt: needQuilt(), ...(isSquare ? { square: target } : { rev: target }), paths, dir, api, codec: await codecFor(client, needQuilt()) })
|
|
205
371
|
return print({ dir, files: r.files, block: { ...r.block, files: undefined } }, `Checked out ${r.files} file(s) of ${isSquare ? target : target} under ${r.block.paths.map((p) => p || '/').join(', ')} into ${dir}.`)
|
|
206
372
|
}
|
|
207
373
|
case 'runner': {
|
|
208
374
|
// seam runner --check ci[,lint] --run "npm test" [--dir <folder>] [--interval <s>] [--log-url <template>] [--once] [--keep]
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
375
|
+
// seam runner --config runner.json: { quilt?, checks: { "<check>": "<command>" } }, one command per check
|
|
376
|
+
const conf = flags['--config'] ? readRunnerConfig(fs.readFileSync(path.resolve(cwd, String(flags['--config'])), 'utf8')) : null
|
|
377
|
+
const checks = conf ? Object.keys(conf.commands) : String(flags['--check'] || '').split(',').map((c) => c.trim()).filter(Boolean)
|
|
378
|
+
if (!conf && (!checks.length || !flags['--run'] || flags['--run'] === true)) throw new Error('seam runner --check <name>[,<name>] --run "<command>" | --config <file> [--dir <folder>] [--interval <seconds>] [--log-url <template>] [--once]')
|
|
379
|
+
const q = flags['--quilt'] || conf?.quilt || needQuilt()
|
|
212
380
|
const say = (l) => { if (!flags['--json']) console.log(l) }
|
|
213
|
-
if (!flags['--once']) say(`seam runner: running ${checks.join(', ')} for ${q} with: ${flags['--run']} (Ctrl-C to stop)`)
|
|
214
|
-
|
|
381
|
+
if (!flags['--once']) say(`seam runner: running ${checks.join(', ')} for ${q}${conf ? ` (${flags['--config']})` : ` with: ${flags['--run']}`} (Ctrl-C to stop)`)
|
|
382
|
+
// --listen <port>: Seam's webhook wakes it; --idle-exit <seconds>: it stops when there is nothing to do
|
|
383
|
+
const listener = flags['--listen'] ? listenForWork({ port: Number(flags['--listen']), secret: process.env.SEAM_WEBHOOK_SECRET || null, say }) : null
|
|
384
|
+
const idle = flags['--idle-exit'] ? Number(flags['--idle-exit']) * 1000 : 0
|
|
385
|
+
const done = await runChecks({ client, quilt: q, checks, ...(conf ? { commands: conf.commands } : { run: String(flags['--run']) }), ...(listener ? { wake: listener.wake } : {}), ...(idle ? { idleExitMs: idle } : {}), api, once: !!flags['--once'], keep: !!flags['--keep'], ...(flags['--dir'] ? { dir: path.resolve(cwd, flags['--dir']) } : {}), ...(flags['--interval'] ? { intervalMs: Math.max(1, Number(flags['--interval'])) * 1000 } : {}), ...(flags['--log-url'] ? { logUrl: String(flags['--log-url']) } : {}), say })
|
|
386
|
+
listener?.server.close()
|
|
215
387
|
return print({ runs: done }, done.length ? `Ran ${done.length} check run(s).` : 'Nothing queued.')
|
|
216
388
|
}
|
|
217
389
|
case 'status': {
|
|
@@ -221,25 +393,71 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
221
393
|
}
|
|
222
394
|
case 'review': {
|
|
223
395
|
// seam review [sq] (the reviews and where the square stands)
|
|
224
|
-
// seam review approve|request-changes|comment|dismiss [sq] [-m <text>] [--path <file> [--line <n>]]
|
|
225
|
-
|
|
396
|
+
// seam review approve|request-changes|comment|dismiss [sq] [-m <text>] [--path <file> [--line <n>]] [--scope <glob>,<glob> (approve: only these paths)]
|
|
397
|
+
// seam review request [sq] [--to <member>[,<member>]] [-m <why>]: asks them, and whoever the line's unmet rules name, for a review
|
|
398
|
+
const VERDICT = { approve: 'approve', 'request-changes': 'request_changes', comment: 'comment', dismiss: 'dismiss', request: 'request' }
|
|
226
399
|
const kind = VERDICT[pos[0]]
|
|
227
400
|
const sq = (kind ? pos[1] : pos[0]) || needSquare()
|
|
401
|
+
// seam review interdiff [sq] [--since <rev>]: what changed since you reviewed (or since <rev>)
|
|
402
|
+
if (pos[0] === 'interdiff') { const r = await client.interdiff(needQuilt(), pos[1] || needSquare(), flags['--since'] ? { since: String(flags['--since']) } : {}); return print(r, describeInterdiff(r)) }
|
|
403
|
+
// seam review apply <review id> [sq]: applies a suggestion as a commit on the square (you must be its holder or a writer)
|
|
404
|
+
if (pos[0] === 'apply') {
|
|
405
|
+
if (!pos[1]) throw new Error('Which suggestion? seam review apply <review id> [square] (the id is in seam review)')
|
|
406
|
+
const r = await client.applySuggestion(needQuilt(), pos[2] || needSquare(), pos[1])
|
|
407
|
+
return print(r, `Applied the suggestion as ${r.result.id.slice(0, 12)} on ${r.square.id || pos[2] || 'the square'}.`)
|
|
408
|
+
}
|
|
228
409
|
if (!kind) { const r = await client.reviews(needQuilt(), sq); return print(r, describeReviews(r)) }
|
|
229
|
-
const r = await client.review(needQuilt(), sq, { kind, body: message || '', ...(flags['--path'] ? { path: flags['--path'] } : {}), ...(flags['--line'] ? { line: Number(flags['--line']) } : {}) })
|
|
410
|
+
const r = await client.review(needQuilt(), sq, { kind, body: message || '', ...(kind === 'request' && flags['--to'] ? { asked: String(flags['--to']).split(',').map((m) => m.trim()).filter(Boolean) } : {}), ...(flags['--path'] ? { path: flags['--path'] } : {}), ...(flags['--line'] ? { line: Number(flags['--line']) } : {}), ...(kind === 'approve' && flags['--scope'] ? { scope: String(flags['--scope']).split(',').map((g) => g.trim()).filter(Boolean) } : {}), ...(flags['--suggest'] != null ? { suggestion: { text: String(flags['--suggest']).replace(/\\n/g, '\n'), ...(flags['--to-line'] ? { to_line: Number(flags['--to-line']) } : {}) } } : {}) })
|
|
230
411
|
return print(r, describeReviewed(r))
|
|
231
412
|
}
|
|
413
|
+
case 'watch': {
|
|
414
|
+
// seam watch (yours; --all: everyone's) | seam watch <glob>... [--classes logic,schema] [--min-lines 50] [--line main] [--note why] | seam watch remove <id>
|
|
415
|
+
if (pos[0] === 'remove') {
|
|
416
|
+
if (!pos[1]) throw new Error('Which one? seam watch remove <id>')
|
|
417
|
+
const r = await client.unwatch(needQuilt(), pos[1])
|
|
418
|
+
return print(r, `Stopped watching (${r.removed}).`)
|
|
419
|
+
}
|
|
420
|
+
if (!pos.length) { const ws = await client.watches(needQuilt(), { all: !!flags['--all'] }); return print(ws, describeWatches(ws)) }
|
|
421
|
+
const w = await client.watch(needQuilt(), { paths: pos, ...(flags['--classes'] ? { classes: String(flags['--classes']).split(',').map((c) => c.trim()).filter(Boolean) } : {}), ...(flags['--min-lines'] != null ? { min_lines: Number(flags['--min-lines']) } : {}), ...(flags['--line'] ? { lines: [flags['--line']] } : {}), ...(flags['--note'] ? { note: flags['--note'] } : {}) })
|
|
422
|
+
return print(w, `Watching ${w.paths.join(', ')}${w.classes ? ` for ${w.classes.join(', ')} changes` : ''}${w.min_lines != null ? ` over ${w.min_lines} lines` : ''}: each matching patch sends you watch.triggered (seam events). ${w.id}`)
|
|
423
|
+
}
|
|
232
424
|
case 'claim': {
|
|
233
|
-
// seam claim (lists the quilt's claims) | seam claim <pattern>... [--note <why>] (claims them for your square)
|
|
425
|
+
// seam claim (lists the quilt's claims) | seam claim <pattern>... [--note <why>] [--expires <seconds>] (claims them for your square)
|
|
234
426
|
if (!pos.length) { const cs = await client.claims(needQuilt(), flags['--square']); return print(cs, describeClaims(cs)) }
|
|
427
|
+
// seam claim request <claim id|pattern> --title <what for> [-m <plan>] | seam claim withdraw <request id> | seam claim handoff <pattern> [--to <request id>] [--note <context>] | seam claim finish [--note <context>]
|
|
428
|
+
if (pos[0] === 'request') {
|
|
429
|
+
if (!pos[1] || !flags['--title']) throw new Error('seam claim request <claim id|pattern> --title <what you need it for> [-m <your plan>]')
|
|
430
|
+
const r = await client.requestClaim(needQuilt(), { claim: pos[1], square: needSquare(), title: flags['--title'], ...(message ? { description: message } : {}) })
|
|
431
|
+
return print(r, `Waiting for ${pos[1]} as request ${r.id}: the holder hands it over when done (seam claim withdraw ${r.id} to stop waiting).`)
|
|
432
|
+
}
|
|
433
|
+
if (pos[0] === 'withdraw') { if (!pos[1]) throw new Error('seam claim withdraw <request id>'); const r = await client.withdrawRequest(needQuilt(), pos[1]); return print(r, `Withdrew request ${pos[1]}.`) }
|
|
434
|
+
if (pos[0] === 'handoff') {
|
|
435
|
+
if (!pos[1]) throw new Error('seam claim handoff <pattern|claim id> [--to <request id>] [--note <what you changed and what is left>]')
|
|
436
|
+
const r = await client.handoffClaim(needQuilt(), needSquare(), { claim: pos[1], ...(flags['--to'] ? { to: flags['--to'] } : {}), ...(flags['--note'] || message ? { note: flags['--note'] || message } : {}) })
|
|
437
|
+
return print(r, describeHandoff(r))
|
|
438
|
+
}
|
|
439
|
+
if (pos[0] === 'finish') {
|
|
440
|
+
const r = await client.finishClaims(needQuilt(), needSquare(), flags['--note'] || message ? { note: flags['--note'] || message } : {})
|
|
441
|
+
return print(r, describeFinished(r))
|
|
442
|
+
}
|
|
235
443
|
const sq = needSquare()
|
|
236
444
|
const made = []
|
|
237
|
-
for (const p of pos) made.push(await client.claim(needQuilt(), sq, { pattern: p, note: flags['--note'] || message }))
|
|
238
|
-
return print(made, made.map((c) => `Claimed ${c.pattern} for ${c.square}: writes to it from other squares on ${c.line} are refused until the square merges or you run seam unclaim ${c.pattern}.`).join('\n'))
|
|
445
|
+
for (const p of pos) made.push(await client.claim(needQuilt(), sq, { pattern: p, note: flags['--note'] || message, ...(flags['--expires'] ? { expires_in: Number(flags['--expires']) } : {}) }))
|
|
446
|
+
return print(made, made.map((c) => `Claimed ${c.pattern} for ${c.square}: writes to it from other squares on ${c.line} are refused until ${c.expires_at ? `${new Date(c.expires_at).toISOString()}, ` : ''}the square merges or you run seam unclaim ${c.pattern}.`).join('\n'))
|
|
239
447
|
}
|
|
240
448
|
case 'check': {
|
|
241
449
|
// seam check <name> red|green|flaky [--line main] [--patch n] | seam check <name> green|red --square <sq>
|
|
242
450
|
const [name, status] = pos
|
|
451
|
+
if (name === 'pipeline') {
|
|
452
|
+
// seam check pipeline <check> [--line main] [--stop]: the open bisect of a red check and the last one's outcome; --stop ends it (admin)
|
|
453
|
+
if (!status) throw new Error('seam check pipeline <check> [--line <line>] [--stop]')
|
|
454
|
+
if (flags['--stop']) {
|
|
455
|
+
const s = await client.stopPipeline(needQuilt(), flags['--line'] || 'main', status)
|
|
456
|
+
return print(s, s.stopped ? `Stopped the bisect of ${status} on ${flags['--line'] || 'main'} (patches ${s.bisect.range[0] + 1} to ${s.bisect.range[1]}): its waiting runs are cancelled and nothing is reverted.` : `No bisect of ${status} is open on ${flags['--line'] || 'main'}.`)
|
|
457
|
+
}
|
|
458
|
+
const r = await client.pipeline(needQuilt(), flags['--line'] || 'main', status)
|
|
459
|
+
return print(r, describePipeline(r))
|
|
460
|
+
}
|
|
243
461
|
if (name === 'rerun') {
|
|
244
462
|
// seam check rerun <check> --square <sq> | --line <line> [--commit <id>]: ask its runners again
|
|
245
463
|
if (!status) throw new Error('seam check rerun <check> --square <square> | --line <line> [--commit <id>]')
|
|
@@ -268,6 +486,27 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
268
486
|
for (const p of pos) out.push(await client.unclaim(needQuilt(), sq, p))
|
|
269
487
|
return print(out, out.map((r) => `Let go of ${r.released.pattern}.`).join('\n'))
|
|
270
488
|
}
|
|
489
|
+
case 'daemon': {
|
|
490
|
+
// seam daemon (foreground: watches this block folder and shares each change as live work) | seam daemon status | flush | stop
|
|
491
|
+
if (!found) throw new Error('seam daemon watches a block folder: run it in a folder made by seam checkout or seam square open.')
|
|
492
|
+
const sub = pos[0]
|
|
493
|
+
if (sub === 'status' || sub === 'flush' || sub === 'stop' || sub === 'rebuild') {
|
|
494
|
+
const r = await askDaemon(found.dir, sub, sub === 'status' ? 'GET' : 'POST')
|
|
495
|
+
if (!r) return print({ running: false }, `No daemon is running for ${found.dir}: start one with seam daemon.`)
|
|
496
|
+
if (sub === 'stop') return print(r, 'The daemon is stopping.')
|
|
497
|
+
if (sub === 'rebuild') return print(r, describeRebuild(r))
|
|
498
|
+
return print(r, `Watching ${r.dir} on ${String(r.square).startsWith("sq:") ? r.square : `sq:${r.square}`} (${r.state}): sent ${r.sent} time(s)${r.pending.length ? `, ${r.pending.length} path(s) waiting` : ''}${r.last_error ? `. ${r.last_error}` : ''}.`)
|
|
499
|
+
}
|
|
500
|
+
if (sub) throw new Error(`seam daemon ${sub}? seam daemon | daemon status | daemon flush | daemon rebuild | daemon stop`)
|
|
501
|
+
const d = await startDaemon({ client, dir: found.dir, codec: block._codec || null, ...(flags['--debounce'] != null ? { debounceMs: Number(flags['--debounce']) } : {}) })
|
|
502
|
+
if (!json) out(`seam daemon: sharing each change in ${found.dir} as live work on ${block.square} (Ctrl-C to stop; control API on 127.0.0.1:${d.port}).`)
|
|
503
|
+
const onSignal = () => d.stop()
|
|
504
|
+
process.once('SIGINT', onSignal); process.once('SIGTERM', onSignal)
|
|
505
|
+
const wait = setInterval(() => { if (stop()) d.stop() }, 500)
|
|
506
|
+
await d.stopped()
|
|
507
|
+
clearInterval(wait); process.off('SIGINT', onSignal); process.off('SIGTERM', onSignal)
|
|
508
|
+
return print(d.status(), 'The daemon stopped.')
|
|
509
|
+
}
|
|
271
510
|
case 'live': {
|
|
272
511
|
// seam live [sq] | live share (a block folder's uncommitted edits) | live clear
|
|
273
512
|
const sub = pos[0]
|
|
@@ -275,10 +514,21 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
275
514
|
const sqRef = flags['--square'] || block?.square
|
|
276
515
|
if (!sqRef) throw new Error(`seam live ${sub} works in a block folder on a square (or pass --square sq:<id>).`)
|
|
277
516
|
if (sub === 'share' && !found) throw new Error('seam live share sends a block folder\'s uncommitted edits: run it in a folder made by seam checkout.')
|
|
278
|
-
|
|
517
|
+
// seam live share <paths>: only those paths, merged into what the square shares (others' paths stay).
|
|
518
|
+
const named = sub === 'share' ? [...pos.slice(1), ...paths] : []
|
|
519
|
+
if (named.length) {
|
|
520
|
+
const r = await client.patchLive(needQuilt(), sqRef, livePatch(found.dir, block, named.map((p) => path.resolve(cwd, p))))
|
|
521
|
+
return print(r, describeLiveSet(r))
|
|
522
|
+
}
|
|
523
|
+
const changes = sub === 'clear' ? [] : localChanges(found.dir, block).map((c) => c.status === 'deleted' ? { path: c.path, op: 'delete' } : { path: c.path, op: 'put', content: bytesOf(c).toString('base64'), mode: c.mode })
|
|
279
524
|
const r = await client.setLive(needQuilt(), sqRef, { changes })
|
|
280
525
|
return print(r, describeLiveSet(r))
|
|
281
526
|
}
|
|
527
|
+
if (sub === 'join') {
|
|
528
|
+
// seam live join [sq] [--member <member>]: whether a member may load the square's live work whole
|
|
529
|
+
const r = await client.liveJoin(needQuilt(), pos[1] || needSquare(), flags['--member'])
|
|
530
|
+
return print(r, describeLiveJoin(r))
|
|
531
|
+
}
|
|
282
532
|
const r = await client.live(needQuilt(), sub || needSquare())
|
|
283
533
|
return print(r, describeLive(r))
|
|
284
534
|
}
|
|
@@ -296,6 +546,8 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
296
546
|
if (!found) {
|
|
297
547
|
throw new Error('seam commit seals a block folder\'s changes; run it in a folder made by seam checkout.')
|
|
298
548
|
}
|
|
549
|
+
// --allow sf_…,sf_…: secret findings a person has checked and lets through (11.9).
|
|
550
|
+
const allowed = flags['--allow'] ? String(flags['--allow']).split(',').map((x) => x.trim()).filter(Boolean) : []
|
|
299
551
|
// --sign: signs the new commit with this computer's key (seam key add).
|
|
300
552
|
const signed = async (r) => {
|
|
301
553
|
if (!flags['--sign'] || !r?.result?.id) return ''
|
|
@@ -304,28 +556,65 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
304
556
|
return `\nSigned with ${s.signature.key}.`
|
|
305
557
|
}
|
|
306
558
|
if (flags['--amend']) {
|
|
307
|
-
const r = await amendFolder(client, found.dir, block, { message, paths: [...pos, ...paths].map((p) => path.resolve(cwd, p)) })
|
|
559
|
+
const r = await amendFolder(client, found.dir, block, { message, paths: [...pos, ...paths].map((p) => path.resolve(cwd, p)), allow: allowed })
|
|
308
560
|
const s = await signed(r)
|
|
309
561
|
return print(r, describeWrite(r, `Amended ${r.amended.slice(0, 12)} as`) + s)
|
|
310
562
|
}
|
|
311
563
|
if (!message) throw new Error('Say what changed: seam commit -m "message" [paths...]')
|
|
312
|
-
const r = await commitFolder(client, found.dir, block, { message, paths: [...pos, ...paths].map((p) => path.resolve(cwd, p)) })
|
|
564
|
+
const r = await commitFolder(client, found.dir, block, { message, paths: [...pos, ...paths].map((p) => path.resolve(cwd, p)), allow: allowed })
|
|
313
565
|
if (r.nothing) return print(r, 'Nothing to commit.')
|
|
314
566
|
const s = await signed(r)
|
|
315
|
-
return print(r, describeWrite(r, 'Committed') + s)
|
|
567
|
+
return print(r, describeOpened(r) + describeWrite(r, 'Committed') + s)
|
|
316
568
|
}
|
|
317
569
|
case 'sign': {
|
|
318
570
|
// seam sign [rev]: signs a commit you made (default: your square's head, or main's tip).
|
|
319
571
|
const r = await signWithLocalKey(client, needQuilt(), pos[0] || (block?.square ? block.square_short : block?.line) || 'main')
|
|
320
572
|
return print(r, `Signed ${r.short} with ${r.signature.key}.`)
|
|
321
573
|
}
|
|
574
|
+
case 'block': {
|
|
575
|
+
// seam block [list] [--square <s>] [--mine] | open <square> [--paths a,b] [--mode sparse|virtual|api-only] [--machine <name>] [--at <rev>] [--read-only] [--exclude <globs>] [--max-fetch-bytes <n>] | touch <id> [--add a,b] [--remove a,b] | close <id>
|
|
576
|
+
const sub = pos[0] || 'list'
|
|
577
|
+
const csv = (f) => flags[f] == null ? undefined : String(flags[f]).split(',').map((x) => x.trim()).filter(Boolean)
|
|
578
|
+
const usage = 'seam block [list] | rebuild | open <square> [--paths a,b] [--mode sparse|virtual|api-only] [--machine <name>] [--at <rev>] [--read-only] | touch <id> [--add a,b] [--remove a,b] | close <id>'
|
|
579
|
+
if (sub === 'rebuild') {
|
|
580
|
+
// seam block rebuild: finishes a move of this folder that stopped part way (the journal in .seam)
|
|
581
|
+
if (!found) throw new Error('seam block rebuild works on a block folder; run it in a folder made by seam checkout.')
|
|
582
|
+
const r = await rebuildBlock(client, found.dir, block)
|
|
583
|
+
return print(r, describeRebuild(r))
|
|
584
|
+
}
|
|
585
|
+
if (sub === 'list') { const r = await client.blocks(needQuilt(), { square: flags['--square'], mine: flags['--mine'] ? 1 : undefined }); return print(r, describeBlocks(r)) }
|
|
586
|
+
if (sub === 'open') {
|
|
587
|
+
if (!pos[1]) throw new Error(usage)
|
|
588
|
+
const r = await client.openBlock(needQuilt(), { square: pos[1], paths: csv('--paths'), mode: flags['--mode'], machine: flags['--machine'] || os.hostname(), at: flags['--at'], read_only: flags['--read-only'] === true || undefined, exclude: csv('--exclude'), max_fetch_bytes: flags['--max-fetch-bytes'] == null ? undefined : Number(flags['--max-fetch-bytes']) })
|
|
589
|
+
return print(r, `Opened ${describeBlock(r)}`)
|
|
590
|
+
}
|
|
591
|
+
if (sub === 'touch') { if (!pos[1]) throw new Error(usage); const r = await client.touchBlock(needQuilt(), pos[1], { add: csv('--add'), remove: csv('--remove') }); return print(r, describeBlock(r)) }
|
|
592
|
+
if (sub === 'close') { if (!pos[1]) throw new Error(usage); const r = await client.closeBlock(needQuilt(), pos[1]); return print(r, `Closed block ${r.id}: the square and the folder's files are untouched.`) }
|
|
593
|
+
throw new Error(usage)
|
|
594
|
+
}
|
|
322
595
|
case 'token': {
|
|
323
|
-
// seam token [list] | new <name> [--level read|write] | revoke <id or name>
|
|
596
|
+
// seam token [list] | new <name> [--level read|write] [--days 1-365] | revoke <id or name>
|
|
597
|
+
// seam token agent [list] | new [--agent agent:<id>] [--level read|write|merge] [--squares own|sq:…,…] [--paths src,docs] [--hours 8] [--checks] [--rate commit_rate=10,merge_rate=5] | revoke <id>
|
|
324
598
|
const sub = pos[0] || 'list'
|
|
599
|
+
if (sub === 'agent') {
|
|
600
|
+
const act = pos[1] || 'list'
|
|
601
|
+
if (act === 'list') { const r = await client.agentTokens(needQuilt()); return print(r, describeAgentTokens(r)) }
|
|
602
|
+
if (act === 'new') {
|
|
603
|
+
const squares = flags['--squares'] && flags['--squares'] !== 'own' ? String(flags['--squares']).split(',').map((x) => x.trim()).filter(Boolean) : 'own'
|
|
604
|
+
const r = await client.createAgentToken(needQuilt(), { ...(flags['--agent'] ? { agent: flags['--agent'] } : {}), max_level: flags['--level'] || 'write', squares, paths: flags['--paths'] ? String(flags['--paths']).split(',').map((x) => x.trim()).filter(Boolean) : [], ...(flags['--hours'] ? { ttl_s: Math.round(Number(flags['--hours']) * 3600) } : {}), ...agentTokenExtras(flags) })
|
|
605
|
+
return print(r, describeAgentToken(r))
|
|
606
|
+
}
|
|
607
|
+
if (act === 'revoke') {
|
|
608
|
+
if (!pos[2]) throw new Error('seam token agent revoke <id>')
|
|
609
|
+
const r = await client.revokeAgentToken(needQuilt(), pos[2])
|
|
610
|
+
return print(r, `Revoked agent token ${r.id}: it no longer reaches the quilt.`)
|
|
611
|
+
}
|
|
612
|
+
throw new Error('seam token agent [list] | new [--agent agent:<id>] [--level read|write|merge] [--squares own|…] [--paths …] [--hours n] | revoke <id>')
|
|
613
|
+
}
|
|
325
614
|
if (sub === 'list') { const r = await client.tokens(needQuilt()); return print(r, describeTokens(r)) }
|
|
326
615
|
if (sub === 'new') {
|
|
327
|
-
if (!pos[1]) throw new Error('seam token new <name> [--level read|write]')
|
|
328
|
-
const r = await client.createToken(needQuilt(), { name: pos[1], level: flags['--level'] || 'read' })
|
|
616
|
+
if (!pos[1]) throw new Error('seam token new <name> [--level read|write] [--days 1-365]')
|
|
617
|
+
const r = await client.createToken(needQuilt(), { name: pos[1], level: flags['--level'] || 'read', ...(flags['--days'] ? { expires_in_days: Number(flags['--days']) } : {}) })
|
|
329
618
|
return print(r, describeNewToken(r))
|
|
330
619
|
}
|
|
331
620
|
if (sub === 'revoke') {
|
|
@@ -333,7 +622,138 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
333
622
|
const r = await client.revokeToken(needQuilt(), pos[1])
|
|
334
623
|
return print(r, `Revoked token ${r.name} (${r.id}): it no longer reaches the quilt.`)
|
|
335
624
|
}
|
|
336
|
-
throw new Error('seam token [list] | new <name> [--level read|write] | revoke <id or name>')
|
|
625
|
+
throw new Error('seam token [list] | new <name> [--level read|write] [--days 1-365] | revoke <id or name>')
|
|
626
|
+
}
|
|
627
|
+
case 'org': {
|
|
628
|
+
// seam org [list] | new <slug> [--title <name>] | show <org> | add <org> <member> [--role member|admin|owner] | remove <org> <member>
|
|
629
|
+
const sub = pos[0] || 'list'
|
|
630
|
+
if (sub === 'list') { const r = await client.orgs(); return print(r, describeOrgs(r)) }
|
|
631
|
+
if (sub === 'new') { if (!pos[1]) throw new Error('seam org new <slug>'); const r = await client.createOrg({ slug: pos[1], name: flags['--title'] }); return print(r, `Made org ${r.slug}; you are its owner. Make its quilts with seam quilts new <slug> --org ${r.slug}.`) }
|
|
632
|
+
if (sub === 'show') { if (!pos[1]) throw new Error('seam org show <org>'); const r = await client.org(pos[1]); return print(r, describeOrg(r)) }
|
|
633
|
+
if (sub === 'log') { if (!pos[1]) throw new Error('seam org log <org> [-n <count>] [--before <id>]'); const r = await client.orgLog(pos[1], { limit: flags['-n'], before: flags['--before'] }); return print(r, describeOrgLog(r)) }
|
|
634
|
+
if (sub === 'add' || sub === 'remove') {
|
|
635
|
+
if (!pos[1] || !pos[2]) throw new Error(`seam org ${sub} <org> <member>${sub === 'add' ? ' [--role member|admin|owner]' : ''}`)
|
|
636
|
+
const r = await client.setOrgMember(pos[1], pos[2], sub === 'add' ? (flags['--role'] || 'member') : null)
|
|
637
|
+
return print(r, (r.invited ? `Invited ${r.email}: ${r.emailed ? 'Seam emailed them a link' : `send them ${r.sign_up}`}. They are in the org from their first sign-in.\n` : '') + describeOrg(r))
|
|
638
|
+
}
|
|
639
|
+
if (sub === 'team') {
|
|
640
|
+
// seam org team <org> <name> <member>... : the team's members, replaced whole; none removes it.
|
|
641
|
+
if (!pos[1] || !pos[2]) throw new Error('seam org team <org> <name> [<member>...]')
|
|
642
|
+
const r = await client.setOrgTeam(pos[1], pos[2], pos.slice(3))
|
|
643
|
+
return print(r, describeOrgTeams(r))
|
|
644
|
+
}
|
|
645
|
+
throw new Error('seam org [list] | new <slug> | show <org> | log <org> | add <org> <member> [--role r] | remove <org> <member> | team <org> <name> [<member>...]')
|
|
646
|
+
}
|
|
647
|
+
case 'policy': {
|
|
648
|
+
// seam policy versions|approve|reject|confirm <line> [<version>]: a line's policy versions (11.4)
|
|
649
|
+
if (['versions', 'approve', 'reject', 'confirm'].includes(pos[0])) {
|
|
650
|
+
if (!pos[1]) throw new Error(`seam policy ${pos[0]} <line>${pos[0] === 'versions' ? '' : ' <version>'}`)
|
|
651
|
+
const q = needQuilt()
|
|
652
|
+
if (pos[0] !== 'versions' && !pos[2]) throw new Error(`seam policy ${pos[0]} ${pos[1]} <version>`)
|
|
653
|
+
let r
|
|
654
|
+
if (pos[0] === 'versions') r = await client.linePolicyVersions(q, pos[1])
|
|
655
|
+
else if (pos[0] === 'approve') r = await client.decideLinePolicy(q, pos[1], pos[2], 'approve')
|
|
656
|
+
else if (pos[0] === 'reject') r = await client.decideLinePolicy(q, pos[1], pos[2], 'reject')
|
|
657
|
+
else r = await confirmWithLocalKey(client, q, pos[1], Number(pos[2]))
|
|
658
|
+
return print(r, describePolicyVersions(r))
|
|
659
|
+
}
|
|
660
|
+
// seam policy [check] [sq] [--line <line>]: the square's change classes and what its line's rules need
|
|
661
|
+
const sq = (pos[0] === 'check' ? pos[1] : pos[0]) || needSquare()
|
|
662
|
+
const r = await client.squarePolicy(needQuilt(), sq, flags['--line'] ? { line: flags['--line'] } : {})
|
|
663
|
+
return print(r, describeSquarePolicy(r))
|
|
664
|
+
}
|
|
665
|
+
case 'seal': {
|
|
666
|
+
// seam seal [status] | key | grant <member> | rotate [--without <member>,...]
|
|
667
|
+
const sub = pos[0] || 'status'
|
|
668
|
+
if (sub === 'key') { const k = await ensureSealingKey(client); return print({ key_id: k.id, public_key: k.public_key }, `This computer's sealing key is ${k.id}. A member who holds a sealed quilt's key gives it to you with seam seal grant <your member id>.`) }
|
|
669
|
+
const q = needQuilt()
|
|
670
|
+
if (sub === 'status') { const r = await client.seal(q); return print(r, describeSeal(r)) }
|
|
671
|
+
if (sub === 'grant') {
|
|
672
|
+
if (!pos[1]) throw new Error('seam seal grant <member id>')
|
|
673
|
+
const r = await grantSealed(client, q, pos[1])
|
|
674
|
+
return print(r, describeSeal(r))
|
|
675
|
+
}
|
|
676
|
+
if (sub === 'rotate') {
|
|
677
|
+
const r = await rotateSealed(client, q, { without: flags['--without'] ? String(flags['--without']).split(',').map((x) => x.trim()).filter(Boolean) : [] })
|
|
678
|
+
return print(r, describeSeal(r))
|
|
679
|
+
}
|
|
680
|
+
throw new Error('seam seal [status] | key | grant <member id> | rotate [--without <member id>,...]')
|
|
681
|
+
}
|
|
682
|
+
case 'metrics': {
|
|
683
|
+
// seam metrics: the numbers Seam keeps for the quilt that ops/alerts.yml names, and which are past their threshold (admin)
|
|
684
|
+
const r = await client.metrics(needQuilt()); return print(r, describeMetrics(r))
|
|
685
|
+
}
|
|
686
|
+
case 'redact': {
|
|
687
|
+
// seam redact <blob id | path[@rev]> --reason "<why>" [--yes] | seam redact list
|
|
688
|
+
const q = needQuilt()
|
|
689
|
+
if (!pos[0] || pos[0] === 'list') { const r = await client.redactions(q); return print(r, describeRedactions(r)) }
|
|
690
|
+
if (!flags['--reason']) throw new Error('seam redact <blob id | path[@rev]> --reason "<why>": the reason is kept in the log')
|
|
691
|
+
let blob = pos[0]
|
|
692
|
+
if (!/^[0-9a-f]{64}$/.test(blob)) {
|
|
693
|
+
const at = blob.lastIndexOf('@')
|
|
694
|
+
const [p, rev] = at > 0 ? [blob.slice(0, at), blob.slice(at + 1)] : [blob, flags['--square'] || block?.square || 'main']
|
|
695
|
+
blob = (await client.cat(q, rev, p)).blob
|
|
696
|
+
if (!blob) throw new Error(`${p} has no content to redact at ${rev}`)
|
|
697
|
+
}
|
|
698
|
+
if (!flags['--yes']) throw new Error(`Redacting ${blob.slice(0, 12)} cannot be undone: every copy of those bytes leaves the quilt. Run it again with --yes.`)
|
|
699
|
+
const r = await client.redact(q, { blob, reason: flags['--reason'] })
|
|
700
|
+
return print(r, describeRedacted(r))
|
|
701
|
+
}
|
|
702
|
+
case 'verify': {
|
|
703
|
+
// seam verify [--after <seq>] [--keys <file.json>]: checks the quilt's log against the server's signatures, on this computer
|
|
704
|
+
const keys = flags['--keys'] ? JSON.parse(fs.readFileSync(path.resolve(cwd, flags['--keys']), 'utf8')) : null
|
|
705
|
+
const v = await verifyQuilt(client, needQuilt(), { after: flags['--after'] || 0, keys: keys ? (Array.isArray(keys) ? keys : keys.keys) : null })
|
|
706
|
+
const r = print(v, describeVerify(v))
|
|
707
|
+
if (v.bad.length || v.gaps.length) process.exitCode = 1
|
|
708
|
+
return r
|
|
709
|
+
}
|
|
710
|
+
case 'protect': {
|
|
711
|
+
// seam protect [list] | add <path> <who> <level> [--exclude] [--at n] [--note why] | remove <n> | set <file.json> | reset | check [<path>]
|
|
712
|
+
const sub = pos[0] || 'list'
|
|
713
|
+
const q = needQuilt()
|
|
714
|
+
if (sub === 'list') { const r = await client.protections(q); return print(r, describeProtections(r)) }
|
|
715
|
+
if (sub === 'check') { const r = await client.access(q, pos[1] || ''); return print(r, describeAccess(r)) }
|
|
716
|
+
if (sub === 'reset') { const r = await client.setProtections(q, null); return print(r, describeProtections(r)) }
|
|
717
|
+
if (sub === 'set') {
|
|
718
|
+
if (!pos[1]) throw new Error('seam protect set <file.json>: a list of { path, principal, level, exclude? }')
|
|
719
|
+
const rows = JSON.parse(fs.readFileSync(path.resolve(cwd, pos[1]), 'utf8'))
|
|
720
|
+
const r = await client.setProtections(q, Array.isArray(rows) ? rows : rows.rows)
|
|
721
|
+
return print(r, describeProtections(r))
|
|
722
|
+
}
|
|
723
|
+
const cur = (await client.protections(q)).rows.map(({ n, ...x }) => x)
|
|
724
|
+
if (sub === 'add') {
|
|
725
|
+
if (pos.length < 4) throw new Error("seam protect add <path> <who> <level> [--exclude] [--at n]: who is everyone, people, agents, role:read|write|admin, team:<name>, person:<id>, agent:<id>, token:<id> or (an org's quilt) org_role:member|admin|owner; path '' is the whole quilt")
|
|
726
|
+
const row = { path: pos[1] === "''" ? '' : pos[1], principal: pos[2], level: pos[3], exclude: !!flags['--exclude'], ...(flags['--note'] ? { note: flags['--note'] } : {}) }
|
|
727
|
+
const at = flags['--at'] ? Number(flags['--at']) : cur.length + 1
|
|
728
|
+
if (!Number.isInteger(at) || at < 1 || at > cur.length + 1) throw new Error(`--at is a position from 1 to ${cur.length + 1}`)
|
|
729
|
+
cur.splice(at - 1, 0, row)
|
|
730
|
+
const r = await client.setProtections(q, cur)
|
|
731
|
+
return print(r, describeProtections(r))
|
|
732
|
+
}
|
|
733
|
+
if (sub === 'remove') {
|
|
734
|
+
const n = Number(pos[1])
|
|
735
|
+
if (!Number.isInteger(n) || n < 1 || n > cur.length) throw new Error(`seam protect remove <n>: a row number from 1 to ${cur.length}`)
|
|
736
|
+
cur.splice(n - 1, 1)
|
|
737
|
+
const r = await client.setProtections(q, cur)
|
|
738
|
+
return print(r, describeProtections(r))
|
|
739
|
+
}
|
|
740
|
+
throw new Error('seam protect [list] | add <path> <who> <level> [--exclude] [--at n] | remove <n> | set <file.json> | reset | check [<path>]')
|
|
741
|
+
}
|
|
742
|
+
case 'team': {
|
|
743
|
+
// seam team [list] | set <name> <member>... | remove <name>
|
|
744
|
+
const sub = pos[0] || 'list'
|
|
745
|
+
if (sub === 'list') { const r = await client.teams(needQuilt()); return print(r, describeTeams(r)) }
|
|
746
|
+
if (sub === 'set') {
|
|
747
|
+
if (!pos[1] || pos.length < 3) throw new Error('seam team set <name> <member> [<member>...]')
|
|
748
|
+
const r = await client.setTeam(needQuilt(), pos[1], pos.slice(2))
|
|
749
|
+
return print(r, describeTeams(r))
|
|
750
|
+
}
|
|
751
|
+
if (sub === 'remove') {
|
|
752
|
+
if (!pos[1]) throw new Error('seam team remove <name>')
|
|
753
|
+
const r = await client.setTeam(needQuilt(), pos[1], [])
|
|
754
|
+
return print(r, describeTeams(r))
|
|
755
|
+
}
|
|
756
|
+
throw new Error('seam team [list] | set <name> <member>... | remove <name>')
|
|
337
757
|
}
|
|
338
758
|
case 'key': {
|
|
339
759
|
// seam key [list [member]] | add [--name <n>] [--public-key <file>] | remove <key>
|
|
@@ -353,18 +773,49 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
353
773
|
}
|
|
354
774
|
case 'log': {
|
|
355
775
|
const rev = pos[0] || (block?.square ? block.square_short : block?.line) || 'main'
|
|
356
|
-
|
|
357
|
-
|
|
776
|
+
// Filters: --grep <text> (-E regex, -i any case), -G <regex>, -S <text>, --author, --on-behalf-of, --task, --since, --until.
|
|
777
|
+
const str = (k) => typeof flags[k] === 'string' ? flags[k] : undefined
|
|
778
|
+
const r = await client.log(needQuilt(), {
|
|
779
|
+
rev, paths, limit: flags['-n'] || flags['--max-count'] || 20, follow: flags['--follow'] ? true : undefined, show_signature: flags['--show-signature'] ? true : undefined, search: str('-S'),
|
|
780
|
+
message: str('--grep'), message_regex: str('--grep') && flags['-E'] ? 1 : undefined, message_ignore_case: str('--grep') && flags['-i'] ? 1 : undefined,
|
|
781
|
+
pickaxe: str('-G'), pickaxe_regex: str('-G') ? 1 : undefined, pickaxe_mode: str('-G') ? 'match' : undefined,
|
|
782
|
+
author: str('--author'), on_behalf_of: str('--on-behalf-of'), task: str('--task'), since: str('--since'), until: str('--until'),
|
|
783
|
+
group_by: str('--group-by'), graph: flags['--graph'] ? 1 : undefined, include_hidden: flags['--include-hidden'] ? 1 : undefined, cursor: str('--cursor')
|
|
784
|
+
})
|
|
785
|
+
return print(r, describeLog(r, { oneline: !!flags['--oneline'], graph: !!flags['--graph'] }))
|
|
358
786
|
}
|
|
359
787
|
case 'describe': {
|
|
360
788
|
const r = await client.describe(needQuilt(), pos[0] || (block?.square ? block.square_short : block?.line) || 'main')
|
|
361
789
|
return print(r, r.describe)
|
|
362
790
|
}
|
|
791
|
+
case 'stack': {
|
|
792
|
+
// seam stack [rev]: the full-stack pointers at a revision (db_migration, db_branch, infra, …).
|
|
793
|
+
const r = await client.stack(needQuilt(), pos[0] || (block?.square ? block.square_short : block?.line) || 'main')
|
|
794
|
+
return print(r, describeStack(r))
|
|
795
|
+
}
|
|
363
796
|
case 'merge-base': {
|
|
364
|
-
if (pos.length !== 2) throw new Error('seam merge-base <revision> <revision>')
|
|
797
|
+
if (pos.length !== 2) throw new Error('seam merge-base [--is-ancestor] <revision> <revision>')
|
|
798
|
+
if (flags['--is-ancestor']) {
|
|
799
|
+
// git merge-base --is-ancestor a b: whether a is in b's history.
|
|
800
|
+
const r = await client.ancestor(needQuilt(), pos[0], pos[1])
|
|
801
|
+
return print(r, describeAncestor(r, pos[0], pos[1]))
|
|
802
|
+
}
|
|
365
803
|
const r = await client.mergeBase(needQuilt(), pos[0], pos[1])
|
|
366
804
|
return print(r, describeMergeBase(r))
|
|
367
805
|
}
|
|
806
|
+
case 'contains': {
|
|
807
|
+
// seam contains <rev>: the lines whose history has it (git branch --contains), and where it was cherry-picked or reverted.
|
|
808
|
+
if (pos.length !== 1) throw new Error('seam contains <revision>')
|
|
809
|
+
const r = await client.contains(needQuilt(), pos[0])
|
|
810
|
+
return print(r, describeContains(r))
|
|
811
|
+
}
|
|
812
|
+
case 'search': {
|
|
813
|
+
// seam search <query> [--at <rev>] [--all | --quilts a,b]: words, "phrases", /regex/, path:, lang:, symbol:, class:, -term, OR
|
|
814
|
+
if (!pos.length) throw new Error('seam search <query> [--at <rev>] [--all | --quilts a,b] e.g. seam search "symbol:mergeSquare lang:js"')
|
|
815
|
+
const q = pos.join(' ')
|
|
816
|
+
const r = flags['--all'] || flags['--quilts'] ? await client.search({ q, rev: flags['--at'], quilts: flags['--quilts'] ? String(flags['--quilts']).split(',') : undefined, limit: flags['--max-count'] }) : await client.searchCode(needQuilt(), { q, rev: flags['--at'] || (block?.square ? block.square_short : undefined), limit: flags['--max-count'] })
|
|
817
|
+
return print(r, describeSearch(r))
|
|
818
|
+
}
|
|
368
819
|
case 'grep': {
|
|
369
820
|
// seam grep <pattern> [revision] [-- paths]: -i ignores case, -F takes the pattern as plain text, -l lists files.
|
|
370
821
|
if (!pos[0]) throw new Error('seam grep <pattern> [revision] [-i] [-F] [-l] [--all] [-- paths]')
|
|
@@ -379,16 +830,16 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
379
830
|
return print(r, describeGrep(r, { filesOnly: !!flags['-l'] }))
|
|
380
831
|
}
|
|
381
832
|
case 'bisect': {
|
|
382
|
-
// seam bisect <bad> <good> --run "<check>": exit 0 good, 125 skip, else bad. Without --run, one step (--marks id:good,id:bad).
|
|
833
|
+
// seam bisect <bad> <good> --run "<check>" [-- paths]: exit 0 good, 125 skip, else bad. Without --run, one step (--marks id:good,id:bad). Paths narrow the commits tested.
|
|
383
834
|
if (pos.length !== 2) throw new Error('seam bisect <bad> <good> --run "<check command>" (or --marks <commit>:good,<commit>:bad for one step)')
|
|
384
835
|
const marks = Object.fromEntries(String(flags['--marks'] || '').split(',').filter(Boolean).map((x) => x.split(':')))
|
|
385
836
|
if (typeof flags['--run'] === 'string') {
|
|
386
837
|
const { bisectRun } = await import('./bisect-run.js')
|
|
387
|
-
const r = await bisectRun(client, { quilt: needQuilt(), bad: pos[0], good: pos[1], run: flags['--run'], marks, api, log: (s) => { if (!json) out(s) } })
|
|
838
|
+
const r = await bisectRun(client, { quilt: needQuilt(), bad: pos[0], good: pos[1], run: flags['--run'], marks, only: paths, api, log: (s) => { if (!json) out(s) } })
|
|
388
839
|
return print(r, describeBisect(r))
|
|
389
840
|
}
|
|
390
|
-
const r = await client.bisect(needQuilt(), { bad: pos[0], good: pos[1], marks })
|
|
391
|
-
return print(r, describeBisect(r) + (r.done ? '' : `\nThen: seam bisect ${pos[0]} ${pos[1]} --marks ${[...Object.entries(marks).map(([k, v]) => `${k}:${v}`), `${r.next.short}:<good|bad|skip>`].join(',')}`))
|
|
841
|
+
const r = await client.bisect(needQuilt(), { bad: pos[0], good: pos[1], marks, paths })
|
|
842
|
+
return print(r, describeBisect(r) + (r.done ? '' : `\nThen: seam bisect ${pos[0]} ${pos[1]} --marks ${[...Object.entries(marks).map(([k, v]) => `${k}:${v}`), `${r.next.short}:<good|bad|skip>`].join(',')}${paths.length ? ` -- ${paths.join(' ')}` : ''}`))
|
|
392
843
|
}
|
|
393
844
|
case 'show': {
|
|
394
845
|
const rev = pos[0] || (block?.square ? block.square_short : 'main')
|
|
@@ -403,7 +854,16 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
403
854
|
return print(d, format === 'unified' ? (d.text || '') : d.changes.map((c) => `${c.status[0].toUpperCase()}\t${c.path}`).join('\n'))
|
|
404
855
|
}
|
|
405
856
|
const [from, to] = pos.length === 1 && pos[0].includes('..') ? pos[0].split('..') : pos.length === 1 ? [pos[0], block?.square ? block.square_short : 'main'] : pos
|
|
406
|
-
|
|
857
|
+
// --stack: which full-stack pointers (db_migration, db_branch, …) changed, not the files.
|
|
858
|
+
if (flags['--stack']) { const r = await client.stackDiff(needQuilt(), from, to); return print(r, describeStackDiff(r)) }
|
|
859
|
+
// --renames off|exact|similar (default similar; --no-renames: off), -w / --ignore-space-at-eol, -U <n>; every page unless --cursor or -n.
|
|
860
|
+
const opts = { from, to, paths, format, renames: flags['--no-renames'] ? 'off' : flags['--renames'] || 'similar', ignore_whitespace: flags['-w'] || flags['--ignore-all-space'] ? 'all' : flags['--ignore-space-at-eol'] ? 'trailing' : undefined, context: flags['-U'] ?? flags['--unified'], limit: flags['-n'] || flags['--max-count'], cursor: flags['--cursor'] }
|
|
861
|
+
const r = await client.diff(needQuilt(), opts)
|
|
862
|
+
for (let page = 0; r.cursor && !opts.cursor && !opts.limit && page < 100; page++) {
|
|
863
|
+
const more = await client.diff(needQuilt(), { ...opts, cursor: r.cursor })
|
|
864
|
+
r.files.push(...more.files); r.add += more.add || 0; r.del += more.del || 0; r.renames = (r.renames || 0) + (more.renames || 0)
|
|
865
|
+
r.cursor = more.cursor
|
|
866
|
+
}
|
|
407
867
|
return print(r, describeDiff(r, { format }))
|
|
408
868
|
}
|
|
409
869
|
case 'merge': {
|
|
@@ -421,8 +881,13 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
421
881
|
return print(r, text)
|
|
422
882
|
}
|
|
423
883
|
const sqId = pos[0] && pos[0].startsWith('sq:') ? pos[0] : needSquare()
|
|
424
|
-
const r = await client.merge(needQuilt(), into, { square: sqId, ...(found && sqId === block.square ? { expect_head: block.head } : {}), message, dry_run: !!flags['--dry-run'], on_conflict: flags['--on-conflict'], expect_tip: flags['--expect-tip'] })
|
|
884
|
+
const r = await client.merge(needQuilt(), into, { square: sqId, ...(found && sqId === block.square ? { expect_head: block.head } : {}), message, dry_run: !!flags['--dry-run'], on_conflict: flags['--on-conflict'], expect_tip: flags['--expect-tip'], ...(flags['--no-commit-live'] ? { commit_live: false } : {}) })
|
|
425
885
|
let text = describeMerge(r)
|
|
886
|
+
// Merged from its own folder: the folder moves to the line's new tip, ready for the next change.
|
|
887
|
+
if (found && sqId === block.square && r.ok && r.merged?.commit && !flags['--dry-run']) {
|
|
888
|
+
const s = await toLineTip(client, found.dir, block, r.merged.line || into, r.merged.commit)
|
|
889
|
+
text += `\nThe folder is now on ${r.merged.line || into} (${s.written} file(s) updated); your next commit opens a new square.`
|
|
890
|
+
}
|
|
426
891
|
// The refused merge left the square rebased onto the tip with its conflicts: bring the folder there.
|
|
427
892
|
if (found && sqId === block.square && !r.ok && r.head && r.head !== block.head) {
|
|
428
893
|
const s = await syncTo(client, found.dir, block, r.head, { square: r.square })
|
|
@@ -447,12 +912,21 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
447
912
|
if (!pos[0]) throw new Error('seam revert <patch:n | revision>')
|
|
448
913
|
const r = await client.revert(needQuilt(), { rev: pos[0], line: flags['--line'] })
|
|
449
914
|
let text = describeWrite(r, 'Reverted in a new square') + `\nMerge it with: seam merge ${r.square.short} --into ${r.square.line}`
|
|
450
|
-
if (flags['--dir']) { const dir = path.resolve(cwd, flags['--dir']); await checkout(client, { quilt: needQuilt(), square: r.square.id, dir, api }); text += `\nChecked out into ${dir}.` }
|
|
915
|
+
if (flags['--dir']) { const dir = path.resolve(cwd, flags['--dir']); await checkout(client, { quilt: needQuilt(), square: r.square.id, dir, api, codec: await codecFor(client, needQuilt()) }); text += `\nChecked out into ${dir}.` }
|
|
451
916
|
return print(r, text)
|
|
452
917
|
}
|
|
453
918
|
case 'resolve': {
|
|
454
|
-
const
|
|
455
|
-
if (
|
|
919
|
+
const rule = flags['--rule']
|
|
920
|
+
if (rule !== undefined) {
|
|
921
|
+
if (!pos.length) throw new Error('seam resolve <path>... --rule additive|whitespace|words|union')
|
|
922
|
+
const rel = (p) => found ? path.relative(found.dir, path.resolve(cwd, p)).split(path.sep).join('/') : p
|
|
923
|
+
const r = await client.resolve(needQuilt(), needSquare(), { ...(found ? { expect_head: block.head } : {}), entries: pos.map((p) => ({ path: rel(p), rule })), ...(flags['--no-remember'] ? { remember: false } : {}) })
|
|
924
|
+
let text = describeWrite(r, 'Resolved')
|
|
925
|
+
if (found) { const s = await syncTo(client, found.dir, block, r.square.head, { square: r.square }); text += `\n${s.written} file(s) updated in the folder.` }
|
|
926
|
+
return print(r, text)
|
|
927
|
+
}
|
|
928
|
+
const choose = flags['--ours'] ? 'ours' : flags['--theirs'] ? 'theirs' : flags['--base'] ? 'base' : flags['--delete'] ? 'delete' : flags['--suggestion'] ? 'suggestion' : flags['--keep-both'] ? 'keep_both' : flags['--side'] != null ? Number(flags['--side']) : null
|
|
929
|
+
if (choose === null || !pos.length) throw new Error('seam resolve <path>... --ours | --theirs | --base | --suggestion | --side n | --delete | --keep-both | --rule <step> [--no-remember] (or edit the file and commit it without the markers)')
|
|
456
930
|
const rel = (p) => found ? path.relative(found.dir, path.resolve(cwd, p)).split(path.sep).join('/') : p
|
|
457
931
|
const r = await client.resolve(needQuilt(), needSquare(), { ...(found ? { expect_head: block.head } : {}), entries: pos.map((p) => ({ path: rel(p), choose })), ...(flags['--no-remember'] ? { remember: false } : {}) })
|
|
458
932
|
let text = describeWrite(r, 'Resolved')
|
|
@@ -464,9 +938,9 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
464
938
|
if (pos[0] === 'forget') {
|
|
465
939
|
if (!pos[1]) throw new Error('Which one? seam resolutions forget <key>')
|
|
466
940
|
const r = await client.forgetResolution(needQuilt(), pos[1])
|
|
467
|
-
return print(r, `Forgot ${r.forgotten.short} (${r.forgotten.path}): that conflict will be asked about again.`)
|
|
941
|
+
return print(r, `Forgot ${r.forgotten.short} (${r.forgotten.path}): that conflict will be asked about again${r.hunks ? ` (with the ${r.hunks} region${r.hunks === 1 ? '' : 's'} remembered with it)` : ''}.`)
|
|
468
942
|
}
|
|
469
|
-
const rows = await client.resolutions(needQuilt(), pos[0])
|
|
943
|
+
const rows = await client.resolutions(needQuilt(), pos[0], { hunks: !!flags['--hunks'] })
|
|
470
944
|
return print(rows, describeResolutions(rows))
|
|
471
945
|
}
|
|
472
946
|
case 'blame': {
|
|
@@ -475,16 +949,28 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
475
949
|
const at = arg.lastIndexOf('@')
|
|
476
950
|
const [p, rev] = at > 0 ? [arg.slice(0, at), arg.slice(at + 1)] : [arg, pos[1] || (block?.square ? block.square_short : 'main')]
|
|
477
951
|
const rel = found && at < 0 ? path.relative(found.dir, path.resolve(cwd, p)).split(path.sep).join('/') : p
|
|
478
|
-
|
|
952
|
+
// -L a,b: a span; --ignore-rev <rev> (again for more; .seam-blame-ignore is read too); -M: moves keep their origin; --reverse <rev>
|
|
953
|
+
const ignore = typeof flags['--ignore-rev'] === 'string' ? flags['--ignore-rev'].split(',').map((x) => x.trim()).filter(Boolean) : []
|
|
954
|
+
const r = await client.blame(needQuilt(), rev, rel, { ...(flags['-L'] ? { range: String(flags['-L']) } : {}), ...(ignore.length ? { ignore } : {}), ...(flags['-M'] ? { moves: true } : {}), ...(flags['--reverse'] ? { reverse: String(flags['--reverse']) } : {}) })
|
|
479
955
|
return print(r, describeBlame(r))
|
|
480
956
|
}
|
|
481
957
|
case 'cat': {
|
|
482
958
|
const arg = pos[0] || ''
|
|
483
959
|
const at = arg.lastIndexOf('@')
|
|
484
960
|
const [p, rev] = at > 0 ? [arg.slice(0, at), arg.slice(at + 1)] : [arg, 'main']
|
|
485
|
-
const
|
|
961
|
+
const ranged = flags['--offset'] != null || flags['--length'] != null
|
|
962
|
+
let r
|
|
963
|
+
try {
|
|
964
|
+
r = await client.cat(needQuilt(), rev, p, { format: flags['--format'] || conflictStyle(), ...(flags['--offset'] != null ? { offset: flags['--offset'] } : {}), ...(flags['--length'] != null ? { length: flags['--length'] } : {}) })
|
|
965
|
+
} catch (err) {
|
|
966
|
+
// A file too large for one JSON answer (10) streams to stdout as it comes.
|
|
967
|
+
if (err.code !== 'too_large' || json || ranged) throw err
|
|
968
|
+
const s = await client.catRaw(needQuilt(), rev, p)
|
|
969
|
+
for await (const piece of s.body) if (!process.stdout.write(piece)) await new Promise((resolve) => process.stdout.once('drain', resolve))
|
|
970
|
+
return
|
|
971
|
+
}
|
|
486
972
|
if (json) return print(r, '')
|
|
487
|
-
process.stdout.write(
|
|
973
|
+
process.stdout.write(await catBytes(client, needQuilt(), r, r.sealed ? await codecFor(client, needQuilt()) : null))
|
|
488
974
|
return
|
|
489
975
|
}
|
|
490
976
|
case 'ls': {
|
|
@@ -504,12 +990,22 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
504
990
|
return print(g, g.installations.map((i) => `${i.login} (installation ${i.id})\n${i.repos.map((r) => ` ${r.full_name}${r.private ? ' (private)' : ''}`).join('\n') || ' no repositories'}`).join('\n'))
|
|
505
991
|
}
|
|
506
992
|
case 'import': {
|
|
993
|
+
// --authors <file>: a JSON object of git author email → member id (14, "Authors to members").
|
|
994
|
+
const readAuthors = () => {
|
|
995
|
+
if (!flags['--authors'] || flags['--authors'] === true) return undefined
|
|
996
|
+
try { return JSON.parse(fs.readFileSync(path.resolve(cwd, flags['--authors']), 'utf8')) } catch (err) { throw new Error(`--authors ${flags['--authors']}: a JSON file of { "email": "member id" } (${err.message})`) }
|
|
997
|
+
}
|
|
507
998
|
// From a git remote, on the server, and following it (14): status and poke name the import.
|
|
508
999
|
if (pos[0] === 'cutover') {
|
|
509
1000
|
if (!pos[1]) throw new Error('seam import cutover <import-id> [--credential <name>]')
|
|
510
1001
|
const r = await client.cutover(pos[1], { credential: flags['--credential'] || undefined })
|
|
511
1002
|
return print(r, `Cut over: import ${r.import.id} is done for good, its lines take merges in Seam, and main exports to ${r.export.target} after each merge.\nseam export status --quilt ${r.import.id}`)
|
|
512
1003
|
}
|
|
1004
|
+
if (pos[0] === 'authors') {
|
|
1005
|
+
if (!pos[1] || !flags['--authors']) throw new Error('seam import authors <import-id> --authors <file>')
|
|
1006
|
+
const r = await client.answerImportAuthors(pos[1], { authors: readAuthors() })
|
|
1007
|
+
return print(r, `Import ${r.id} has its authors and runs now.\nseam import status ${r.id}`)
|
|
1008
|
+
}
|
|
513
1009
|
if (pos[0] === 'status' || pos[0] === 'poke') {
|
|
514
1010
|
if (!pos[1]) throw new Error(`seam import ${pos[0]} <import-id>`)
|
|
515
1011
|
const r = pos[0] === 'status' ? await client.importStatus(pos[1]) : await client.pokeImport(pos[1])
|
|
@@ -517,28 +1013,75 @@ export async function runSeamCommand (cmd, argv, { cwd = process.cwd(), token, o
|
|
|
517
1013
|
}
|
|
518
1014
|
if (pos[0] === 'git' && pos[1] && (flags['--follow'] || flags['--remote'] || /^(https?|ssh|git):\/\/|^[\w.-]+@[\w.-]+:/.test(pos[1]))) {
|
|
519
1015
|
const slug = flags['--into'] || path.basename(String(pos[1]).replace(/\.git$/, '').replace(/\/+$/, '')).toLowerCase().replace(/[^a-z0-9-]/g, '-')
|
|
520
|
-
const r = await client.startImport({ url: pos[1], slug, follow: !!flags['--follow'], interval: flags['--interval'] ? Number(flags['--interval']) : undefined, credential: flags['--credential'] || undefined })
|
|
521
|
-
return print(r, `Importing ${pos[1]} into ${slug} on the server (import ${r.id}).${r.follow ? ` It follows main and release/* every ${r.interval_s} s; they are frozen until cut-over.\nPoke it from a git hook or cron job with: curl -X POST -H 'Authorization: Bearer ${r.poke_secret}' <server>/v1/imports/${r.id}/poke (the secret is shown once)` : ''}\nseam import status ${r.id}`)
|
|
1016
|
+
const r = await client.startImport({ url: pos[1], slug, authors: readAuthors(), ask_authors: !!flags['--ask-authors'], follow: !!flags['--follow'], interval: flags['--interval'] ? Number(flags['--interval']) : undefined, credential: flags['--credential'] || undefined })
|
|
1017
|
+
return print(r, `Importing ${pos[1]} into ${slug} on the server (import ${r.id}).${r.ask_authors ? ' It waits for you to answer its authors first (seam import status, then seam import authors).' : ''}${r.follow ? ` It follows main and release/* every ${r.interval_s} s; they are frozen until cut-over.\nPoke it from a git hook or cron job with: curl -X POST -H 'Authorization: Bearer ${r.poke_secret}' <server>/v1/imports/${r.id}/poke (the secret is shown once)` : ''}\nseam import status ${r.id}`)
|
|
522
1018
|
}
|
|
523
|
-
if (pos[0] !== 'git' || !pos[1]) throw new Error('seam import git <path> [--into <quilt-slug>] [--validate] | seam import git <remote-url> [--into <slug>] [--follow] [--interval <s>] [--credential <name>] | seam import status|poke <import-id>')
|
|
1019
|
+
if (pos[0] !== 'git' || !pos[1]) throw new Error('seam import git <path> [--into <quilt-slug>] [--validate] [--authors <file>] | seam import git <remote-url> [--into <slug>] [--authors <file>] [--ask-authors] [--follow] [--interval <s>] [--credential <name>] | seam import authors <import-id> --authors <file> | seam import status|poke <import-id>')
|
|
524
1020
|
const { importGit } = await import('./import-git.js')
|
|
525
1021
|
const repo = path.resolve(cwd, pos[1])
|
|
526
1022
|
if (!fs.existsSync(path.join(repo, '.git')) && !fs.existsSync(path.join(repo, 'HEAD'))) throw new Error(`${repo} is not a git repository`)
|
|
527
|
-
const r = await importGit(client, { repo, slug: flags['--into'] || path.basename(repo).toLowerCase().replace(/[^a-z0-9-]/g, '-'), log: (s) => { if (!json) out(s) }, validate: !!flags['--validate'], batch: Number(flags['--batch']) || 100 })
|
|
1023
|
+
const r = await importGit(client, { repo, authors: readAuthors(), slug: flags['--into'] || path.basename(repo).toLowerCase().replace(/[^a-z0-9-]/g, '-'), log: (s) => { if (!json) out(s) }, validate: !!flags['--validate'], batch: Number(flags['--batch']) || 100 })
|
|
528
1024
|
return print(r, describeImport(r))
|
|
529
1025
|
}
|
|
530
1026
|
case 'export': {
|
|
531
1027
|
// To a git remote, on the server, after each merge (14): seam export remote <url> | status.
|
|
532
1028
|
if (pos[0] === 'remote' && pos[1]) {
|
|
533
|
-
const r = await client.setExport(needQuilt(), { target: pos[1], lines: flags['--line'] ? String(flags['--line']).split(',') : undefined, releases: !!flags['--releases'], credential: flags['--credential'] || undefined, scratch: !!flags['--scratch'], enabled: !flags['--off'] })
|
|
534
|
-
return print(r, r.enabled ? `Exporting ${r.lines.join(', ')} to ${r.target} after each merge${r.releases ? ', with release tags' : ''}. seam export status` : `The export to ${r.target} is off.`)
|
|
1029
|
+
const r = await client.setExport(needQuilt(), { target: pos[1], lines: flags['--line'] ? String(flags['--line']).split(',') : undefined, releases: !!flags['--releases'], credential: flags['--credential'] || undefined, scratch: !!flags['--scratch'], enabled: !flags['--off'], lfs: flags['--lfs-url'] ? { url: String(flags['--lfs-url']), ...(flags['--lfs-threshold'] != null ? { threshold_bytes: Number(flags['--lfs-threshold']) } : {}) } : undefined })
|
|
1030
|
+
return print(r, r.enabled ? `Exporting ${r.lines.join(', ')} to ${r.target} after each merge${r.releases ? ', with release tags' : ''}${r.lfs ? `, files over ${r.lfs.threshold_bytes} bytes to LFS at ${r.lfs.url}` : ''}. seam export status` : `The export to ${r.target} is off.`)
|
|
535
1031
|
}
|
|
536
1032
|
if (pos[0] === 'status') {
|
|
537
1033
|
const r = await client.exportStatus(needQuilt())
|
|
538
1034
|
const st = r.status || {}
|
|
539
|
-
return print(r, [`Export to ${r.target} (${r.lines.join(', ')}${r.releases ? ', releases' : ''}${r.enabled ? '' : ', off'}): ${st.state || 'idle'}${st.error ? ` - ${st.error}` : ''}`, ...Object.entries(st.lines || {}).map(([l, x]) => ` ${l}: patch ${x.patch_no} -> ${String(x.sha || '').slice(0, 12)}`)].join('\n'))
|
|
1035
|
+
return print(r, [`Export to ${r.target} (${r.lines.join(', ')}${r.releases ? ', releases' : ''}${r.enabled ? '' : ', off'}): ${st.state || 'idle'}${st.error ? ` - ${st.error}` : ''}`, ...Object.entries(st.lines || {}).map(([l, x]) => ` ${l}: patch ${x.patch_no} -> ${String(x.sha || '').slice(0, 12)}`), ...(st.signing_key ? [` signed with ${st.signing_key}`] : [])].join('\n'))
|
|
1036
|
+
}
|
|
1037
|
+
// One commit as a .tar.gz (git archive): seam export tarball [rev] [--paths a,b] [--out file].
|
|
1038
|
+
if (pos[0] === 'tarball') {
|
|
1039
|
+
const r = await client.tarball(needQuilt(), pos[1] || 'main', { paths: flags['--paths'] ? String(flags['--paths']).split(',') : undefined })
|
|
1040
|
+
const file = path.resolve(cwd, flags['--out'] || r.name)
|
|
1041
|
+
fs.writeFileSync(file, r.bytes)
|
|
1042
|
+
return print({ file, commit: r.commit, bytes: r.bytes.length }, `Wrote ${pos[1] || 'main'} (${String(r.commit || 'empty').slice(0, 12)}) to ${path.relative(cwd, file) || file}: ${r.bytes.length} bytes.`)
|
|
1043
|
+
}
|
|
1044
|
+
// The whole quilt as one .tar (14, exit path; admin): seam export archive [--out file].
|
|
1045
|
+
if (pos[0] === 'archive') {
|
|
1046
|
+
const r = await client.exitArchive(needQuilt())
|
|
1047
|
+
const file = path.resolve(cwd, flags['--out'] || r.name)
|
|
1048
|
+
fs.writeFileSync(file, r.bytes)
|
|
1049
|
+
return print({ file, bytes: r.bytes.length }, `Wrote the whole quilt to ${path.relative(cwd, file) || file}: ${r.bytes.length} bytes. Check it any time with seam export check ${path.relative(cwd, file) || file}`)
|
|
1050
|
+
}
|
|
1051
|
+
// A commit's files out of an archive, offline: seam export extract <file> [rev] --to <dir>.
|
|
1052
|
+
if (pos[0] === 'extract') {
|
|
1053
|
+
if (!pos[1] || !flags['--to']) throw new Error('seam export extract <file> [rev] --to <dir>')
|
|
1054
|
+
const { readArchive, snapshotIn } = await import('./exit-archive.js')
|
|
1055
|
+
const a = await readArchive(fs.readFileSync(path.resolve(cwd, pos[1])))
|
|
1056
|
+
const s = snapshotIn(a, pos[2] || 'main')
|
|
1057
|
+
const dir = path.resolve(cwd, flags['--to'])
|
|
1058
|
+
for (const f of s.files) {
|
|
1059
|
+
const file = path.resolve(dir, f.path)
|
|
1060
|
+
if (!file.startsWith(dir + path.sep)) throw new Error(`${f.path} would land outside ${flags['--to']}`)
|
|
1061
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
1062
|
+
if (f.mode === 'symlink') fs.symlinkSync(f.bytes.toString('utf8'), file)
|
|
1063
|
+
else fs.writeFileSync(file, f.bytes, { mode: f.mode === 'exec' ? 0o755 : 0o644 })
|
|
1064
|
+
}
|
|
1065
|
+
return print({ commit: s.commit, files: s.files.length, dir }, `Took ${s.files.length} files of ${pos[2] || 'main'} (${s.commit.slice(0, 12)}) out of ${pos[1]} into ${path.relative(cwd, dir) || dir}.`)
|
|
1066
|
+
}
|
|
1067
|
+
// A line's git history rebuilt from an archive, offline: seam export git <dir> --from <archive>.
|
|
1068
|
+
if (pos[0] === 'git' && pos[1] && flags['--from']) {
|
|
1069
|
+
const { readArchive, archiveClient } = await import('./exit-archive.js')
|
|
1070
|
+
const { exportGit } = await import('./export-git.js')
|
|
1071
|
+
const a = await readArchive(fs.readFileSync(path.resolve(cwd, flags['--from'])))
|
|
1072
|
+
const r = await exportGit(archiveClient(a), { q: a.manifest.quilt.id, dir: path.resolve(cwd, pos[1]), line: flags['--line'] || 'main', branch: flags['--branch'] || undefined, log: (s) => { if (!json) out(s) } })
|
|
1073
|
+
return print(r, `Rebuilt ${r.exported} patches of ${flags['--line'] || 'main'} from ${flags['--from']} as ${r.branch} in ${pos[1]}: ${r.sha || 'empty'} (patch ${r.patch_no}).`)
|
|
1074
|
+
}
|
|
1075
|
+
// Reads an archive offline and checks it: seam export check <file>.
|
|
1076
|
+
if (pos[0] === 'check') {
|
|
1077
|
+
if (!pos[1]) throw new Error('seam export check <file>')
|
|
1078
|
+
const { readArchive } = await import('./exit-archive.js')
|
|
1079
|
+
const a = await readArchive(fs.readFileSync(path.resolve(cwd, pos[1])))
|
|
1080
|
+
const lines = [...a.lines.values()].map((l) => `${l.name} at patch ${l.patch_count || 0}`)
|
|
1081
|
+
const r = { quilt: a.manifest.quilt, rows: a.log.length, commits: a.commits.size, packs: a.packs.length, members: a.members.length, lines: [...a.lines.values()].map((l) => ({ name: l.name, tip: l.tip, patch_count: l.patch_count || 0 })) }
|
|
1082
|
+
return print(r, `${pos[1]} is whole: quilt ${a.manifest.quilt.slug || a.manifest.quilt.id}, ${r.rows} log rows, ${r.commits} commits, ${r.packs} packs, ${r.members} members. Lines: ${lines.join(', ') || 'none'}.`)
|
|
540
1083
|
}
|
|
541
|
-
if (pos[0] !== 'git' || !pos[1]) throw new Error('seam export git <dir> [--line main] [--branch <name>]')
|
|
1084
|
+
if (pos[0] !== 'git' || !pos[1]) throw new Error('seam export git <dir> [--line main] [--branch <name>] | seam export tarball [rev] [--paths a,b] [--out file] | seam export archive [--out file] | seam export check <file> | seam export extract <file> [rev] --to <dir>')
|
|
542
1085
|
const { exportGit } = await import('./export-git.js')
|
|
543
1086
|
const r = await exportGit(client, { q: needQuilt(), dir: path.resolve(cwd, pos[1]), line: flags['--line'] || 'main', branch: flags['--branch'] || undefined, log: (s) => { if (!json) out(s) } })
|
|
544
1087
|
return print(r, r.exported ? `Exported ${r.exported} patches to ${r.branch} in ${pos[1]}: ${r.branch} is ${r.sha} (patch ${r.patch_no}).` : `Nothing new: ${r.branch} is ${r.sha || 'empty'} (patch ${r.patch_no}).`)
|