@shardflux/cli 0.16.0 → 0.18.0-next.4
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 +67 -2
- package/README.md +47 -8
- package/dist/agent-commands.d.ts +9 -1
- package/dist/agent-commands.js +65 -16
- package/dist/agent-models.d.ts +61 -0
- package/dist/agent-models.js +79 -0
- package/dist/agent-state.d.ts +10 -0
- package/dist/agent-state.js +13 -0
- package/dist/agent-target-commands.d.ts +6 -1
- package/dist/agent-target-commands.js +113 -16
- package/dist/agent-targets.d.ts +17 -5
- package/dist/agent-targets.js +27 -11
- package/dist/args.d.ts +5 -1
- package/dist/args.js +49 -9
- package/dist/cli-proxy-forward.d.ts +45 -0
- package/dist/cli-proxy-forward.js +432 -0
- package/dist/cli-proxy-hub.d.ts +273 -0
- package/dist/cli-proxy-hub.js +749 -0
- package/dist/cli-proxy.d.ts +325 -0
- package/dist/cli-proxy.js +1016 -0
- package/dist/codex-cloud.d.ts +2 -1
- package/dist/codex-cloud.js +16 -6
- package/dist/command-center-actions.js +16 -1
- package/dist/conversation-local.js +2 -2
- package/dist/conversation-stash.js +4 -3
- package/dist/conversation-terminal.d.ts +5 -0
- package/dist/conversation-terminal.js +45 -6
- package/dist/conversations.d.ts +18 -2
- package/dist/conversations.js +12 -5
- package/dist/desktop-linux.d.ts +40 -0
- package/dist/desktop-linux.js +186 -0
- package/dist/device-approvals.d.ts +12 -0
- package/dist/device-approvals.js +75 -0
- package/dist/device-helper.d.ts +3 -1
- package/dist/device-helper.js +5 -2
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +1 -1
- package/dist/git-remote-bin.js +0 -0
- package/dist/handlers.js +3 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/linux-helper.d.ts +12 -0
- package/dist/linux-helper.js +88 -0
- package/dist/linux-terminal.d.ts +11 -0
- package/dist/linux-terminal.js +42 -0
- package/dist/main.js +30 -6
- package/dist/menubar.js +44 -4
- package/dist/onboarding.d.ts +2 -0
- package/dist/onboarding.js +59 -9
- package/dist/t3-asar.d.ts +10 -0
- package/dist/t3-asar.js +91 -0
- package/dist/t3-launch.d.ts +37 -0
- package/dist/t3-launch.js +133 -0
- package/dist/t3-linux.d.ts +68 -0
- package/dist/t3-linux.js +343 -0
- package/dist/t3-mode.d.ts +129 -0
- package/dist/t3-mode.js +676 -0
- package/dist/t3-phone.d.ts +17 -0
- package/dist/t3-phone.js +15 -0
- package/dist/t3-provider.d.ts +5 -3
- package/dist/t3-provider.js +12 -49
- package/dist/t3-thread-script.d.ts +18 -0
- package/dist/t3-thread-script.js +245 -0
- package/dist/terminal-onboarding.d.ts +11 -0
- package/dist/terminal-onboarding.js +75 -0
- package/dist/thread-on-computer.d.ts +2 -2
- package/dist/thread-on-computer.js +13 -7
- package/dist/update.d.ts +1 -1
- package/dist/update.js +28 -1
- package/dist/worktree-twin-engine.d.ts +31 -1
- package/dist/worktree-twin-engine.js +253 -40
- package/dist/worktree-twin.d.ts +17 -0
- package/dist/worktree-twin.js +147 -22
- package/package.json +7 -19
package/dist/worktree-twin.js
CHANGED
|
@@ -516,19 +516,23 @@ export async function landingRound(cell, input) {
|
|
|
516
516
|
await cell.exec.run(['rm', '-rf', '--', dir], { timeoutMs: 60_000 }).catch(() => undefined);
|
|
517
517
|
}
|
|
518
518
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const
|
|
522
|
-
const c = input.conversation, branch = `refs/heads/${input.into}`, hub = input.hub ?? HUB_PATH, vmTmp = input.vmTmp ?? '/tmp';
|
|
519
|
+
/** The twin of the checkout where the branch is checked out (or of the branch itself), or why not now. */
|
|
520
|
+
async function takeSnapshot(input) {
|
|
521
|
+
const branch = `refs/heads/${input.into}`;
|
|
523
522
|
if (input.worktree) {
|
|
524
523
|
const reason = pauseReason(input.worktree);
|
|
525
524
|
if (reason)
|
|
526
|
-
return { state: 'paused', reason
|
|
525
|
+
return { state: 'paused', reason };
|
|
527
526
|
}
|
|
528
527
|
const snap = input.worktree ? await snapshotWorktree(input.worktree, { device: input.device, stateDir: input.stateDir, signal: input.signal }) : await refTwin(input.repo, branch, input.device);
|
|
529
528
|
if (input.worktree && snap.base !== await verifyCommit(input.repo, branch))
|
|
530
|
-
return { state: 'moved', reason: 'branch_switched'
|
|
531
|
-
|
|
529
|
+
return { state: 'moved', reason: 'branch_switched' };
|
|
530
|
+
return snap;
|
|
531
|
+
}
|
|
532
|
+
const landRefs = (name) => ({ tip: `refs/shard/land/${name}/tip`, result: `refs/shard/land/${name}/result` });
|
|
533
|
+
/** The landing script in the conversation's VM against the twin; a ready landing's tip and result are fetched here. */
|
|
534
|
+
async function mergeInVm(cell, input, snap, mark) {
|
|
535
|
+
const c = input.conversation, hub = input.hub ?? HUB_PATH, vmTmp = input.vmTmp ?? '/tmp';
|
|
532
536
|
const out = `${vmTmp}/shard-land-${randomUUID()}.bundle`;
|
|
533
537
|
const send = async (known) => {
|
|
534
538
|
// The VM has the conversation's base and what earlier landings made there; the bundle carries the rest.
|
|
@@ -582,9 +586,8 @@ async function landingRoundIn(cell, input) {
|
|
|
582
586
|
await runLandingScript(cell, { op: 'confirm', repo: c.worktree_path, hub, name: c.name, upto: land.upto, into: input.into }, input.signal).catch(() => undefined);
|
|
583
587
|
}
|
|
584
588
|
if (land.state !== 'ready')
|
|
585
|
-
return
|
|
586
|
-
const fetched = join(tmpdir(), `shard-land-${randomUUID()}.bundle`);
|
|
587
|
-
const tipRef = `refs/shard/land/${c.name}/tip`, resultRef = `refs/shard/land/${c.name}/result`;
|
|
589
|
+
return land;
|
|
590
|
+
const fetched = join(tmpdir(), `shard-land-${randomUUID()}.bundle`), refs = landRefs(c.name);
|
|
588
591
|
try {
|
|
589
592
|
let bytes;
|
|
590
593
|
if (land.bundle_b64)
|
|
@@ -598,8 +601,8 @@ async function landingRoundIn(cell, input) {
|
|
|
598
601
|
throw new Error('the landing bundle changed while it was read');
|
|
599
602
|
}
|
|
600
603
|
await writeFile(fetched, bytes, { mode: 0o600 });
|
|
601
|
-
await git(input.repo, ['fetch', '-q', '--no-tags', fetched, `+refs/shard/land/tip:${
|
|
602
|
-
if (await verifyCommit(input.repo,
|
|
604
|
+
await git(input.repo, ['fetch', '-q', '--no-tags', fetched, `+refs/shard/land/tip:${refs.tip}`, `+refs/shard/land/result:${refs.result}`]);
|
|
605
|
+
if (await verifyCommit(input.repo, refs.tip) !== land.tip || await verifyCommit(input.repo, refs.result) !== land.result)
|
|
603
606
|
throw new Error('the landing bundle does not match the landing');
|
|
604
607
|
}
|
|
605
608
|
finally {
|
|
@@ -608,22 +611,144 @@ async function landingRoundIn(cell, input) {
|
|
|
608
611
|
await cell.files.remove(out).catch(() => undefined);
|
|
609
612
|
}
|
|
610
613
|
mark('download');
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
614
|
+
return land;
|
|
615
|
+
}
|
|
616
|
+
async function dropLandRefs(repo, name) {
|
|
617
|
+
const refs = landRefs(name);
|
|
618
|
+
await git(repo, ['update-ref', '-d', refs.result], { ok: [0, 1] });
|
|
619
|
+
await git(repo, ['update-ref', '-d', refs.tip], { ok: [0, 1] });
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* The files are on disk. Recording the landing in the VM and copying it to the project hub (shared storage, seconds)
|
|
623
|
+
* runs after, without holding up the next landing.
|
|
624
|
+
*/
|
|
625
|
+
function confirmInVm(cell, input, snap, land, timings, since, now) {
|
|
626
|
+
const c = input.conversation, hub = input.hub ?? HUB_PATH;
|
|
619
627
|
const confirmed = runLandingScript(cell, { op: 'confirm', repo: c.worktree_path, hub, name: c.name, upto: land.upto, into: input.into,
|
|
620
628
|
twin: land.twin, hub_twin_ref: hubTwinRef(input.device, snap.id), tip: land.tip, result: land.result }, input.signal).then(r => {
|
|
621
629
|
if (r.state === 'error')
|
|
622
630
|
throw new Error(`landing confirmation failed: ${r.message ?? r.reason}`);
|
|
623
|
-
timings.confirm = now() -
|
|
631
|
+
timings.confirm = now() - since;
|
|
624
632
|
});
|
|
625
633
|
confirmed.catch(() => undefined);
|
|
626
|
-
return
|
|
634
|
+
return confirmed;
|
|
635
|
+
}
|
|
636
|
+
async function landingRoundIn(cell, input) {
|
|
637
|
+
const now = input.now ?? Date.now, t0 = now(), timings = {};
|
|
638
|
+
const mark = (name) => { timings[name] = now() - t0; };
|
|
639
|
+
const c = input.conversation, branch = `refs/heads/${input.into}`;
|
|
640
|
+
const snap = await takeSnapshot(input);
|
|
641
|
+
if ('state' in snap)
|
|
642
|
+
return { ...snap, timings };
|
|
643
|
+
mark('snapshot');
|
|
644
|
+
const land = await mergeInVm(cell, input, snap, mark);
|
|
645
|
+
if (land.state !== 'ready')
|
|
646
|
+
return { state: land.state === 'error' ? 'error' : land.state, land, timings };
|
|
647
|
+
const outcome = await applyLanding({ worktree: input.worktree, repo: input.repo, branch, base: land.base, twin: land.twin, tip: land.tip, result: land.result }, { backupRoot: input.backupRoot, quietMs: input.quietMs, now, label: { conversation_id: c.id, name: c.name, title: c.title } });
|
|
648
|
+
mark('apply');
|
|
649
|
+
await dropLandRefs(input.repo, c.name);
|
|
650
|
+
if (outcome.state !== 'applied')
|
|
651
|
+
return { ...outcome, reason: 'reason' in outcome ? outcome.reason : 'busy', land, timings };
|
|
652
|
+
return { state: 'landed', land, applied: outcome, timings, confirmed: confirmInVm(cell, input, snap, land, timings, t0, now) };
|
|
653
|
+
}
|
|
654
|
+
async function mergeTree(repo, base, ours, theirs, strategy) {
|
|
655
|
+
const r = await git(repo, ['merge-tree', '--write-tree', '--name-only', '--no-messages', '-z', `--merge-base=${base}`, ...(strategy ? ['-X', strategy] : []), ours, theirs], { ok: [0, 1] });
|
|
656
|
+
const parts = r.stdout.toString('utf8').split('\0');
|
|
657
|
+
return { tree: parts[0].trim(), conflicts: r.code === 1 ? [...new Set(parts.slice(1).filter(Boolean))].sort() : [] };
|
|
658
|
+
}
|
|
659
|
+
/** cur with each first-parent commit of base..tip replayed on it (author and message kept), or null on a conflict. */
|
|
660
|
+
async function replayOnto(repo, cur, base, tip) {
|
|
661
|
+
const commits = (await gitText(repo, ['rev-list', '--reverse', '--first-parent', tip, `^${base}`])).split('\n').filter(Boolean);
|
|
662
|
+
for (const x of commits) {
|
|
663
|
+
const parent = await verifyCommit(repo, `${x}^1`);
|
|
664
|
+
if (!parent)
|
|
665
|
+
return null;
|
|
666
|
+
const { tree, conflicts } = await mergeTree(repo, parent, cur, x);
|
|
667
|
+
if (conflicts.length)
|
|
668
|
+
return null;
|
|
669
|
+
if (tree === await gitText(repo, ['rev-parse', `${cur}^{tree}`]))
|
|
670
|
+
continue;
|
|
671
|
+
const [name, email, date] = (await gitText(repo, ['log', '-1', '--format=%an%x00%ae%x00%ad', '--date=raw', x])).split('\0');
|
|
672
|
+
const raw = (await git(repo, ['cat-file', 'commit', x])).stdout, message = raw.subarray(raw.indexOf('\n\n') + 2);
|
|
673
|
+
cur = await gitText(repo, ['commit-tree', tree, '-p', cur, '-F', '-'], { input: message, env: { GIT_AUTHOR_NAME: name, GIT_AUTHOR_EMAIL: email, GIT_AUTHOR_DATE: date } });
|
|
674
|
+
}
|
|
675
|
+
return cur;
|
|
676
|
+
}
|
|
677
|
+
/** The user's working state with the branch's new commits merged in; where both changed the same lines, the user's text. */
|
|
678
|
+
async function resultCommit(repo, base, twin, tip) {
|
|
679
|
+
let { tree, conflicts } = await mergeTree(repo, base, twin, tip);
|
|
680
|
+
const shadowed = conflicts;
|
|
681
|
+
if (conflicts.length) {
|
|
682
|
+
({ tree, conflicts } = await mergeTree(repo, base, twin, tip, 'ours'));
|
|
683
|
+
if (conflicts.length) {
|
|
684
|
+
const dir = join(tmpdir(), `shard-index-${randomUUID()}`), env = { GIT_INDEX_FILE: dir };
|
|
685
|
+
try {
|
|
686
|
+
await git(repo, ['read-tree', tree], { env });
|
|
687
|
+
await git(repo, ['rm', '-r', '-q', '--cached', '--ignore-unmatch', '--', ...conflicts], { env });
|
|
688
|
+
const listing = (await git(repo, ['ls-tree', '-r', '-z', '--full-tree', twin, '--', ...conflicts])).stdout.toString('utf8').split('\0').filter(Boolean)
|
|
689
|
+
.map(l => l.replace(/ (blob|commit) /, ' '));
|
|
690
|
+
if (listing.length)
|
|
691
|
+
await git(repo, ['update-index', '-z', '--index-info'], { env, input: listing.join('\0') + '\0' });
|
|
692
|
+
tree = await gitText(repo, ['write-tree'], { env });
|
|
693
|
+
}
|
|
694
|
+
finally {
|
|
695
|
+
rmSync(dir, { force: true });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
const commit = await gitText(repo, ['commit-tree', tree, '-p', tip, '-F', '-'], { input: 'Shardflux landing result\n', env: { GIT_AUTHOR_NAME: 'Shardflux', GIT_AUTHOR_EMAIL: 'landing@shardflux.dev', GIT_COMMITTER_NAME: 'Shardflux', GIT_COMMITTER_EMAIL: 'landing@shardflux.dev' } });
|
|
700
|
+
return { commit, shadowed };
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Lands several finished conversations into one branch with one apply: one twin, the merges in their VMs in parallel,
|
|
704
|
+
* then their new commits stacked here in the given (finish) order and the files written once. A conversation whose
|
|
705
|
+
* commits do not stack cleanly on the earlier ones is 'deferred' (it lands next, against the new state).
|
|
706
|
+
*/
|
|
707
|
+
export async function landingBatch(items, shared) {
|
|
708
|
+
const now = shared.now ?? Date.now, t0 = now(), timings = {};
|
|
709
|
+
const mark = (name) => { timings[name] = now() - t0; };
|
|
710
|
+
const snap = await takeSnapshot(shared);
|
|
711
|
+
if ('state' in snap)
|
|
712
|
+
return items.map(() => ({ ...snap, timings }));
|
|
713
|
+
mark('snapshot');
|
|
714
|
+
const inputs = items.map(it => ({ ...shared, mode: 'auto', conversation: it.conversation, upto: it.upto, known: it.known }));
|
|
715
|
+
const lands = await Promise.all(items.map((it, i) => mergeInVm(it.cell, inputs[i], snap, () => undefined)
|
|
716
|
+
.catch((error) => ({ state: 'error', reason: 'exception', message: error instanceof Error ? error.message : String(error) }))));
|
|
717
|
+
mark('merge');
|
|
718
|
+
const outcomes = lands.map(land => land.state === 'ready' ? undefined : { state: land.state === 'error' ? 'error' : land.state, land, timings });
|
|
719
|
+
const branch = `refs/heads/${shared.into}`, base = snap.base, included = [];
|
|
720
|
+
let cur = base;
|
|
721
|
+
for (const [i, land] of lands.entries()) {
|
|
722
|
+
if (land.state !== 'ready')
|
|
723
|
+
continue;
|
|
724
|
+
const next = cur === base ? land.tip : await replayOnto(shared.repo, cur, base, land.tip);
|
|
725
|
+
if (next) {
|
|
726
|
+
cur = next;
|
|
727
|
+
included.push(i);
|
|
728
|
+
}
|
|
729
|
+
else
|
|
730
|
+
outcomes[i] = { state: 'deferred', reason: 'stacked', land, timings };
|
|
731
|
+
}
|
|
732
|
+
try {
|
|
733
|
+
if (!included.length)
|
|
734
|
+
return outcomes.map(o => o);
|
|
735
|
+
const result = included.length === 1 ? { commit: lands[included[0]].result, shadowed: lands[included[0]].shadowed ?? [] } : await resultCommit(shared.repo, base, snap.commit, cur);
|
|
736
|
+
mark('stack');
|
|
737
|
+
const first = items[included[0]].conversation;
|
|
738
|
+
const applied = await applyLanding({ worktree: shared.worktree, repo: shared.repo, branch, base, twin: snap.commit, tip: cur, result: result.commit }, { backupRoot: shared.backupRoot, quietMs: shared.quietMs, now, label: { conversation_id: first.id, name: included.map(i => items[i].conversation.name).join('+'), title: first.title } });
|
|
739
|
+
mark('apply');
|
|
740
|
+
for (const i of included) {
|
|
741
|
+
const land = { ...lands[i], shadowed: result.shadowed };
|
|
742
|
+
outcomes[i] = applied.state === 'applied'
|
|
743
|
+
? { state: 'landed', land, applied, timings, confirmed: confirmInVm(items[i].cell, inputs[i], snap, land, timings, t0, now) }
|
|
744
|
+
: { ...applied, reason: 'reason' in applied ? applied.reason : 'busy', land, timings };
|
|
745
|
+
}
|
|
746
|
+
return outcomes.map(o => o);
|
|
747
|
+
}
|
|
748
|
+
finally {
|
|
749
|
+
for (const it of items)
|
|
750
|
+
await dropLandRefs(shared.repo, it.conversation.name);
|
|
751
|
+
}
|
|
627
752
|
}
|
|
628
753
|
export class TwinLedger {
|
|
629
754
|
store;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardflux/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0-next.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "shard: the Shardflux command line. Create an account, sign in, set up keys and billing, then open, run commands in, move files to, suspend, resume and fork serverless agent workspaces.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -43,29 +43,17 @@
|
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
+
"@shardflux/sdk": "0.24.0-next.4",
|
|
46
47
|
"classic-level": "3.0.0",
|
|
47
48
|
"smol-toml": "1.9.0",
|
|
48
49
|
"snappyjs": "0.7.0",
|
|
49
|
-
"yaml": "2.9.1"
|
|
50
|
-
"@shardflux/sdk": "^0.22.0"
|
|
51
|
-
},
|
|
52
|
-
"devDependencies": {
|
|
53
|
-
"@eslint/js": "10.0.1",
|
|
54
|
-
"@types/node": "24.13.6",
|
|
55
|
-
"eslint": "10.11.0",
|
|
56
|
-
"playwright": "1.62.1",
|
|
57
|
-
"typescript": "5.9.3",
|
|
58
|
-
"typescript-eslint": "8.70.1"
|
|
50
|
+
"yaml": "2.9.1"
|
|
59
51
|
},
|
|
60
52
|
"optionalDependencies": {
|
|
61
|
-
"@shardflux/menubar-darwin": "^0.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"build": "node scripts/build.mjs",
|
|
65
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
66
|
-
"lint": "eslint .",
|
|
67
|
-
"test": "node --test test/*.test.ts"
|
|
53
|
+
"@shardflux/menubar-darwin": "^0.5.0",
|
|
54
|
+
"@shardflux/desktop-linux-x64": "0.1.0-next.4",
|
|
55
|
+
"@shardflux/desktop-linux-arm64": "0.1.0-next.4"
|
|
68
56
|
},
|
|
69
57
|
"main": "./dist/index.js",
|
|
70
58
|
"types": "./dist/index.d.ts"
|
|
71
|
-
}
|
|
59
|
+
}
|