@shardflux/cli 0.20.0 → 0.23.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 +312 -1
- package/README.md +242 -75
- package/dist/account-args.js +7 -2
- package/dist/account-commands.js +56 -106
- package/dist/account-pool.d.ts +19 -15
- package/dist/account-pool.js +47 -96
- package/dist/account-scope.d.ts +65 -0
- package/dist/account-scope.js +246 -0
- package/dist/acp-journal.js +58 -6
- package/dist/acp-login.d.ts +13 -5
- package/dist/acp-login.js +37 -8
- package/dist/acp-models.d.ts +6 -1
- package/dist/acp-models.js +29 -19
- package/dist/acp.js +120 -14
- package/dist/agent-commands.d.ts +4 -1
- package/dist/agent-commands.js +110 -42
- package/dist/agent-config-return.d.ts +46 -2
- package/dist/agent-config-return.js +110 -4
- package/dist/agent-credentials.js +7 -3
- package/dist/agent-repository.d.ts +16 -3
- package/dist/agent-repository.js +149 -74
- package/dist/agent-setup.d.ts +14 -3
- package/dist/agent-setup.js +78 -36
- package/dist/agent-target-commands.d.ts +20 -1
- package/dist/agent-target-commands.js +73 -31
- package/dist/agent-targets.js +6 -1
- package/dist/args.d.ts +1 -1
- package/dist/args.js +95 -54
- package/dist/bin.js +5 -1
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -14
- package/dist/claude-cloud-login.d.ts +24 -2
- package/dist/claude-cloud-login.js +151 -11
- package/dist/cli-proxy-hub.d.ts +61 -25
- package/dist/cli-proxy-hub.js +202 -54
- package/dist/cli-proxy.d.ts +44 -1
- package/dist/cli-proxy.js +177 -28
- package/dist/client-settings.d.ts +39 -0
- package/dist/client-settings.js +91 -0
- package/dist/codex-cloud.d.ts +14 -4
- package/dist/codex-cloud.js +135 -31
- package/dist/codex-linux.d.ts +14 -0
- package/dist/codex-linux.js +69 -0
- package/dist/command-center-actions.js +18 -5
- package/dist/command-center-model.d.ts +2 -0
- package/dist/command-center-model.js +6 -1
- package/dist/command-center.js +3 -3
- package/dist/commands.js +173 -24
- package/dist/conversation-commands.d.ts +5 -0
- package/dist/conversation-commands.js +8 -5
- package/dist/conversation-terminal.d.ts +10 -2
- package/dist/conversation-terminal.js +78 -13
- package/dist/conversations.d.ts +86 -1
- package/dist/conversations.js +306 -23
- package/dist/desktop-linux.js +16 -3
- package/dist/device-bridge.d.ts +1 -1
- package/dist/device-bridge.js +6 -4
- package/dist/device-helper.js +43 -22
- package/dist/errors.d.ts +29 -2
- package/dist/errors.js +50 -7
- package/dist/filesystem-commands.d.ts +16 -9
- package/dist/filesystem-commands.js +133 -57
- package/dist/format.d.ts +2 -1
- package/dist/format.js +10 -1
- package/dist/handlers.js +3 -1
- package/dist/initial-prompt.d.ts +8 -0
- package/dist/initial-prompt.js +43 -1
- package/dist/large-media.d.ts +14 -0
- package/dist/large-media.js +100 -0
- package/dist/linux-terminal.d.ts +5 -3
- package/dist/linux-terminal.js +6 -20
- package/dist/local-sessions.d.ts +26 -2
- package/dist/local-sessions.js +113 -24
- package/dist/machine-setup.d.ts +11 -1
- package/dist/machine-setup.js +69 -22
- package/dist/machine-status.d.ts +67 -0
- package/dist/machine-status.js +140 -0
- package/dist/main.js +11 -13
- package/dist/menubar.d.ts +6 -5
- package/dist/menubar.js +38 -28
- package/dist/move-worktrees-restore.py +144 -0
- package/dist/move-worktrees.d.ts +69 -0
- package/dist/move-worktrees.js +252 -0
- package/dist/move.d.ts +10 -1
- package/dist/move.js +111 -16
- package/dist/onboarding.d.ts +11 -7
- package/dist/onboarding.js +63 -29
- package/dist/prepared-repository.d.ts +204 -0
- package/dist/prepared-repository.js +860 -0
- package/dist/prepared-repository.py +1307 -0
- package/dist/project-preparation.d.ts +62 -0
- package/dist/project-preparation.js +316 -0
- package/dist/project-sync-manifest.d.ts +4 -0
- package/dist/project-sync-manifest.js +5 -1
- package/dist/project-sync-transfer.js +1 -1
- package/dist/project-sync.d.ts +22 -4
- package/dist/project-sync.js +34 -8
- package/dist/project-template-setup.d.ts +33 -1
- package/dist/project-template-setup.js +59 -2
- package/dist/secret-ask.d.ts +72 -0
- package/dist/secret-ask.js +351 -0
- package/dist/seed-phases.d.ts +8 -1
- package/dist/seed-phases.js +17 -2
- package/dist/setup-cache.d.ts +7 -0
- package/dist/setup-cache.js +32 -0
- package/dist/t3-launch.d.ts +2 -1
- package/dist/t3-launch.js +12 -6
- package/dist/t3-mode.d.ts +8 -2
- package/dist/t3-mode.js +19 -4
- package/dist/t3-phone.d.ts +4 -2
- package/dist/t3-phone.js +12 -4
- package/dist/t3-proxy.d.ts +81 -0
- package/dist/t3-proxy.js +325 -0
- package/dist/terminal-apps.d.ts +168 -0
- package/dist/terminal-apps.js +574 -0
- package/dist/terminal-onboarding.js +6 -1
- package/dist/this-computer.d.ts +11 -0
- package/dist/this-computer.js +36 -0
- package/dist/thread-on-computer.js +4 -1
- package/dist/update.d.ts +2 -0
- package/dist/update.js +43 -9
- package/dist/worktree-twin.d.ts +10 -2
- package/dist/worktree-twin.js +27 -9
- package/package.json +5 -5
package/dist/cli-proxy-hub.js
CHANGED
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
* Hub info (`cli-proxy.json`, 0600, in the config directory, in the hub and in the machine template so every VM has
|
|
12
12
|
* it): { version, api_url, workspace_id, callback_url, callback_created_at, management_key }. Never printed or logged.
|
|
13
13
|
*/
|
|
14
|
-
import { randomBytes } from 'node:crypto';
|
|
14
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
15
15
|
import { readFileSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
|
-
import { Document, isMap, parseDocument } from 'yaml';
|
|
17
|
+
import { Document, isMap, isScalar, isSeq, parseDocument } from 'yaml';
|
|
18
18
|
import { ShardfluxApiError } from '@shardflux/sdk';
|
|
19
19
|
import { writePrivateFile } from "./credentials.js";
|
|
20
20
|
import { CodedError, UsageError } from "./errors.js";
|
|
@@ -42,21 +42,14 @@ export const HUB_TEMPLATE = 'codex-connector';
|
|
|
42
42
|
/**
|
|
43
43
|
* The codex-connector version a hub runs at least: the first with the hub's boot service and an sf-agent that serves
|
|
44
44
|
* the VM port (cli-proxy-filter). New hubs are created on it; an older hub is upgraded in place (its home, and so its
|
|
45
|
-
* logins and settings, stay) before VMs get the hub's address.
|
|
45
|
+
* logins and settings, stay) before VMs get the hub's address. A hub on a newer version keeps it (never downgraded).
|
|
46
46
|
*/
|
|
47
47
|
export const HUB_TEMPLATE_VERSION = 4;
|
|
48
|
-
/**
|
|
49
|
-
* The codex-connector version whose sf-agent runs the account pool router: a hub set up for pooled accounts is created
|
|
50
|
-
* on it, or upgraded to it. HUB_TEMPLATE_VERSION moves to it once it is registered in every environment.
|
|
51
|
-
*/
|
|
52
|
-
export const POOL_HUB_TEMPLATE_VERSION = 5;
|
|
53
48
|
export const HUB_CAPS = { cpu_millis: 1000, memory_mib: 256, disk_gib: 2, allocation_mode: 'fixed' };
|
|
54
49
|
export const HUB_INFO_FILE = 'cli-proxy.json';
|
|
55
50
|
/** Paths in the hub workspace. */
|
|
56
51
|
export const HUB_DIR = '/home/user/.cli-proxy-hub';
|
|
57
52
|
export const HUB_AUTH_DIR = '/home/user/.cli-proxy-api';
|
|
58
|
-
/** Pool mode: the router reads the API URL and the hub token (`sfh_…`) from these hub files (0600). */
|
|
59
|
-
export const HUB_POOL_FILES = { apiUrl: `${HUB_DIR}/api-url`, apiToken: `${HUB_DIR}/api-token` };
|
|
60
53
|
const CALLBACK_PATH = /^\/__shardflux\/callback\/sfcb_[A-Za-z0-9_-]+\/$/;
|
|
61
54
|
function callbackUrlOk(cb) {
|
|
62
55
|
try {
|
|
@@ -315,7 +308,7 @@ export function sdkHubCloud(cloud, opts = {}) {
|
|
|
315
308
|
}
|
|
316
309
|
},
|
|
317
310
|
async authFiles() {
|
|
318
|
-
const r = await hub.sh(
|
|
311
|
+
const r = await hub.sh(AUTH_FILES_SCRIPT);
|
|
319
312
|
if (r.code !== 0)
|
|
320
313
|
throw new HubError(`Could not list the hub’s logins: ${r.stderr.trim() || `exit ${r.code}`}`);
|
|
321
314
|
return r.stdout.split('\n').filter(Boolean);
|
|
@@ -380,6 +373,11 @@ export function sdkHubCloud(cloud, opts = {}) {
|
|
|
380
373
|
probe: (url) => probeCallback(url),
|
|
381
374
|
};
|
|
382
375
|
}
|
|
376
|
+
/** The hub's auth files, oldest first (modification time, then path in byte order), one relative path per line. */
|
|
377
|
+
export const AUTH_FILES_SCRIPT = `set -euo pipefail
|
|
378
|
+
[ -d ${HUB_AUTH_DIR} ] || exit 0; cd ${HUB_AUTH_DIR}
|
|
379
|
+
find . -type f -name '*.json' -printf '%T@ %P\\n' | LC_ALL=C sort -t ' ' -k1,1n -k2 | cut -d ' ' -f 2-
|
|
380
|
+
`;
|
|
383
381
|
/** Installs the pinned release once: download, verify its SHA-256, then move the binary into place. */
|
|
384
382
|
export const INSTALL_SCRIPT = `set -euo pipefail
|
|
385
383
|
v=${CLI_PROXY_VERSION}; dir=${HUB_DIR}; bin="$dir/bin/cli-proxy-api-$v"
|
|
@@ -600,12 +598,12 @@ export async function ensureCliProxyHub(cloud, opts) {
|
|
|
600
598
|
const progress = opts.progress ?? (() => { });
|
|
601
599
|
const local = readHubInfo(opts.dir, opts.apiUrl);
|
|
602
600
|
progress('Opening the CLIProxyAPI hub…');
|
|
603
|
-
|
|
604
|
-
const want = opts.pool ? POOL_HUB_TEMPLATE_VERSION : HUB_TEMPLATE_VERSION;
|
|
601
|
+
const want = HUB_TEMPLATE_VERSION;
|
|
605
602
|
let opened = await hubCloud.open(!opts.repairOnly, want);
|
|
606
603
|
if (!opened)
|
|
607
604
|
throw new HubError('This account has no hosted CLIProxyAPI yet.', 'move yours: shard cli-proxy import');
|
|
608
|
-
// A hub on an older template is upgraded before VMs get its address (a plain repair only starts it again).
|
|
605
|
+
// A hub on an older template is upgraded before VMs get its address (a plain repair only starts it again). One on a
|
|
606
|
+
// newer version (codex-connector 5) keeps it: its VM port serves exactly as on 4 once retirePool has run.
|
|
609
607
|
const template = opened.template;
|
|
610
608
|
if (!opened.created && (!opts.repairOnly || opts.renewInvalid) && hubCloud.upgrade && template?.version != null && template.version < want && (template.upgradeTo ?? 0) >= want) {
|
|
611
609
|
progress('Updating the CLIProxyAPI hub…');
|
|
@@ -654,14 +652,11 @@ export async function ensureCliProxyHub(cloud, opts) {
|
|
|
654
652
|
}
|
|
655
653
|
if ((await ws.list(HUB_AUTH_DIR)) === null)
|
|
656
654
|
await ws.sh(`mkdir -p -m 0700 ${HUB_AUTH_DIR}`);
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
}
|
|
663
|
-
await setUpPool(ws, opts.apiUrl, opts.pool);
|
|
664
|
-
}
|
|
655
|
+
// A hub that once ran the retired account pool router: its files go, and its sf-agent starts again without it.
|
|
656
|
+
if (await retirePool(ws))
|
|
657
|
+
restart = true;
|
|
658
|
+
// The user's logins stay as they are; only the retired account pool router's files are kept disabled.
|
|
659
|
+
const logins = await ws.withLock('logins', () => settleHubLogins(ws));
|
|
665
660
|
const started = await ws.sh(START_SCRIPT, [restart ? '1' : '0'], { timeoutMs: 180_000 });
|
|
666
661
|
const startLines = started.stdout.trim().split('\n');
|
|
667
662
|
const state = startLines.find((l) => ['running', 'started', 'restarted'].includes(l));
|
|
@@ -681,46 +676,199 @@ export async function ensureCliProxyHub(cloud, opts) {
|
|
|
681
676
|
};
|
|
682
677
|
if (!local || hubInfoText(local) !== hubInfoText(info))
|
|
683
678
|
writeHubInfo(opts.dir, info);
|
|
684
|
-
return { info, created, process: state, callbackReplaced, config };
|
|
679
|
+
return { info, created, process: state, callbackReplaced, config, logins };
|
|
680
|
+
}
|
|
681
|
+
const docOf = (text) => {
|
|
682
|
+
try {
|
|
683
|
+
const v = JSON.parse(text ?? '');
|
|
684
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? v : null;
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
const publicLogins = (logins) => logins.filter((l) => !l.pool).map(({ file, provider, email, disabled, active }) => ({ file, provider, email, disabled, active }));
|
|
691
|
+
/** The hub's logins, oldest first, as they are (nothing changes). Files the retired account pool router left are not listed. */
|
|
692
|
+
export async function listHubLogins(ws) {
|
|
693
|
+
return publicLogins(await readHubLogins(ws));
|
|
694
|
+
}
|
|
695
|
+
/** The hub's logins, oldest first (the order authFiles lists them in). */
|
|
696
|
+
async function readHubLogins(ws) {
|
|
697
|
+
const files = await ws.authFiles();
|
|
698
|
+
const texts = await Promise.all(files.map((f) => ws.read(`${HUB_AUTH_DIR}/${f}`)));
|
|
699
|
+
return files.map((file, i) => {
|
|
700
|
+
const text = texts[i] ?? null;
|
|
701
|
+
const doc = docOf(text);
|
|
702
|
+
const type = typeof doc?.type === 'string' && doc.type.trim() ? doc.type.trim().toLowerCase() : null;
|
|
703
|
+
const email = typeof doc?.email === 'string' && doc.email ? doc.email : null;
|
|
704
|
+
// The retired pool router's files (`sfa-<account id>.json`, `"prefix": "sfa-…"`) were never the user's logins.
|
|
705
|
+
const pool = /^sfa-[^/]+\.json$/.test(file) || (typeof doc?.prefix === 'string' && doc.prefix.startsWith('sfa-'));
|
|
706
|
+
const disabled = doc?.disabled === true;
|
|
707
|
+
return { file, provider: type, email, disabled, active: !pool && !disabled && type !== null, text, doc, pool };
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
/** The auth file's text with `disabled` set (true) or removed (false), every other field kept; null when unchanged. */
|
|
711
|
+
export function withDisabled(text, disabled) {
|
|
712
|
+
const doc = docOf(text);
|
|
713
|
+
if (!doc || (doc.disabled === true) === disabled)
|
|
714
|
+
return null;
|
|
715
|
+
if (disabled)
|
|
716
|
+
doc.disabled = true;
|
|
717
|
+
else
|
|
718
|
+
delete doc.disabled;
|
|
719
|
+
return `${JSON.stringify(doc, null, text.trimEnd().includes('\n') ? 2 : undefined)}${text.endsWith('\n') ? '\n' : ''}`;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Replaces one auth file atomically (0600, a temporary file in its folder then rename), only while it still holds what
|
|
723
|
+
* the device read ($2, its SHA-256): CLIProxyAPI rewrites a file when it refreshes the login, and a newer token must
|
|
724
|
+
* never be overwritten. The modification time stays, so the order of the hub's logins (oldest first) does not change.
|
|
725
|
+
* $1 is the path relative to the auth directory, the new content comes on standard input. Prints `written`, `changed`
|
|
726
|
+
* or `gone`.
|
|
727
|
+
*/
|
|
728
|
+
export const SET_AUTH_SCRIPT = `set -euo pipefail
|
|
729
|
+
a=${HUB_AUTH_DIR}; rel=$1; want=$2
|
|
730
|
+
case "$rel" in ''|/*|*..*) echo "not a path inside the auth folder" >&2; exit 2 ;; esac
|
|
731
|
+
f="$a/$rel"
|
|
732
|
+
[ -f "$f" ] || { echo gone; exit 0; }
|
|
733
|
+
tmp=$(mktemp "$(dirname "$f")/.writing.XXXXXX"); trap 'rm -f "$tmp"' EXIT
|
|
734
|
+
chmod 0600 "$tmp"; cat > "$tmp"; touch -r "$f" "$tmp"
|
|
735
|
+
if [ "$(sha256sum < "$f" | cut -d' ' -f1)" != "$want" ]; then echo changed; exit 0; fi
|
|
736
|
+
mv -f "$tmp" "$f"; trap - EXIT
|
|
737
|
+
echo written
|
|
738
|
+
`;
|
|
739
|
+
const sha256 = (text) => createHash('sha256').update(text, 'utf8').digest('hex');
|
|
740
|
+
/** Sets one login's `disabled` (true) or clears it (false), reading the file again when CLIProxyAPI changed it meanwhile. */
|
|
741
|
+
async function setDisabled(ws, login, disabled) {
|
|
742
|
+
let text = login.text;
|
|
743
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
744
|
+
if (text === null)
|
|
745
|
+
return;
|
|
746
|
+
const next = withDisabled(text, disabled);
|
|
747
|
+
if (next === null)
|
|
748
|
+
return;
|
|
749
|
+
const r = await ws.sh(SET_AUTH_SCRIPT, [login.file, sha256(text)], { stdin: next });
|
|
750
|
+
const out = r.stdout.trim();
|
|
751
|
+
if (r.code !== 0)
|
|
752
|
+
throw new HubError(`Could not update ${login.file} in the hub: ${r.stderr.trim().split('\n').at(-1) || `exit ${r.code}`}`, 'run: shard cli-proxy import');
|
|
753
|
+
if (out === 'written' || out === 'gone')
|
|
754
|
+
return;
|
|
755
|
+
text = await ws.read(`${HUB_AUTH_DIR}/${login.file}`);
|
|
756
|
+
}
|
|
757
|
+
throw new HubError(`${login.file} kept changing in the hub while it was being ${disabled ? 'disabled' : 'enabled'}.`, 'run: shard cli-proxy import');
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Under the hub's `logins` lock: the retired account pool router's files stay disabled (they were never the user's);
|
|
761
|
+
* every other login keeps the state its owner gave it. Returns the logins as they are now, oldest first.
|
|
762
|
+
*/
|
|
763
|
+
export async function settleHubLogins(ws) {
|
|
764
|
+
const logins = await readHubLogins(ws);
|
|
765
|
+
for (const l of logins)
|
|
766
|
+
if (l.pool && !l.disabled)
|
|
767
|
+
await setDisabled(ws, l, true);
|
|
768
|
+
return publicLogins(logins);
|
|
685
769
|
}
|
|
686
770
|
/**
|
|
687
|
-
* The
|
|
688
|
-
*
|
|
689
|
-
*
|
|
771
|
+
* The user's choice of which logins serve requests (`shard cli-proxy use`; call it under the hub's `logins` lock).
|
|
772
|
+
* `only[provider]` names the one login of that provider that stays enabled (it is enabled first, then the provider's
|
|
773
|
+
* other logins are disabled, so the provider is never without one); `all` enables every login of the hub, which then
|
|
774
|
+
* routes across them as its config says. Providers the choice does not name are left as they are. Returns the logins
|
|
775
|
+
* as they are now, oldest first.
|
|
690
776
|
*/
|
|
691
|
-
export function
|
|
692
|
-
|
|
777
|
+
export async function useHubLogins(ws, choice) {
|
|
778
|
+
const logins = await readHubLogins(ws);
|
|
779
|
+
const only = choice.only ?? {};
|
|
780
|
+
for (const [p, file] of Object.entries(only)) {
|
|
781
|
+
if (!logins.some((l) => !l.pool && l.provider === p && l.file === file))
|
|
782
|
+
throw new HubError(`The hub has no ${p} login ${file} any more.`, 'see the hub’s logins: shard cli-proxy');
|
|
783
|
+
}
|
|
784
|
+
const wanted = (l) => {
|
|
785
|
+
if (l.pool || l.provider === null)
|
|
786
|
+
return null;
|
|
787
|
+
if (only[l.provider] !== undefined)
|
|
788
|
+
return only[l.provider] === l.file;
|
|
789
|
+
return choice.all ? true : null;
|
|
790
|
+
};
|
|
791
|
+
for (const l of logins)
|
|
792
|
+
if (wanted(l) === true && l.disabled)
|
|
793
|
+
await setDisabled(ws, l, false);
|
|
794
|
+
for (const l of logins)
|
|
795
|
+
if (wanted(l) === false && !l.disabled)
|
|
796
|
+
await setDisabled(ws, l, true);
|
|
797
|
+
// Computers still on CLI 0.22.0 keep the login active.json names for each provider: it names this choice too.
|
|
798
|
+
if (Object.keys(only).length) {
|
|
799
|
+
const text = await ws.read(LEGACY_ACTIVE_FILE);
|
|
800
|
+
let stored = {};
|
|
801
|
+
try {
|
|
802
|
+
const v = JSON.parse(text ?? '');
|
|
803
|
+
if (v && typeof v === 'object' && !Array.isArray(v))
|
|
804
|
+
stored = v;
|
|
805
|
+
}
|
|
806
|
+
catch { /* none yet */ }
|
|
807
|
+
const next = `${JSON.stringify({ ...stored, ...only }, null, 2)}\n`;
|
|
808
|
+
if (next !== text) {
|
|
809
|
+
const r = await ws.sh(LEGACY_ACTIVE_SCRIPT, [], { stdin: next });
|
|
810
|
+
if (r.code !== 0)
|
|
811
|
+
throw new HubError(`The hub did not record the choice: ${r.stderr.trim().split('\n').at(-1) || `exit ${r.code}`}`, 'run: shard cli-proxy use <email>');
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return listHubLogins(ws);
|
|
815
|
+
}
|
|
816
|
+
/** Where CLI 0.22.0 reads each provider's one login ({"<provider>": "<auth file>"}); written atomically (0600). */
|
|
817
|
+
export const LEGACY_ACTIVE_FILE = `${HUB_DIR}/active.json`;
|
|
818
|
+
export const LEGACY_ACTIVE_SCRIPT = `set -euo pipefail
|
|
819
|
+
dir=${HUB_DIR}; mkdir -p "$dir"; umask 077
|
|
820
|
+
tmp=$(mktemp "$dir/.active.XXXXXX"); trap 'rm -f "$tmp"' EXIT
|
|
821
|
+
cat > "$tmp"; mv -f "$tmp" "$dir/active.json"; trap - EXIT
|
|
822
|
+
echo written
|
|
823
|
+
`;
|
|
824
|
+
/** What the retired account pool router kept in the hub (codex-connector 5 with pooled accounts). */
|
|
825
|
+
const POOL_LEFTOVERS = ['api-token', 'api-url', 'pool.json', 'pool-creds.json', 'pool-status.json', 'pool-client.key', 'pool.lock'];
|
|
826
|
+
/** Removes the router's files; prints `retired` when its hub token was there (its sf-agent then starts again without it). */
|
|
827
|
+
export const RETIRE_POOL_SCRIPT = `set -euo pipefail
|
|
828
|
+
cd ${HUB_DIR} 2>/dev/null || exit 0
|
|
829
|
+
router=no; if [ -e api-token ] || [ -e api-url ]; then router=yes; fi
|
|
830
|
+
rm -f ${POOL_LEFTOVERS.join(' ')}
|
|
831
|
+
if [ "$router" = yes ]; then echo retired; fi
|
|
832
|
+
`;
|
|
833
|
+
/** The config without the router's own client key (`api-keys`, or v8 `access.api-keys`); the text itself when absent. */
|
|
834
|
+
export function withoutClientKey(text, key) {
|
|
835
|
+
const doc = parseDocument(text);
|
|
693
836
|
if (doc.errors.length > 0 || !isMap(doc.contents))
|
|
694
|
-
doc = new Document({});
|
|
695
|
-
const v8 = doc.hasIn(['config-version']) || V8_SECTIONS.some((k) => isMap(doc.getIn([k])));
|
|
696
|
-
const path = v8 ? ['requests', 'passthrough-headers'] : ['passthrough-headers'];
|
|
697
|
-
if (doc.getIn(path) === true)
|
|
698
837
|
return text;
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
if ((await ws.read(HUB_POOL_FILES.apiUrl))?.trim() !== apiUrl)
|
|
709
|
-
await ws.write(HUB_POOL_FILES.apiUrl, `${apiUrl}\n`, '0600');
|
|
710
|
-
const have = (await ws.read(HUB_POOL_FILES.apiToken))?.trim() ?? '';
|
|
711
|
-
if (pool.renew || !HUB_TOKEN_RE.test(have)) {
|
|
712
|
-
const token = (await pool.hubToken(ws.id)).trim();
|
|
713
|
-
if (!HUB_TOKEN_RE.test(token))
|
|
714
|
-
throw new HubError('The Shardflux API did not return a hub token.');
|
|
715
|
-
await ws.write(HUB_POOL_FILES.apiToken, `${token}\n`, '0600');
|
|
838
|
+
let changed = false;
|
|
839
|
+
for (const path of [['api-keys'], ['access', 'api-keys']]) {
|
|
840
|
+
const seq = doc.getIn(path, true);
|
|
841
|
+
if (!isSeq(seq))
|
|
842
|
+
continue;
|
|
843
|
+
const keep = seq.items.filter((item) => (isScalar(item) ? item.value : item) !== key);
|
|
844
|
+
if (keep.length !== seq.items.length) {
|
|
845
|
+
seq.items = keep;
|
|
846
|
+
changed = true;
|
|
716
847
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
848
|
+
}
|
|
849
|
+
return changed ? doc.toString() : text;
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* A hub that ran the account pool router (codex-connector 5): its hub token, API URL and router state go, and so does
|
|
853
|
+
* the router's client key in the hub's config. Returns whether the router could be running (the hub then restarts its
|
|
854
|
+
* sf-agent, which starts without it). A hub that never pooled costs one directory listing.
|
|
855
|
+
*/
|
|
856
|
+
async function retirePool(ws) {
|
|
857
|
+
const names = (await ws.list(HUB_DIR)) ?? [];
|
|
858
|
+
if (!names.some((n) => POOL_LEFTOVERS.includes(n)))
|
|
859
|
+
return false;
|
|
860
|
+
const key = (await ws.read(`${HUB_DIR}/pool-client.key`))?.trim();
|
|
861
|
+
const configPath = `${HUB_DIR}/config.yaml`;
|
|
862
|
+
const config = key ? await ws.read(configPath) : null;
|
|
863
|
+
if (key && config !== null) {
|
|
864
|
+
const next = withoutClientKey(config, key);
|
|
721
865
|
if (next !== config)
|
|
722
866
|
await ws.write(configPath, next, '0600');
|
|
723
|
-
}
|
|
867
|
+
}
|
|
868
|
+
const r = await ws.sh(RETIRE_POOL_SCRIPT);
|
|
869
|
+
if (r.code !== 0)
|
|
870
|
+
throw new HubError(`The hub did not remove the account pool’s files: ${r.stderr.trim().split('\n').at(-1) || `exit ${r.code}`}`, 'run: shard cli-proxy import');
|
|
871
|
+
return r.stdout.trim() === 'retired';
|
|
724
872
|
}
|
|
725
873
|
/**
|
|
726
874
|
* The hub's one callback URL. Under the hub's callback lock: the stored one is kept while the port's callback still
|
package/dist/cli-proxy.d.ts
CHANGED
|
@@ -211,6 +211,10 @@ export interface HubAccount {
|
|
|
211
211
|
email: string | null;
|
|
212
212
|
status: string | null;
|
|
213
213
|
disabled: boolean;
|
|
214
|
+
/** The auth file, relative to the hub's auth directory (null when CLIProxyAPI does not name it). */
|
|
215
|
+
file: string | null;
|
|
216
|
+
/** It serves requests (enabled); a disabled login stays in the hub unused. */
|
|
217
|
+
active: boolean;
|
|
214
218
|
}
|
|
215
219
|
/** The hub's logins from its management API (wakes the hub). */
|
|
216
220
|
export declare function hubAccounts(info: HubInfo, timeoutMs?: number): Promise<HubAccount[]>;
|
|
@@ -230,6 +234,12 @@ export interface ImportOptions {
|
|
|
230
234
|
machine?: boolean;
|
|
231
235
|
platform?: NodeJS.Platform;
|
|
232
236
|
}
|
|
237
|
+
/** A login named by provider, email and auth file (never its tokens). */
|
|
238
|
+
export interface LoginRef {
|
|
239
|
+
provider: string;
|
|
240
|
+
email: string | null;
|
|
241
|
+
file: string;
|
|
242
|
+
}
|
|
233
243
|
export interface ImportResult {
|
|
234
244
|
hub: {
|
|
235
245
|
workspace_id: string;
|
|
@@ -238,7 +248,8 @@ export interface ImportResult {
|
|
|
238
248
|
};
|
|
239
249
|
/**
|
|
240
250
|
* added: accounts this import gave the hub; already: accounts the hub had (its tokens stay); moved_aside: where this
|
|
241
|
-
* computer's auth files went once the hub had them (`<dir>.moved-to-shardflux`).
|
|
251
|
+
* computer's auth files went once the hub had them (`<dir>.moved-to-shardflux`). active: the logins that serve
|
|
252
|
+
* requests; disabled: logins kept in the hub unused (as they were on the computer, or by --use).
|
|
242
253
|
*/
|
|
243
254
|
logins: {
|
|
244
255
|
uploaded: string[];
|
|
@@ -252,6 +263,8 @@ export interface ImportResult {
|
|
|
252
263
|
email: string | null;
|
|
253
264
|
}>;
|
|
254
265
|
moved_aside: string[];
|
|
266
|
+
active: LoginRef[];
|
|
267
|
+
disabled: LoginRef[];
|
|
255
268
|
};
|
|
256
269
|
/** Whether the hub's config was made from this computer's, or the hub kept the one it has. */
|
|
257
270
|
config: 'from_this_computer' | 'kept';
|
|
@@ -271,11 +284,21 @@ export interface ImportResult {
|
|
|
271
284
|
dropped: string[];
|
|
272
285
|
accounts: HubAccount[] | null;
|
|
273
286
|
}
|
|
287
|
+
/** The logins a `--use` or `shard cli-proxy use` value names: by email (any case), path in the auth folder, or file name. */
|
|
288
|
+
export declare function matchLogins<T extends {
|
|
289
|
+
provider: string | null;
|
|
290
|
+
email: string | null;
|
|
291
|
+
file: string;
|
|
292
|
+
}>(spec: string, logins: readonly T[]): T[];
|
|
274
293
|
/**
|
|
275
294
|
* Moves CLIProxyAPI from this computer into the account (or connects this computer to the account's hub): ensure the
|
|
276
295
|
* hub from the user's config, stop the local copy, upload the auth files the hub does not have yet (never overwriting
|
|
277
296
|
* one: the hub's copy may hold a newer refresh token), start the forwarder on 127.0.0.1:8317 and put the hub info into
|
|
278
297
|
* the machine template.
|
|
298
|
+
*
|
|
299
|
+
* Each login arrives as it is on this computer (enabled or disabled), and the hub routes across the enabled ones as
|
|
300
|
+
* the user's config says. `--use <email|file>` (repeatable, one per provider) makes that login the only one serving
|
|
301
|
+
* its provider (the provider's others are kept in the hub, disabled), as `shard cli-proxy use` does.
|
|
279
302
|
*/
|
|
280
303
|
export declare function importCliProxy(ctx: Ctx, opts?: ImportOptions): Promise<ImportResult>;
|
|
281
304
|
export interface ExportOptions {
|
|
@@ -306,6 +329,26 @@ export interface ExportResult {
|
|
|
306
329
|
* Undone by `shard cli-proxy import`.
|
|
307
330
|
*/
|
|
308
331
|
export declare function exportCliProxy(ctx: Ctx, opts?: ExportOptions): Promise<ExportResult>;
|
|
332
|
+
export interface UseResult {
|
|
333
|
+
/** Each provider this command set to one login, with the login that served it alone before (null if not one) and the one now. */
|
|
334
|
+
switched: Array<{
|
|
335
|
+
provider: string;
|
|
336
|
+
from: LoginRef | null;
|
|
337
|
+
to: LoginRef;
|
|
338
|
+
}>;
|
|
339
|
+
/** The logins that serve requests now, and the ones kept in the hub, disabled. */
|
|
340
|
+
active: LoginRef[];
|
|
341
|
+
disabled: LoginRef[];
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* `shard cli-proxy use <email|file>`: that login alone serves its provider (an email with a Claude and a Codex login
|
|
345
|
+
* sets both); the provider's other logins stay in the hub, disabled. `--all`: every login serves again, and the hub
|
|
346
|
+
* routes across them as its config says. Under the hub's logins lock.
|
|
347
|
+
*/
|
|
348
|
+
export declare function useCliProxyLogin(ctx: Ctx, spec: string, opts?: {
|
|
349
|
+
hub?: HubCloud;
|
|
350
|
+
all?: boolean;
|
|
351
|
+
}): Promise<UseResult>;
|
|
309
352
|
/**
|
|
310
353
|
* After the edge refused the hub's callback URL (`callback_invalid`: the API key that created it was revoked):
|
|
311
354
|
* replaces the URLs the edge refuses (under the hub's lock), saves them locally and gives VMs the new VM address.
|