@shardflux/cli 0.24.1 → 0.28.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/CHANGELOG.md +159 -1
- package/README.md +182 -35
- package/dist/account-pool.d.ts +11 -1
- package/dist/account-pool.js +48 -0
- package/dist/acp-journal.js +23 -1
- package/dist/agent-commands.js +4 -3
- package/dist/agent-models.d.ts +31 -1
- package/dist/agent-models.js +27 -4
- package/dist/args.d.ts +1 -1
- package/dist/args.js +97 -14
- package/dist/board-args.js +26 -4
- package/dist/board-commands.d.ts +9 -5
- package/dist/board-commands.js +43 -16
- package/dist/brain-commands.d.ts +2 -0
- package/dist/brain-commands.js +78 -0
- package/dist/client-settings.d.ts +2 -1
- package/dist/client-settings.js +14 -2
- package/dist/codex-cloud.d.ts +2 -0
- package/dist/codex-cloud.js +14 -11
- package/dist/commands.js +8 -0
- package/dist/conversation-commands.d.ts +5 -1
- package/dist/conversation-commands.js +76 -8
- package/dist/conversation-terminal.js +2 -2
- package/dist/conversations.d.ts +2 -1
- package/dist/desktop-linux.d.ts +12 -1
- package/dist/desktop-linux.js +47 -18
- package/dist/device-approvals.js +24 -2
- package/dist/device-bridge.d.ts +21 -4
- package/dist/device-bridge.js +173 -7
- package/dist/device-folders.d.ts +14 -1
- package/dist/device-folders.js +20 -8
- package/dist/device-helper.d.ts +8 -0
- package/dist/device-helper.js +83 -13
- package/dist/device-mode.d.ts +44 -0
- package/dist/device-mode.js +119 -0
- package/dist/drive-commands.d.ts +2 -2
- package/dist/drive-commands.js +1 -1
- package/dist/errors.js +2 -1
- package/dist/format.js +6 -2
- package/dist/handlers.js +5 -2
- package/dist/land.d.ts +121 -0
- package/dist/land.js +284 -0
- package/dist/mac-app.d.ts +11 -0
- package/dist/mac-app.js +11 -0
- package/dist/main.js +9 -1
- package/dist/menubar.d.ts +18 -13
- package/dist/menubar.js +60 -50
- package/dist/onboarding.d.ts +4 -4
- package/dist/orchestrator-args.d.ts +7 -0
- package/dist/orchestrator-args.js +64 -0
- package/dist/orchestrator-commands.d.ts +43 -0
- package/dist/orchestrator-commands.js +262 -0
- package/dist/person-scope.d.ts +20 -0
- package/dist/person-scope.js +61 -0
- package/dist/project-hub-seed.d.ts +20 -0
- package/dist/project-hub-seed.js +59 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +2 -1
- package/dist/router-commands.d.ts +12 -10
- package/dist/router-commands.js +5 -59
- package/dist/t3-launch.js +2 -2
- package/dist/task-land-hub.py +165 -0
- package/dist/this-computer.d.ts +4 -2
- package/dist/this-computer.js +20 -2
- package/dist/thread-on-computer.js +2 -2
- package/dist/timing.js +3 -1
- package/dist/transcribe-commands.d.ts +7 -0
- package/dist/transcribe-commands.js +84 -0
- package/dist/update.js +4 -2
- package/dist/wait-status.d.ts +1 -1
- package/dist/wait-status.js +2 -0
- package/dist/worktree-twin-engine.d.ts +8 -0
- package/dist/worktree-twin-engine.js +29 -23
- package/dist/worktree-twin.d.ts +6 -0
- package/dist/worktree-twin.js +8 -1
- package/package.json +6 -5
package/dist/land.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Landing a result for real (0.25.0+): `shard task land <task>` and `shard conversations land <id> merge`.
|
|
3
|
+
*
|
|
4
|
+
* A board task has one result: its master merges its workers' branches into shard/<task id> on the project hub before it
|
|
5
|
+
* seals done (a master that did not is helped here: its workers' branches are merged into it when they merge cleanly).
|
|
6
|
+
* Landing then
|
|
7
|
+
* 1. merges the result into the project's default branch on the hub, in a VM of the project (task-land-hub.py): a
|
|
8
|
+
* fast-forward, or a merge commit when the merge is clean, never by force; and checks there that the result
|
|
9
|
+
* commit is contained in the branch;
|
|
10
|
+
* 2. brings it into the person's linked folder on this computer through the live twin's own landing
|
|
11
|
+
* (worktree-twin.ts landingRound from the hub): the branch moves to the new commits, uncommitted edits stay, every
|
|
12
|
+
* rewritten file is backed up first, and a Git operation in progress or a file being edited waits;
|
|
13
|
+
* 3. only then records it (POST /v1/tasks/{id}/land with the commit; for a conversation POST /land merge into the
|
|
14
|
+
* branch). A conflict changes nothing: the task's master is asked to merge (a steer) and nothing is recorded.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
20
|
+
import { conversationClient } from "./conversation-commands.js";
|
|
21
|
+
import { localConversations } from "./conversations.js";
|
|
22
|
+
import { CodedError } from "./errors.js";
|
|
23
|
+
import { selectedProjects } from "./project-preparation.js";
|
|
24
|
+
import { projectIdentity } from "./project-identity.js";
|
|
25
|
+
import { HUB_PATH, backupRoot, deviceId, git, gitText, inspectWorktree, landingRound, listWorktrees } from "./worktree-twin.js";
|
|
26
|
+
import { projectHubCell } from "./worktree-twin-engine.js";
|
|
27
|
+
let script;
|
|
28
|
+
/** The hub side of a landing, shipped next to this module (src in the monorepo, dist when published). */
|
|
29
|
+
export function hubScript() { return script ??= readFileSync(new URL('./task-land-hub.py', import.meta.url), 'utf8'); }
|
|
30
|
+
/** Merges `result` (made from `sources` when missing) into the hub's default branch, in a VM that reads the hub. */
|
|
31
|
+
export async function landOnHub(cell, input, signal) {
|
|
32
|
+
const body = { hub: input.hub ?? HUB_PATH, result: input.result, sources: input.sources, title: input.title, ...(input.into ? { default: input.into } : {}) };
|
|
33
|
+
const r = await cell.exec.run(['python3', '-c', hubScript()], { stdin: JSON.stringify(body), timeoutMs: 300_000, maxOutputBytes: 1 << 20, ...(signal ? { signal } : {}) });
|
|
34
|
+
const line = r.stdout.trim().split('\n').at(-1) ?? '';
|
|
35
|
+
if (r.exitCode !== 0 || !line.startsWith('{'))
|
|
36
|
+
return { state: 'error', into: input.into ?? '', reason: 'script', message: `the landing script exited ${r.exitCode}: ${r.stderr.trim().slice(-400)}` };
|
|
37
|
+
return JSON.parse(line);
|
|
38
|
+
}
|
|
39
|
+
/** The branch to land in: the one checked out in the folder, else the hub's default branch when the folder has it. */
|
|
40
|
+
export async function folderBranch(folder, fallback) {
|
|
41
|
+
const info = await inspectWorktree(folder);
|
|
42
|
+
const own = info.branch?.replace(/^refs\/heads\//, '');
|
|
43
|
+
if (own && !own.startsWith('shard/'))
|
|
44
|
+
return own;
|
|
45
|
+
const has = await git(folder, ['rev-parse', '-q', '--verify', `refs/heads/${fallback}^{commit}`], { ok: [0, 1, 128] });
|
|
46
|
+
return has.code === 0 ? fallback : null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The result into the folder through the twin's landing (from the hub): the branch moves, the person's uncommitted
|
|
50
|
+
* edits stay, and the landed commit is checked to be on the branch.
|
|
51
|
+
*/
|
|
52
|
+
export async function landInFolder(cell, f) {
|
|
53
|
+
const deadline = Date.now() + (f.patienceMs ?? 20_000);
|
|
54
|
+
const target = (await listWorktrees(f.folder)).find(w => w.branch === `refs/heads/${f.into}`);
|
|
55
|
+
const repo = target?.root ?? f.folder;
|
|
56
|
+
for (let attempt = 0;; attempt++) {
|
|
57
|
+
const worktree = target ? await inspectWorktree(target.root) : null;
|
|
58
|
+
const o = await landingRound(cell, {
|
|
59
|
+
conversation: { id: f.name, name: f.name, title: f.title, summary: '', branch: f.branch, base_commit: f.base, worktree_path: '' },
|
|
60
|
+
into: f.into, mode: 'auto', device: f.device, repo, worktree, stateDir: f.stateDir, backupRoot: backupRoot(f.stateDir), fromHub: {},
|
|
61
|
+
...(f.hub ? { hub: f.hub } : {}), ...(f.quietMs !== undefined ? { quietMs: f.quietMs } : {}), ...(f.signal ? { signal: f.signal } : {}),
|
|
62
|
+
});
|
|
63
|
+
const tip = async () => gitText(repo, ['rev-parse', `refs/heads/${f.into}`]);
|
|
64
|
+
switch (o.state) {
|
|
65
|
+
case 'landed': {
|
|
66
|
+
await o.confirmed.catch(() => undefined);
|
|
67
|
+
const landed = o.land.tip, now = await tip();
|
|
68
|
+
const on = await git(repo, ['merge-base', '--is-ancestor', landed, `refs/heads/${f.into}`], { ok: [0, 1] });
|
|
69
|
+
if (on.code !== 0)
|
|
70
|
+
throw new CodedError('land_unverified', `${f.into} in ${f.folder} does not contain the landed commit ${landed.slice(0, 7)}.`);
|
|
71
|
+
return { path: f.folder, into: f.into, state: 'landed', sha: now, method: o.land.method ?? 'landed', files: o.applied.files.length, kept: o.applied.raced, backup: o.applied.backup };
|
|
72
|
+
}
|
|
73
|
+
case 'noop': return { path: f.folder, into: f.into, state: 'already', sha: await tip() };
|
|
74
|
+
case 'conflict': return { path: f.folder, into: f.into, state: 'conflict', conflicts: o.land.conflicts ?? [] };
|
|
75
|
+
case 'error': throw new CodedError('land_failed', `Could not land into ${f.folder}: ${o.land.message ?? o.land.reason ?? 'the landing failed'}.`);
|
|
76
|
+
case 'pr':
|
|
77
|
+
case 'deferred': return { path: f.folder, into: f.into, state: 'waiting', reason: o.state };
|
|
78
|
+
default: {
|
|
79
|
+
// paused (a Git operation), moved (the branch or a file changed while landing), busy (a file just saved).
|
|
80
|
+
const wait = o.state === 'busy' ? Math.min(o.waitMs ?? 1000, 3000) : o.state === 'moved' ? 200 : 1000;
|
|
81
|
+
if (Date.now() + wait > deadline || attempt >= 20)
|
|
82
|
+
return { path: f.folder, into: f.into, state: 'waiting', reason: o.reason };
|
|
83
|
+
await delay(wait, undefined, f.signal ? { signal: f.signal } : {});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** The repository a folder holds, as conversations started from it name it (its origin, else the folder itself). */
|
|
89
|
+
export function folderRepository(folder) {
|
|
90
|
+
try {
|
|
91
|
+
const id = projectIdentity(folder);
|
|
92
|
+
return id.remote ?? `file://${id.folder}`;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The person's folder for a project on this computer: a linked folder (shard projects link) or where it was started.
|
|
100
|
+
* With `repository` (a project holds every folder its people linked), only a folder of that repository.
|
|
101
|
+
*/
|
|
102
|
+
export function linkedFolder(ctx, projectId, conversationId, repository) {
|
|
103
|
+
const repo = (folder) => folder && existsSync(join(folder, '.git')) && (!repository || folderRepository(folder) === repository) ? folder : null;
|
|
104
|
+
const records = localConversations(ctx);
|
|
105
|
+
const own = conversationId ? repo(records[conversationId]?.folder) : null;
|
|
106
|
+
if (own)
|
|
107
|
+
return own;
|
|
108
|
+
// A folder linked with `shard projects link` belongs to the signed-in project until its preparation names one.
|
|
109
|
+
const profile = ctx.store.profile(ctx.apiUrl), mine = profile.project_id ?? profile.api_key?.project_id;
|
|
110
|
+
const linked = selectedProjects(ctx).filter(p => (p.project_id ?? mine) === projectId).map(p => repo(p.folder)).find(Boolean);
|
|
111
|
+
if (linked)
|
|
112
|
+
return linked;
|
|
113
|
+
return Object.values(records).filter(r => r.project_id === projectId).map(r => repo(r.folder)).find(Boolean) ?? null;
|
|
114
|
+
}
|
|
115
|
+
export const displayPath = (path) => { const home = homedir(); return path === home ? '~' : path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path; };
|
|
116
|
+
const short = (sha) => (sha ?? '').slice(0, 7);
|
|
117
|
+
const list = (files) => files.length > 4 ? `${files.slice(0, 4).join(', ')} and ${files.length - 4} more` : files.join(', ');
|
|
118
|
+
function message(o) {
|
|
119
|
+
const f = o.folder;
|
|
120
|
+
if (f && (f.state === 'landed' || f.state === 'already'))
|
|
121
|
+
return `Landed in ${f.display} · ${short(f.sha)}`;
|
|
122
|
+
const hub = `Landed in ${o.hub.into} on the project hub · ${short(o.hub.sha)}`;
|
|
123
|
+
if (f?.state === 'conflict')
|
|
124
|
+
return `${hub}. Not in ${f.display}: your commits on ${f.into} change ${list(f.conflicts ?? [])} too.`;
|
|
125
|
+
if (f?.state === 'waiting')
|
|
126
|
+
return `${hub}. ${f.display} gets it once it is not busy (${f.reason ?? 'busy'}).`;
|
|
127
|
+
return hub;
|
|
128
|
+
}
|
|
129
|
+
export function landDeps(ctx) {
|
|
130
|
+
return { client: conversationClient(ctx), hubCell: p => projectHubCell(ctx, p, ctx.signal, 'shard-land'), folder: (p, c, r) => linkedFolder(ctx, p, c, r),
|
|
131
|
+
device: deviceId(ctx.store.dir), stateDir: ctx.store.dir, signal: ctx.signal };
|
|
132
|
+
}
|
|
133
|
+
async function land(d, l, onConflict) {
|
|
134
|
+
const vm = await d.hubCell(l.projectId);
|
|
135
|
+
try {
|
|
136
|
+
const input = { result: l.result, sources: l.sources.map(s => s.branch), title: l.title, ...(d.hub ? { hub: d.hub } : {}) };
|
|
137
|
+
let hub = await landOnHub(vm.cell, input, d.signal);
|
|
138
|
+
if (hub.state === 'raced')
|
|
139
|
+
hub = await landOnHub(vm.cell, input, d.signal);
|
|
140
|
+
if (hub.state === 'conflict') {
|
|
141
|
+
await onConflict(hub);
|
|
142
|
+
throw new CodedError('land_conflict', `Not landed: ${list(hub.conflicts ?? [])} changed on both sides (${hub.stage === 'integrate' ? `${hub.source} and ${l.result}` : `${l.result} and ${hub.into}`}).`, 'its master merges them and asks to land again', { stage: hub.stage, into: hub.into, conflicts: hub.conflicts ?? [] });
|
|
143
|
+
}
|
|
144
|
+
// The hub's default branch is another repository's (a project holds every folder its people linked): a result
|
|
145
|
+
// whose repository has its folder here lands in that folder, and the hub's default branch stays as it is.
|
|
146
|
+
// Awaited here: the computer that reads the hub closes in `finally` (staging 2026-10-17: returned unawaited, the
|
|
147
|
+
// landing was cut off mid-way, "Interrupted.").
|
|
148
|
+
if (hub.state === 'unrelated' && hub.stage === 'default' && l.repository && l.folder && hub.result)
|
|
149
|
+
return await ownFolder(d, vm.cell, l, hub);
|
|
150
|
+
// No history in common (started from another repository than the branch it lands in): it never clears by trying
|
|
151
|
+
// again, so it says what to do instead.
|
|
152
|
+
if (hub.state === 'unrelated') {
|
|
153
|
+
throw new CodedError('land_unrelated', hub.stage === 'integrate'
|
|
154
|
+
? `Not landed: ${hub.source} and ${l.result} have no history in common; they were started from different repositories.`
|
|
155
|
+
: `Not landed: ${l.result} has no history in common with ${hub.into} on the project hub; it was started from another repository than the one ${hub.into} holds.`, 'land it from the folder of the repository it was started from, or start the task again from this project’s own folder', { stage: hub.stage, into: hub.into });
|
|
156
|
+
}
|
|
157
|
+
if (hub.state === 'missing')
|
|
158
|
+
throw new CodedError('land_nothing', `Nothing to land: the project hub has no ${l.result} yet.`, 'land after the work is done', { branch: l.result });
|
|
159
|
+
if (hub.state !== 'landed')
|
|
160
|
+
throw new CodedError('land_failed', `Could not land on the project hub: ${hub.message ?? hub.reason ?? hub.state}.`, 'try again in a moment', { stage: hub.stage ?? null });
|
|
161
|
+
const contained = l.sources.filter(s => hub.contained?.includes(s.branch)).map(s => s.conversation_id);
|
|
162
|
+
let folder = null;
|
|
163
|
+
if (l.folder) {
|
|
164
|
+
const into = await folderBranch(l.folder, hub.into);
|
|
165
|
+
if (!into)
|
|
166
|
+
folder = { path: l.folder, display: displayPath(l.folder), into: hub.into, state: 'skipped', reason: 'detached' };
|
|
167
|
+
else {
|
|
168
|
+
const f = await landInFolder(vm.cell, { folder: l.folder, into, name: l.name, title: l.title, branch: l.result, base: l.base ?? hub.merge_base ?? hub.before ?? hub.result,
|
|
169
|
+
device: d.device, stateDir: d.stateDir, ...(d.hub ? { hub: d.hub } : {}), ...(d.quietMs !== undefined ? { quietMs: d.quietMs } : {}), ...(d.signal ? { signal: d.signal } : {}) });
|
|
170
|
+
folder = { ...f, display: displayPath(f.path) };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { title: l.title, result: { branch: l.result, sha: hub.result }, hub: { into: hub.into, sha: hub.after, method: hub.method }, folder, contained };
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
vm.close();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** A result into the folder of its own repository only (the hub's default branch holds another repository). */
|
|
180
|
+
async function ownFolder(d, cell, l, hub) {
|
|
181
|
+
const folder = l.folder, into = await folderBranch(folder, 'main');
|
|
182
|
+
const contained = l.sources.filter(s => hub.contained?.includes(s.branch)).map(s => s.conversation_id);
|
|
183
|
+
if (!into)
|
|
184
|
+
return { title: l.title, result: { branch: l.result, sha: hub.result }, hub: { into: '', sha: hub.result, method: 'folder' }, contained,
|
|
185
|
+
folder: { path: folder, display: displayPath(folder), into: '', state: 'skipped', reason: 'detached' } };
|
|
186
|
+
const f = await landInFolder(cell, { folder, into, name: l.name, title: l.title, branch: l.result, base: l.base ?? hub.result, device: d.device, stateDir: d.stateDir,
|
|
187
|
+
...(d.hub ? { hub: d.hub } : {}), ...(d.quietMs !== undefined ? { quietMs: d.quietMs } : {}), ...(d.signal ? { signal: d.signal } : {}) });
|
|
188
|
+
if (f.state !== 'landed' && f.state !== 'already') {
|
|
189
|
+
// Nothing else holds it: a folder that could not take it yet is not a landing.
|
|
190
|
+
throw new CodedError('land_failed', f.state === 'conflict'
|
|
191
|
+
? `Not landed yet: ${list(f.conflicts ?? [])} changed in ${displayPath(folder)} too since ${l.result} started.`
|
|
192
|
+
: `Not landed yet: ${displayPath(folder)} is busy (${f.reason ?? 'busy'}).`, f.state === 'conflict' ? 'commit or put aside those changes, and it lands' : 'it lands once the folder is quiet', { into, folder: displayPath(folder) });
|
|
193
|
+
}
|
|
194
|
+
return { title: l.title, result: { branch: l.result, sha: hub.result }, hub: { into, sha: f.sha ?? hub.result, method: 'folder' }, folder: { ...f, display: displayPath(f.path) }, contained };
|
|
195
|
+
}
|
|
196
|
+
/** What a landing did, for people: the line, then what was not recorded. */
|
|
197
|
+
export function landedText(o) {
|
|
198
|
+
return `${o.message}\n${o.recorded ? '' : `Not recorded on the board: ${o.record_error ?? 'unknown'}.\n`}`;
|
|
199
|
+
}
|
|
200
|
+
const recordFailure = (error) => {
|
|
201
|
+
const e = error;
|
|
202
|
+
return e.status === 404 ? 'this API does not record task landings yet' : (e.message ?? String(error));
|
|
203
|
+
};
|
|
204
|
+
/** The task's current result: its master's latest final report, when it is done. */
|
|
205
|
+
function currentResult(d) {
|
|
206
|
+
const own = d.task.master_id ? d.reports.filter(r => r.author_id === d.task.master_id) : d.reports;
|
|
207
|
+
const final = [...own].reverse().find(r => r.milestone === 'done' || r.milestone === 'failed' || r.milestone === 'planned');
|
|
208
|
+
return final?.milestone === 'done' ? final.id : null;
|
|
209
|
+
}
|
|
210
|
+
const resultBranch = (taskId) => `shard/${taskId}`;
|
|
211
|
+
/** `shard task land <task>`: the task's result into the project's default branch and the linked folder, then recorded. */
|
|
212
|
+
export async function landTask(d, taskId, opts = {}) {
|
|
213
|
+
const client = d.client, signal = d.signal ? { signal: d.signal } : {};
|
|
214
|
+
const detail = await client.tasks.get(taskId, signal);
|
|
215
|
+
const task = detail.task, title = task.title ?? task.text.split('\n')[0].slice(0, 80);
|
|
216
|
+
const report = currentResult(detail);
|
|
217
|
+
if (!report || (task.state !== 'done' && task.state !== 'unverified'))
|
|
218
|
+
throw new CodedError('task_not_done', 'This task has no finished result to land yet.', 'land it after its master’s done report', { state: task.state });
|
|
219
|
+
const workers = [];
|
|
220
|
+
for (const c of task.conversations) {
|
|
221
|
+
if (c.role === 'master' || c.role === 'reviewer')
|
|
222
|
+
continue;
|
|
223
|
+
const w = await client.conversations.get(c.id);
|
|
224
|
+
if (w.join_mode !== 'reviewer' && (w.changes?.commits ?? 0) > 0 && w.landed?.how !== 'discarded')
|
|
225
|
+
workers.push(w);
|
|
226
|
+
}
|
|
227
|
+
// The repository the result is of: its workers' (or its master's), so it lands only in a folder of that repository.
|
|
228
|
+
const repository = workers.find(w => w.repo_remote)?.repo_remote ?? (task.master_id ? (await client.conversations.get(task.master_id).catch(() => null))?.repo_remote : undefined) ?? null;
|
|
229
|
+
const folder = opts.path ?? d.folder(task.project_id, undefined, repository ?? undefined);
|
|
230
|
+
const done = await land(d, { projectId: task.project_id, title, name: taskId, result: resultBranch(taskId), sources: workers.map(w => ({ branch: w.branch, conversation_id: w.id })),
|
|
231
|
+
base: workers.find(w => w.base_commit)?.base_commit ?? null, folder, repository }, async (hub) => {
|
|
232
|
+
const what = hub.stage === 'integrate' ? `${hub.source} into ${resultBranch(taskId)}` : `${resultBranch(taskId)} into ${hub.into}`;
|
|
233
|
+
await client.tasks.steer(taskId, `Land could not merge ${what}: ${list(hub.conflicts ?? [])} changed on both sides. Merge ${hub.into} from the project hub and every worker branch into ${resultBranch(taskId)}, resolve the conflicts so both changes work (ask the person only if the choice is theirs), run the tests, push ${resultBranch(taskId)} to the hub and seal done again.`, signal);
|
|
234
|
+
});
|
|
235
|
+
let recorded = false, record_error;
|
|
236
|
+
try {
|
|
237
|
+
await client.tasks.recordLanding(taskId, { sha: done.result.sha, into: done.hub.into, branch: done.result.branch, reportId: report, conversationIds: done.contained }, signal);
|
|
238
|
+
recorded = true;
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
record_error = recordFailure(error);
|
|
242
|
+
// An API without task landings (before train 41) keeps a land item per worker: the workers whose branches are in
|
|
243
|
+
// the landed commit are recorded as merged into the branch, which is true, so those items go.
|
|
244
|
+
if (error.status === 404 && done.contained.length) {
|
|
245
|
+
let all = true;
|
|
246
|
+
for (const id of done.contained) {
|
|
247
|
+
try {
|
|
248
|
+
await client.conversations.land(id, 'merge', { into: done.hub.into, ...signal });
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
all = false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (all) {
|
|
255
|
+
recorded = true;
|
|
256
|
+
record_error = undefined;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const { title: t, result, hub, folder: f } = done;
|
|
261
|
+
const o = { title: t, result, hub, folder: f, landed: true, task_id: taskId, recorded, ...(record_error ? { record_error } : {}) };
|
|
262
|
+
return { ...o, message: message(o) };
|
|
263
|
+
}
|
|
264
|
+
/** `shard conversations land <id> merge`: the conversation's branch into the default branch and its folder, then recorded. */
|
|
265
|
+
export async function landConversation(d, id, opts = {}) {
|
|
266
|
+
const client = d.client;
|
|
267
|
+
const c = await client.conversations.get(id);
|
|
268
|
+
if (c.changes?.uncommitted)
|
|
269
|
+
throw new CodedError('uncommitted_changes', 'The conversation has uncommitted changes.', 'ask its agent to commit them, then land');
|
|
270
|
+
const folder = opts.path ?? d.folder(c.project_id, c.id);
|
|
271
|
+
const done = await land(d, { projectId: c.project_id, title: c.title || c.branch, name: c.name ?? c.branch.replace(/^shard\//, ''), result: c.branch, sources: [],
|
|
272
|
+
base: c.base_commit ?? null, folder }, () => Promise.resolve());
|
|
273
|
+
let recorded = false, record_error;
|
|
274
|
+
try {
|
|
275
|
+
await client.conversations.land(id, 'merge', { into: done.hub.into, ...(d.signal ? { signal: d.signal } : {}) });
|
|
276
|
+
recorded = true;
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
record_error = recordFailure(error);
|
|
280
|
+
}
|
|
281
|
+
const { title: t, result, hub, folder: f } = done;
|
|
282
|
+
const o = { title: t, result, hub, folder: f, landed: true, conversation_id: id, recorded, ...(record_error ? { record_error } : {}) };
|
|
283
|
+
return { ...o, message: message(o) };
|
|
284
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which macOS app this CLI installs and updates. On (0.27.0+): the one Shardflux app from
|
|
3
|
+
* @shardflux/desktop-darwin-<arch>, which replaces the menu bar app (@shardflux/menubar-darwin) in place. The CLI's
|
|
4
|
+
* optionalDependencies carry the same packages (test/one-app.test.ts keeps the two in step): with the desktop package
|
|
5
|
+
* in the install, `shard`, its refresh and the running menu bar app's self-update replace the menu bar app with it.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ONE_APP_ON_MAC = true;
|
|
8
|
+
/** The npm package that carries the one Shardflux app for this Mac's CPU (it replaces @shardflux/menubar-darwin). */
|
|
9
|
+
export declare const desktopDarwinPackage: (arch?: string) => string;
|
|
10
|
+
/** The npm package that carries the macOS app this CLI installs and updates (see ONE_APP_ON_MAC). */
|
|
11
|
+
export declare const macAppPackage: (arch?: string) => string;
|
package/dist/mac-app.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which macOS app this CLI installs and updates. On (0.27.0+): the one Shardflux app from
|
|
3
|
+
* @shardflux/desktop-darwin-<arch>, which replaces the menu bar app (@shardflux/menubar-darwin) in place. The CLI's
|
|
4
|
+
* optionalDependencies carry the same packages (test/one-app.test.ts keeps the two in step): with the desktop package
|
|
5
|
+
* in the install, `shard`, its refresh and the running menu bar app's self-update replace the menu bar app with it.
|
|
6
|
+
*/
|
|
7
|
+
export const ONE_APP_ON_MAC = true;
|
|
8
|
+
/** The npm package that carries the one Shardflux app for this Mac's CPU (it replaces @shardflux/menubar-darwin). */
|
|
9
|
+
export const desktopDarwinPackage = (arch = process.arch) => `@shardflux/desktop-darwin-${arch === 'x64' ? 'x64' : 'arm64'}`;
|
|
10
|
+
/** The npm package that carries the macOS app this CLI installs and updates (see ONE_APP_ON_MAC). */
|
|
11
|
+
export const macAppPackage = (arch = process.arch) => ONE_APP_ON_MAC ? desktopDarwinPackage(arch) : '@shardflux/menubar-darwin';
|
package/dist/main.js
CHANGED
|
@@ -66,7 +66,11 @@ function hintFor(info, cred, file, command = '') {
|
|
|
66
66
|
if (info.code === 'timeout' && info.operation_id)
|
|
67
67
|
return `the operation continues server side; keep waiting with: shard operations wait ${info.operation_id}`;
|
|
68
68
|
if (info.code === 'operation_failed' && info.retryable) {
|
|
69
|
-
|
|
69
|
+
if (info.details?.error_code === 'capacity_unavailable') {
|
|
70
|
+
const retry = info.details.retry_after_seconds;
|
|
71
|
+
return `nothing was started; run the same command again${typeof retry === 'number' && retry > 0 ? ` after ${retry} s` : ''}`;
|
|
72
|
+
}
|
|
73
|
+
return 'the operation may succeed if you retry it later.';
|
|
70
74
|
}
|
|
71
75
|
if (info.code === 'interrupted' && info.operation_id)
|
|
72
76
|
return `keep waiting with: shard operations wait ${info.operation_id}`;
|
|
@@ -133,6 +137,8 @@ function hintFor(info, cred, file, command = '') {
|
|
|
133
137
|
const current = typeof info.details?.current_revision === 'string' ? info.details.current_revision : 'unknown';
|
|
134
138
|
return `the file changed since that revision (current revision: ${current}); read it again, then retry with --expected-revision ${current}`;
|
|
135
139
|
}
|
|
140
|
+
if (reason === 'workspace_disk_full' || info.code === 'workspace_disk_full')
|
|
141
|
+
return 'free space with shard files delete <id|key> <path>, or run a cleanup command; retry the write after space is available';
|
|
136
142
|
if (reason === 'edit_not_found')
|
|
137
143
|
return `--old${index} does not occur in the file (it must match exactly, including whitespace)`;
|
|
138
144
|
if (reason === 'edit_ambiguous')
|
|
@@ -175,6 +181,8 @@ function allowanceHint(info) {
|
|
|
175
181
|
const resets = typeof info.details?.resets_at === 'string' ? ` (the period resets ${time(info.details.resets_at)})` : '';
|
|
176
182
|
switch (info.details?.reason) {
|
|
177
183
|
case 'allowance_used':
|
|
184
|
+
if (info.details?.spend_cap === null)
|
|
185
|
+
return 'choose a paid plan: shard billing plans; overage is unavailable on the current plan';
|
|
178
186
|
return `upgrade (shard billing plans), or turn on overage: shard billing overage on --cap <dollars>${resets}`;
|
|
179
187
|
case 'spend_cap_reached':
|
|
180
188
|
return `the spend cap is reached: raise it (shard billing overage cap <dollars>, at most the plan price) or upgrade${resets}`;
|
package/dist/menubar.d.ts
CHANGED
|
@@ -30,12 +30,6 @@ export declare function compareAppVersions(left: string, right: string): number;
|
|
|
30
30
|
export declare const SELF_UPDATING_APP = "0.3.2";
|
|
31
31
|
/** CFBundleShortVersionString of a Shardflux app from its XML Info.plist, read without any tool (null: not readable). */
|
|
32
32
|
export declare function plistVersion(app: string): string | null;
|
|
33
|
-
/**
|
|
34
|
-
* Whether any of these processes shows a window: the menu, the launcher, Settings, onboarding, an approval. The status
|
|
35
|
-
* item itself (a strip at the top of the screen on the status layer, owned by the app before macOS 26) does not count.
|
|
36
|
-
* Null when the probe could not answer: then nothing may be replaced.
|
|
37
|
-
*/
|
|
38
|
-
export declare function windowsOpen(text: string): boolean | null;
|
|
39
33
|
export declare function parseNativeStatus(text: string): NativeStatus;
|
|
40
34
|
/**
|
|
41
35
|
* The app's executables keep their execute permission (0.13.6+). @shardflux/menubar-darwin 0.1.1 and 0.1.2 reached npm
|
|
@@ -56,17 +50,15 @@ export declare class MenubarManager {
|
|
|
56
50
|
get platform(): NodeJS.Platform;
|
|
57
51
|
/** The version of the app this CLI's npm package carries (what `start` installs). */
|
|
58
52
|
packagedVersion(): Promise<string>;
|
|
59
|
-
/** Whether the app shows any window (see windowsOpen); null when that could not be read. */
|
|
60
|
-
windowsOpen(pids: number[]): Promise<boolean | null>;
|
|
61
53
|
status(configDir?: string): Promise<MenubarStatus>;
|
|
62
54
|
stop(configDir?: string): Promise<MenubarStatus>;
|
|
63
55
|
login(value: 'on' | 'off', configDir?: string): Promise<MenubarStatus>;
|
|
64
56
|
start(configDir: string, apiUrl: string): Promise<MenubarStatus>;
|
|
65
57
|
/**
|
|
66
58
|
* Brings an installed app up to the one this CLI's npm package carries, without disturbing the person: nothing when
|
|
67
|
-
* the versions are equal
|
|
68
|
-
*
|
|
69
|
-
*
|
|
59
|
+
* the versions are equal, the copy on disk alone when it is not running. A running app swaps itself in when it is
|
|
60
|
+
* idle (every app from SELF_UPDATING_APP on: the menu bar app, and the one Shardflux app that replaces it, which runs
|
|
61
|
+
* `shard menubar start` when nothing is open); an older running one is restarted unless a `shard update` runs.
|
|
70
62
|
*/
|
|
71
63
|
refresh(configDir: string, apiUrl: string): Promise<{
|
|
72
64
|
result: 'current' | 'not_installed' | 'installed' | 'restarted' | 'deferred' | 'busy';
|
|
@@ -79,8 +71,21 @@ export declare class MenubarManager {
|
|
|
79
71
|
export declare function ensureMenubar(dir: string, apiUrl: string, manager?: MenubarManager, platform?: NodeJS.Platform): Promise<string | undefined>;
|
|
80
72
|
/** How often a command may start a background refresh while the app is busy (a window open). */
|
|
81
73
|
export declare const REFRESH_RETRY_MS: number;
|
|
82
|
-
/**
|
|
83
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Where this CLI's npm package keeps the app it carries, or null when the optional package is missing. The links npm
|
|
76
|
+
* leaves out of the package are put back first (restorePackagedAppLinks), so the path is the complete, signed app.
|
|
77
|
+
*/
|
|
78
|
+
export declare function packagedAppPath(arch?: string): string | null;
|
|
79
|
+
/**
|
|
80
|
+
* Puts back the symbolic links of the package's Shardflux.app that npm leaves out of a package (0.27.0+): an Electron
|
|
81
|
+
* app's frameworks are made of them (Versions/Current -> A, ...), and without them the app neither starts nor verifies.
|
|
82
|
+
* The package lists them in links.json; its postinstall restores them too, but pnpm and bun skip install scripts. Only
|
|
83
|
+
* relative links inside the app; a path that exists is left as it is. `missing`: links that could not be created.
|
|
84
|
+
*/
|
|
85
|
+
export declare function restorePackagedAppLinks(packageDir: string): {
|
|
86
|
+
restored: string[];
|
|
87
|
+
missing: string[];
|
|
88
|
+
};
|
|
84
89
|
export interface BackgroundRefreshOptions {
|
|
85
90
|
dir: string;
|
|
86
91
|
home: string;
|
package/dist/menubar.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { desktopAccount } from "./desktop-account.js";
|
|
2
2
|
import { execFile, spawn } from 'node:child_process';
|
|
3
|
-
import { chmodSync, closeSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, renameSync, rmSync, writeSync } from 'node:fs';
|
|
3
|
+
import { chmodSync, closeSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, renameSync, rmSync, symlinkSync, writeSync } from 'node:fs';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
|
-
import { dirname, join } from 'node:path';
|
|
6
|
+
import { dirname, isAbsolute, join, resolve as resolvePath, sep } from 'node:path';
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
8
|
import { compareVersions } from '@shardflux/sdk';
|
|
9
9
|
import { AgentStateStore } from "./agent-state.js";
|
|
10
10
|
import { UsageError } from "./errors.js";
|
|
11
11
|
import { json } from "./format.js";
|
|
12
12
|
import { LinuxDesktopManager, linuxDesktopPackage } from "./desktop-linux.js";
|
|
13
|
+
import { macAppPackage } from "./mac-app.js";
|
|
13
14
|
const execute = promisify(execFile);
|
|
14
15
|
/**
|
|
15
16
|
* Every native tool a default MenubarManager runs (plutil, codesign, xattr, open, the app's --control) goes through
|
|
@@ -56,35 +57,6 @@ export function plistVersion(app) {
|
|
|
56
57
|
return null;
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
|
-
const WINDOW_PROBE = `ObjC.import('CoreGraphics');
|
|
60
|
-
function run(argv) {
|
|
61
|
-
const pids = argv.map(Number);
|
|
62
|
-
const list = ObjC.deepUnwrap(ObjC.castRefToObject($.CGWindowListCopyWindowInfo($.kCGWindowListOptionOnScreenOnly, $.kCGNullWindowID))) || [];
|
|
63
|
-
return JSON.stringify(list.filter(w => pids.includes(w.kCGWindowOwnerPID)).map(w => ({ pid: w.kCGWindowOwnerPID, layer: w.kCGWindowLayer,
|
|
64
|
-
x: w.kCGWindowBounds.X, y: w.kCGWindowBounds.Y, width: w.kCGWindowBounds.Width, height: w.kCGWindowBounds.Height, alpha: w.kCGWindowAlpha })));
|
|
65
|
-
}`;
|
|
66
|
-
/**
|
|
67
|
-
* Whether any of these processes shows a window: the menu, the launcher, Settings, onboarding, an approval. The status
|
|
68
|
-
* item itself (a strip at the top of the screen on the status layer, owned by the app before macOS 26) does not count.
|
|
69
|
-
* Null when the probe could not answer: then nothing may be replaced.
|
|
70
|
-
*/
|
|
71
|
-
export function windowsOpen(text) {
|
|
72
|
-
let rows;
|
|
73
|
-
try {
|
|
74
|
-
rows = JSON.parse(text);
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
if (!Array.isArray(rows))
|
|
80
|
-
return null;
|
|
81
|
-
const windows = rows;
|
|
82
|
-
if (!windows.every(w => w && typeof w === 'object' && ['layer', 'y', 'width', 'height'].every(k => typeof w[k] === 'number')))
|
|
83
|
-
return null;
|
|
84
|
-
// A status item is a strip on the status layer at the top of the screen (39 pt high on a notched display).
|
|
85
|
-
const statusItem = (w) => w.layer === 25 && w.y <= 0 && w.height <= 50;
|
|
86
|
-
return windows.some(w => w.layer >= 0 && !statusItem(w) && (w.alpha ?? 1) > 0 && w.width > 1 && w.height > 1);
|
|
87
|
-
}
|
|
88
60
|
export function parseNativeStatus(text) {
|
|
89
61
|
const value = JSON.parse(text);
|
|
90
62
|
if (!value || typeof value !== 'object')
|
|
@@ -129,6 +101,8 @@ export class MenubarManager {
|
|
|
129
101
|
const app = packagedAppPath();
|
|
130
102
|
if (!app)
|
|
131
103
|
throw new UsageError('The macOS app package is missing. Reinstall shard with optional dependencies enabled, then run shard menubar start.');
|
|
104
|
+
if (restorePackagedAppLinks(dirname(app)).missing.length)
|
|
105
|
+
throw new UsageError(`The Shardflux app in ${dirname(app)} is incomplete and that folder is not writable. Reinstall shard (npm i -g shardflux), then run shard.`);
|
|
132
106
|
return app;
|
|
133
107
|
});
|
|
134
108
|
}
|
|
@@ -150,17 +124,6 @@ export class MenubarManager {
|
|
|
150
124
|
compareBundleVersions(version, version);
|
|
151
125
|
return version;
|
|
152
126
|
}
|
|
153
|
-
/** Whether the app shows any window (see windowsOpen); null when that could not be read. */
|
|
154
|
-
async windowsOpen(pids) {
|
|
155
|
-
if (!pids.length)
|
|
156
|
-
return false;
|
|
157
|
-
try {
|
|
158
|
-
return windowsOpen(await this.#run('/usr/bin/osascript', ['-l', 'JavaScript', '-e', WINDOW_PROBE, ...pids.map(String)]));
|
|
159
|
-
}
|
|
160
|
-
catch {
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
127
|
/**
|
|
165
128
|
* The app's --control for `configDir`: its pids are the app running for that config directory, wherever it runs from
|
|
166
129
|
* (menu bar 0.8.4+, one app per config directory), and any app running from the installed bundle.
|
|
@@ -213,7 +176,9 @@ export class MenubarManager {
|
|
|
213
176
|
const staged = join(stage, 'Shardflux.app'), backup = join(stage, 'previous.app');
|
|
214
177
|
let committed = false, restored = false;
|
|
215
178
|
try {
|
|
216
|
-
|
|
179
|
+
// Relative symlinks stay relative (an app's frameworks are made of them): Node's default rewrites them to absolute
|
|
180
|
+
// paths into the npm package, which breaks the bundle's seal and its helpers (0.27.0+).
|
|
181
|
+
cpSync(source, staged, { recursive: true, dereference: false, verbatimSymlinks: true });
|
|
217
182
|
if (current.running)
|
|
218
183
|
await this.stop(configDir);
|
|
219
184
|
if (current.installed)
|
|
@@ -265,9 +230,9 @@ export class MenubarManager {
|
|
|
265
230
|
}
|
|
266
231
|
/**
|
|
267
232
|
* Brings an installed app up to the one this CLI's npm package carries, without disturbing the person: nothing when
|
|
268
|
-
* the versions are equal
|
|
269
|
-
*
|
|
270
|
-
*
|
|
233
|
+
* the versions are equal, the copy on disk alone when it is not running. A running app swaps itself in when it is
|
|
234
|
+
* idle (every app from SELF_UPDATING_APP on: the menu bar app, and the one Shardflux app that replaces it, which runs
|
|
235
|
+
* `shard menubar start` when nothing is open); an older running one is restarted unless a `shard update` runs.
|
|
271
236
|
*/
|
|
272
237
|
async refresh(configDir, apiUrl) {
|
|
273
238
|
this.#requireMac();
|
|
@@ -280,7 +245,7 @@ export class MenubarManager {
|
|
|
280
245
|
if (current.running) {
|
|
281
246
|
if (compareAppVersions(from, SELF_UPDATING_APP) >= 0)
|
|
282
247
|
return { result: 'deferred', from, to };
|
|
283
|
-
if (updateRunning(configDir)
|
|
248
|
+
if (updateRunning(configDir))
|
|
284
249
|
return { result: 'busy', from, to };
|
|
285
250
|
await this.start(configDir, apiUrl);
|
|
286
251
|
return { result: 'restarted', from, to };
|
|
@@ -332,14 +297,59 @@ const REFRESH_MARK = 'menubar-refresh.json';
|
|
|
332
297
|
const REFRESH_LOCK = 'menubar-refresh.lock';
|
|
333
298
|
/** How often a command may start a background refresh while the app is busy (a window open). */
|
|
334
299
|
export const REFRESH_RETRY_MS = 5 * 60_000;
|
|
335
|
-
/**
|
|
336
|
-
|
|
300
|
+
/**
|
|
301
|
+
* Where this CLI's npm package keeps the app it carries, or null when the optional package is missing. The links npm
|
|
302
|
+
* leaves out of the package are put back first (restorePackagedAppLinks), so the path is the complete, signed app.
|
|
303
|
+
*/
|
|
304
|
+
export function packagedAppPath(arch = process.arch) {
|
|
305
|
+
let dir;
|
|
337
306
|
try {
|
|
338
|
-
|
|
307
|
+
dir = dirname(createRequire(import.meta.url).resolve(`${macAppPackage(arch)}/package.json`));
|
|
339
308
|
}
|
|
340
309
|
catch {
|
|
341
310
|
return null;
|
|
342
311
|
}
|
|
312
|
+
restorePackagedAppLinks(dir);
|
|
313
|
+
return join(dir, 'Shardflux.app');
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Puts back the symbolic links of the package's Shardflux.app that npm leaves out of a package (0.27.0+): an Electron
|
|
317
|
+
* app's frameworks are made of them (Versions/Current -> A, ...), and without them the app neither starts nor verifies.
|
|
318
|
+
* The package lists them in links.json; its postinstall restores them too, but pnpm and bun skip install scripts. Only
|
|
319
|
+
* relative links inside the app; a path that exists is left as it is. `missing`: links that could not be created.
|
|
320
|
+
*/
|
|
321
|
+
export function restorePackagedAppLinks(packageDir) {
|
|
322
|
+
const restored = [], missing = [];
|
|
323
|
+
let spec;
|
|
324
|
+
try {
|
|
325
|
+
spec = JSON.parse(readFileSync(join(packageDir, 'links.json'), 'utf8'));
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return { restored, missing };
|
|
329
|
+
}
|
|
330
|
+
const app = join(packageDir, 'Shardflux.app');
|
|
331
|
+
const links = spec.links;
|
|
332
|
+
for (const entry of Array.isArray(links) ? links : []) {
|
|
333
|
+
const [path, target] = Array.isArray(entry) ? entry : [];
|
|
334
|
+
if (typeof path !== 'string' || typeof target !== 'string' || !path || !target || isAbsolute(path) || isAbsolute(target))
|
|
335
|
+
continue;
|
|
336
|
+
const at = resolvePath(app, path);
|
|
337
|
+
if (!at.startsWith(app + sep) || !resolvePath(dirname(at), target).startsWith(app + sep))
|
|
338
|
+
continue;
|
|
339
|
+
try {
|
|
340
|
+
lstatSync(at);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
catch { /* missing: put it back */ }
|
|
344
|
+
try {
|
|
345
|
+
symlinkSync(target, at);
|
|
346
|
+
restored.push(path);
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
missing.push(path);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return { restored, missing };
|
|
343
353
|
}
|
|
344
354
|
/**
|
|
345
355
|
* Every `shard` command (0.15.2+): when the npm package carries a newer app than the installed one (two Info.plist
|
package/dist/onboarding.d.ts
CHANGED
|
@@ -111,7 +111,7 @@ export declare function onboardingReport(ctx: Ctx, values: Values, platform?: No
|
|
|
111
111
|
};
|
|
112
112
|
conversation: {
|
|
113
113
|
id: string;
|
|
114
|
-
state: "archived" | "working" | "needs_you" | "unresolved" | "done";
|
|
114
|
+
state: "deleted" | "archived" | "working" | "needs_you" | "unresolved" | "done";
|
|
115
115
|
summary: string;
|
|
116
116
|
} | null;
|
|
117
117
|
commands: never[];
|
|
@@ -142,7 +142,7 @@ export declare function onboardingReport(ctx: Ctx, values: Values, platform?: No
|
|
|
142
142
|
};
|
|
143
143
|
conversation: {
|
|
144
144
|
id: string;
|
|
145
|
-
state: "archived" | "working" | "needs_you" | "unresolved" | "done";
|
|
145
|
+
state: "deleted" | "archived" | "working" | "needs_you" | "unresolved" | "done";
|
|
146
146
|
summary: string;
|
|
147
147
|
} | null;
|
|
148
148
|
commands: string[];
|
|
@@ -195,7 +195,7 @@ export declare function liveActivity(ctx: Ctx, ref: string): Promise<{
|
|
|
195
195
|
};
|
|
196
196
|
conversation: {
|
|
197
197
|
id: string;
|
|
198
|
-
state: "archived" | "working" | "needs_you" | "unresolved" | "done";
|
|
198
|
+
state: "deleted" | "archived" | "working" | "needs_you" | "unresolved" | "done";
|
|
199
199
|
summary: string;
|
|
200
200
|
} | null;
|
|
201
201
|
commands: never[];
|
|
@@ -226,7 +226,7 @@ export declare function liveActivity(ctx: Ctx, ref: string): Promise<{
|
|
|
226
226
|
};
|
|
227
227
|
conversation: {
|
|
228
228
|
id: string;
|
|
229
|
-
state: "archived" | "working" | "needs_you" | "unresolved" | "done";
|
|
229
|
+
state: "deleted" | "archived" | "working" | "needs_you" | "unresolved" | "done";
|
|
230
230
|
summary: string;
|
|
231
231
|
} | null;
|
|
232
232
|
commands: string[];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Your orchestrator's command table (0.27.0+, SDK 0.29.0): `shard orchestrator say|list|retry|dismiss`, and `restore`
|
|
3
|
+
* (0.28.0+, SDK 0.30.0). Pure data like
|
|
4
|
+
* args.ts, which merges it into COMMANDS; orchestrator-commands.ts has the handlers.
|
|
5
|
+
*/
|
|
6
|
+
import type { CommandSpec } from './args.js';
|
|
7
|
+
export declare const ORCHESTRATOR_COMMANDS: CommandSpec[];
|