@shardflux/cli 0.16.0 → 0.17.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 +62 -2
- package/README.md +43 -5
- 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 +117 -16
- package/dist/agent-targets.d.ts +2 -0
- package/dist/agent-targets.js +10 -4
- 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/device-approvals.d.ts +12 -0
- package/dist/device-approvals.js +73 -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/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 +29 -6
- package/dist/onboarding.d.ts +2 -0
- package/dist/onboarding.js +53 -5
- package/dist/t3-launch.d.ts +37 -0
- package/dist/t3-launch.js +133 -0
- package/dist/t3-mode.d.ts +121 -0
- package/dist/t3-mode.js +654 -0
- package/dist/t3-phone.d.ts +17 -0
- package/dist/t3-phone.js +15 -0
- 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 +12 -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 +3 -3
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import type { Caps, Shardflux } from '@shardflux/sdk';
|
|
2
|
+
import { CodedError } from './errors.js';
|
|
3
|
+
/** The pinned CLIProxyAPI release and the SHA-256 of its Linux assets (verified in the hub before install). */
|
|
4
|
+
export declare const CLI_PROXY_VERSION = "8.0.23";
|
|
5
|
+
export declare const CLI_PROXY_ASSETS: {
|
|
6
|
+
readonly x86_64: {
|
|
7
|
+
readonly asset: "linux_amd64";
|
|
8
|
+
readonly sha256: "744000a1a4f9ce19f5964774f853d029797b87ef53796d054389258c66cb1224";
|
|
9
|
+
};
|
|
10
|
+
readonly aarch64: {
|
|
11
|
+
readonly asset: "linux_aarch64";
|
|
12
|
+
readonly sha256: "7c54c9306167b3e252090924c39a9b9dd5c09dffd376551271d8902f391f0246";
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export declare const CLI_PROXY_PORT = 8317;
|
|
16
|
+
/**
|
|
17
|
+
* The hub's VM port: `sf-agent cli-proxy-filter` in front of 8317 without the management API. VMs receive only this
|
|
18
|
+
* port's callback URL; 8317's (the management port) stays with the computers that imported the hub.
|
|
19
|
+
*/
|
|
20
|
+
export declare const CLI_PROXY_VM_PORT = 8318;
|
|
21
|
+
/** The hub's workspace key in the API key's project, and its labels. */
|
|
22
|
+
export declare const HUB_KEY = "shardflux/cli-proxy";
|
|
23
|
+
export declare const HUB_LABELS: {
|
|
24
|
+
readonly shard_role: "cli_proxy";
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* `codex-connector`: the smallest fleet template (256 MiB fixed, 1 vCPU, 2 GiB disk) with curl, coreutils, util-linux
|
|
28
|
+
* and CA roots, which is all a single Go binary needs; `coding` would hold 2 GiB and start Docker, SSH and the agent
|
|
29
|
+
* services for nothing.
|
|
30
|
+
*/
|
|
31
|
+
export declare const HUB_TEMPLATE = "codex-connector";
|
|
32
|
+
/**
|
|
33
|
+
* The codex-connector version a hub runs at least: the first with the hub's boot service and an sf-agent that serves
|
|
34
|
+
* the VM port (cli-proxy-filter). New hubs are created on it; an older hub is upgraded in place (its home, and so its
|
|
35
|
+
* logins and settings, stay) before VMs get the hub's address.
|
|
36
|
+
*/
|
|
37
|
+
export declare const HUB_TEMPLATE_VERSION = 4;
|
|
38
|
+
export declare const HUB_CAPS: Caps;
|
|
39
|
+
export declare const HUB_INFO_FILE = "cli-proxy.json";
|
|
40
|
+
/** Paths in the hub workspace. */
|
|
41
|
+
export declare const HUB_DIR = "/home/user/.cli-proxy-hub";
|
|
42
|
+
export declare const HUB_AUTH_DIR = "/home/user/.cli-proxy-api";
|
|
43
|
+
export interface HubInfo {
|
|
44
|
+
version: 1;
|
|
45
|
+
api_url: string;
|
|
46
|
+
workspace_id: string;
|
|
47
|
+
/** `https://8317-<handle>.<domain>/__shardflux/callback/sfcb_…/`: a request to `<callback_url><rest>` reaches `/<rest>`. */
|
|
48
|
+
callback_url: string;
|
|
49
|
+
/** When the port's callback URL was created: a different value on the port means this one was replaced. */
|
|
50
|
+
callback_created_at: string;
|
|
51
|
+
/** The hub's MANAGEMENT_PASSWORD (Authorization: Bearer on /v0/management/*). */
|
|
52
|
+
management_key: string;
|
|
53
|
+
/** The VM port's (8318) callback URL: what VMs receive. Absent while the hub does not serve that port. */
|
|
54
|
+
vm_callback_url?: string;
|
|
55
|
+
vm_callback_created_at?: string;
|
|
56
|
+
}
|
|
57
|
+
/** The hub info in `text`, or null when it is not a valid one (an unreadable file is treated as absent). */
|
|
58
|
+
export declare function parseHubInfo(text: string): HubInfo | null;
|
|
59
|
+
export declare function hubInfoPath(dir: string): string;
|
|
60
|
+
/** The saved hub info for `apiUrl` (any API URL when omitted), or null. */
|
|
61
|
+
export declare function readHubInfo(dir: string, apiUrl?: string): HubInfo | null;
|
|
62
|
+
export declare function hubInfoText(info: HubInfo): string;
|
|
63
|
+
/**
|
|
64
|
+
* What every VM receives (in the machine template): the VM port's (8318) callback URL, which serves CLIProxyAPI
|
|
65
|
+
* without its management API, and nothing else secret. The management key and the management port's URL stay on the
|
|
66
|
+
* computers that imported the hub, so no agent in a VM reaches the hub's auth files, whatever key it finds. Null
|
|
67
|
+
* while the hub does not serve the VM port.
|
|
68
|
+
*/
|
|
69
|
+
export declare function vmHubInfoText(info: HubInfo): string | null;
|
|
70
|
+
export declare function writeHubInfo(dir: string, info: HubInfo): void;
|
|
71
|
+
export interface HubConfig {
|
|
72
|
+
text: string;
|
|
73
|
+
/** Settings of the user's config that only work on their own computer, left out of the hub's copy. */
|
|
74
|
+
dropped: string[];
|
|
75
|
+
/** The user's own management key (its bcrypt hash) is kept, so tools that use it keep working. */
|
|
76
|
+
keepsUserManagementKey: boolean;
|
|
77
|
+
}
|
|
78
|
+
/** A proxy URL that points at the user's own computer or network (unreachable from the hub). */
|
|
79
|
+
export declare function isLocalProxyUrl(value: unknown): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* The hub's config: the user's own CLIProxyAPI config (routing, retries, client API keys, model aliases, upstream keys,
|
|
82
|
+
* payload rules…), so the hosted copy answers exactly like the local one, with what only works on their computer
|
|
83
|
+
* replaced: listen address, TLS, auth directory, management (remote, the hub key in MANAGEMENT_PASSWORD, the user's own
|
|
84
|
+
* key kept, control panel off), plugins, pprof, a proxy-url on their network, and logging (rotating files in the hub,
|
|
85
|
+
* 100 MB at most, no request bodies). The file keeps its layout: the legacy flat one or `config-version: 8`.
|
|
86
|
+
*/
|
|
87
|
+
export declare function hubConfig(userText: string | null): HubConfig;
|
|
88
|
+
/** What ensuring the hub needs from its workspace (the SDK in production, a stand-in in tests). */
|
|
89
|
+
export interface HubWorkspace {
|
|
90
|
+
readonly id: string;
|
|
91
|
+
/** bash -c `script` as the workspace user; argv after the script are $1…. */
|
|
92
|
+
sh(script: string, args?: string[], opts?: {
|
|
93
|
+
stdin?: string;
|
|
94
|
+
timeoutMs?: number;
|
|
95
|
+
}): Promise<{
|
|
96
|
+
code: number | null;
|
|
97
|
+
stdout: string;
|
|
98
|
+
stderr: string;
|
|
99
|
+
}>;
|
|
100
|
+
/** File content, or null when it does not exist. */
|
|
101
|
+
read(path: string): Promise<string | null>;
|
|
102
|
+
write(path: string, data: string, mode: '0600' | '0700'): Promise<void>;
|
|
103
|
+
/** Names in a directory, or null when it does not exist. */
|
|
104
|
+
list(dir: string): Promise<string[] | null>;
|
|
105
|
+
/** The hub's auth files, as paths relative to its auth directory (subdirectories included, as CLIProxyAPI reads them). */
|
|
106
|
+
authFiles(): Promise<string[]>;
|
|
107
|
+
/** Exposes the port (idempotent): when its callback URL was created, null without one. */
|
|
108
|
+
exposePort(port: number): Promise<{
|
|
109
|
+
callbackCreatedAt: string | null;
|
|
110
|
+
}>;
|
|
111
|
+
createCallbackUrl(port: number): Promise<{
|
|
112
|
+
url: string;
|
|
113
|
+
createdAt: string;
|
|
114
|
+
}>;
|
|
115
|
+
/**
|
|
116
|
+
* Runs `decide` while the hub's callback lock is held (one device at a time): it gets the stored callback, or null,
|
|
117
|
+
* and returns the one to keep; a different one is stored in the hub before the lock is released.
|
|
118
|
+
*/
|
|
119
|
+
withCallbackLock(decide: (stored: StoredCallback | null) => Promise<StoredCallback>, file?: string): Promise<StoredCallback>;
|
|
120
|
+
/** Runs `fn` while the hub-side lock `name` is held (one device at a time). */
|
|
121
|
+
withLock<T>(name: string, fn: () => Promise<T>): Promise<T>;
|
|
122
|
+
}
|
|
123
|
+
export interface HubCloud {
|
|
124
|
+
/** The hub workspace, running (resumed or woken as needed); with `create`, created on first use. Null when missing. */
|
|
125
|
+
open(create: boolean): Promise<{
|
|
126
|
+
ws: HubWorkspace;
|
|
127
|
+
created: boolean;
|
|
128
|
+
template?: HubTemplate;
|
|
129
|
+
} | null>;
|
|
130
|
+
/** Upgrades the hub to its template's newest version now (files and home stay; processes start again). */
|
|
131
|
+
upgrade?: (id: string) => Promise<void>;
|
|
132
|
+
/** The workspace's observed state (`running`, `suspended`, …), or null when it no longer exists. Never wakes it. */
|
|
133
|
+
state(id: string): Promise<string | null>;
|
|
134
|
+
/** Suspends the hub workspace (it wakes on the next request or open). */
|
|
135
|
+
suspend(id: string): Promise<void>;
|
|
136
|
+
/** Whether the edge still accepts a stored callback URL (probeCallback); without it a stored URL counts as valid. */
|
|
137
|
+
probe?: (url: string) => Promise<CallbackProbe>;
|
|
138
|
+
}
|
|
139
|
+
/** The template version a hub runs, and the newer one it can be upgraded to (null when none). */
|
|
140
|
+
export interface HubTemplate {
|
|
141
|
+
version: number | null;
|
|
142
|
+
upgradeTo: number | null;
|
|
143
|
+
}
|
|
144
|
+
/** A project of the account (the hub's home is the organization's first one). */
|
|
145
|
+
export interface HomeProject {
|
|
146
|
+
id: string;
|
|
147
|
+
slug: string;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Where a new hub may be created: only in the organization's first project (its oldest), so every project's key finds
|
|
151
|
+
* the same one hub. `home` is null without a session to list the projects with.
|
|
152
|
+
*/
|
|
153
|
+
export declare function hubPlacement(keyProjectId: string, home: HomeProject | null): void;
|
|
154
|
+
export declare function sdkHubCloud(cloud: Shardflux, opts?: {
|
|
155
|
+
signal?: AbortSignal;
|
|
156
|
+
wakeTimeoutMs?: number;
|
|
157
|
+
agentLabel?: string;
|
|
158
|
+
homeProject?: (organizationId: string) => Promise<HomeProject | null>;
|
|
159
|
+
}): HubCloud;
|
|
160
|
+
/** Installs the pinned release once: download, verify its SHA-256, then move the binary into place. */
|
|
161
|
+
export declare const INSTALL_SCRIPT: string;
|
|
162
|
+
/**
|
|
163
|
+
* The supervisor (setsid, its own process group): runs CLIProxyAPI in the foreground and starts it again 2 s after it
|
|
164
|
+
* exits. The management key is read from its 0600 file into MANAGEMENT_PASSWORD (never in the config or argv); logs go
|
|
165
|
+
* to rotating files under the hub directory (WRITABLE_PATH). First it ends whatever an earlier supervisor that died
|
|
166
|
+
* alone left running, which would hold 8317 and 8318.
|
|
167
|
+
*/
|
|
168
|
+
export declare const RUN_SCRIPT = "#!/bin/bash\nset -u\ndir=${CLI_PROXY_HUB_DIR:-/home/user/.cli-proxy-hub}\nclear_stale() {\n local p i a found=() left=()\n for p in /proc/[0-9]*; do\n p=${p#/proc/}\n [ \"$p\" != \"$$\" ] && [ -O \"/proc/$p\" ] || continue\n a=(); mapfile -d '' -t a 2>/dev/null < \"/proc/$p/cmdline\" || continue\n [ \"${#a[@]}\" -gt 0 ] || continue\n if [[ \"${a[0]}\" == \"$dir/bin/cli-proxy-api\"* ]] || { [[ \"${a[0]}\" == */bash || \"${a[0]}\" == bash ]] && [ \"${a[1]-}\" = \"$dir/run.sh\" ]; }; then\n found+=(\"$p\"); continue\n fi\n if [[ \"${a[0]}\" == *sf-agent ]]; then\n for ((i = 1; i + 1 < ${#a[@]}; i++)); do\n if [ \"${a[i]}\" = cli-proxy-filter ] && [ \"${a[i+1]}\" = --listen ]; then found+=(\"$p\"); break; fi\n done\n fi\n done\n [ \"${#found[@]}\" -gt 0 ] || return 0\n kill -TERM \"${found[@]}\" 2>/dev/null || true\n for ((i = 0; i < 50; i++)); do\n left=()\n for p in \"${found[@]}\"; do\n a=(); mapfile -d '' -t a 2>/dev/null < \"/proc/$p/cmdline\" || continue\n [ \"${#a[@]}\" -gt 0 ] && left+=(\"$p\")\n done\n [ \"${#left[@]}\" -gt 0 ] || return 0\n sleep 0.1\n done\n kill -KILL \"${left[@]}\" 2>/dev/null || true\n return 0\n}\nclear_stale\necho $$ > \"$dir/run.pid\"\ncd \"$dir\"\n# Its own session: stopping it ends the whole group (CLIProxyAPI and the VM port).\ntrap 'trap - TERM INT; kill -TERM 0 2>/dev/null; exit 0' TERM INT\n# The VM port (0.0.0.0:8318): CLIProxyAPI without its management API, served by sf-agent (the\n# template's, else one placed in the hub's bin).\n(\n while :; do\n filter=\n for b in /usr/local/bin/sf-agent \"$dir/bin/sf-agent\"; do\n if [ -x \"$b\" ] && \"$b\" cli-proxy-filter --probe >/dev/null 2>&1; then filter=$b; break; fi\n done\n if [ -z \"$filter\" ]; then sleep 300; continue; fi\n \"$filter\" cli-proxy-filter --listen 0.0.0.0:8318 --upstream 127.0.0.1:8317\n echo \"$(date -u +%FT%TZ) the VM port exited with status $?; starting it again in 2 s\"\n sleep 2\n done\n) &\nwhile :; do\n MANAGEMENT_PASSWORD=\"$(cat \"$dir/management.key\")\" WRITABLE_PATH=\"$dir\" \"$dir/bin/cli-proxy-api-8.0.23\" -config \"$dir/config.yaml\" &\n child=$!\n wait \"$child\"; code=$?\n echo \"$(date -u +%FT%TZ) cli-proxy-api exited with status $code; starting it again in 2 s\"\n sleep 2\ndone\n";
|
|
169
|
+
/**
|
|
170
|
+
* Starts the supervisor unless it runs and the hub answers /healthz (serialized by a lock in the hub, so two devices
|
|
171
|
+
* ensuring at once never start two copies). $1=1 restarts it (a new binary, run script or management key).
|
|
172
|
+
* Prints `running`, `started` or `restarted`.
|
|
173
|
+
*
|
|
174
|
+
* The codex-connector template's `cli-proxy-hub` service (images/templates/codex-connector/start-cli-proxy-hub) starts
|
|
175
|
+
* the same run.sh at boot under the same lock and run.pid rules; a template without it (version 3 and earlier) relies
|
|
176
|
+
* on this script alone. run.pid counts only while that pid's command line is run.sh: after a cold boot the old pid
|
|
177
|
+
* may belong to any process, which must be neither waited for nor signalled.
|
|
178
|
+
*/
|
|
179
|
+
export declare const START_SCRIPT = "set -euo pipefail\ndir=/home/user/.cli-proxy-hub; restart=${1:-0}\nexec 9>\"$dir/start.lock\"; flock -w 90 9 || { echo \"another start of CLIProxyAPI is still running in the hub\" >&2; exit 6; }\nhealthy() { curl -fsS -m 2 -o /dev/null http://127.0.0.1:8317/healthz 2>/dev/null; }\n# The second line says whether the VM port answers (it starts a moment after CLIProxyAPI).\nvmport() { for _ in $(seq 25); do if curl -fsS -m 1 -o /dev/null http://127.0.0.1:8318/healthz 2>/dev/null; then echo \"vm-port yes\"; return; fi; sleep 0.2; done; echo \"vm-port no\"; }\nalive() {\n [ -s \"$dir/run.pid\" ] || return 1\n pid=$(cat \"$dir/run.pid\")\n [[ \"$pid\" =~ ^[0-9]+$ ]] || return 1\n cmd=$(tr '\\0' ' ' 2>/dev/null < \"/proc/$pid/cmdline\") || return 1\n [[ \"$cmd\" == *\"$dir/run.sh\"* ]]\n}\nstop() {\n if alive; then\n pid=$(cat \"$dir/run.pid\"); kill -TERM -- \"-$pid\" 2>/dev/null || kill -TERM \"$pid\" 2>/dev/null || true\n for _ in $(seq 50); do alive || break; sleep 0.1; done\n if alive; then kill -KILL -- \"-$pid\" 2>/dev/null || true; fi\n fi\n rm -f \"$dir/run.pid\"\n}\nstate=started\nif [ \"$restart\" = 1 ] && alive; then stop; state=restarted; fi\nif alive; then\n for _ in $(seq 100); do if healthy; then echo running; vmport; exit 0; fi; sleep 0.2; done\n stop; state=restarted\nfi\nif [ -f \"$dir/supervisor.log\" ] && [ \"$(stat -c %s \"$dir/supervisor.log\")\" -gt 1048576 ]; then : > \"$dir/supervisor.log\"; fi\n# The supervisor must not inherit the lock (fd 9), or the next start would wait for it forever.\nsetsid \"$dir/run.sh\" 9>&- >>\"$dir/supervisor.log\" 2>&1 </dev/null &\nfor _ in $(seq 150); do if healthy; then echo \"$state\"; vmport; exit 0; fi; sleep 0.2; done\necho \"CLIProxyAPI did not answer on port 8317 within 30 s\" >&2\ntail -n 5 \"$dir/supervisor.log\" >&2 2>/dev/null || true\nexit 5\n";
|
|
180
|
+
/**
|
|
181
|
+
* Stops CLIProxyAPI in the hub for `shard cli-proxy export` (under the start lock): the supervisor and anything it
|
|
182
|
+
* started end (CLIProxyAPI and the VM port, also when an earlier supervisor left them running), run.sh is set aside
|
|
183
|
+
* (the boot service stays idle) and `exported` marks the hub, so no forwarder starts it again until the next import.
|
|
184
|
+
* Prints `stopped`.
|
|
185
|
+
*/
|
|
186
|
+
export declare const STOP_SCRIPT = "set -euo pipefail\ndir=${CLI_PROXY_HUB_DIR:-/home/user/.cli-proxy-hub}\nclear_stale() {\n local p i a found=() left=()\n for p in /proc/[0-9]*; do\n p=${p#/proc/}\n [ \"$p\" != \"$$\" ] && [ -O \"/proc/$p\" ] || continue\n a=(); mapfile -d '' -t a 2>/dev/null < \"/proc/$p/cmdline\" || continue\n [ \"${#a[@]}\" -gt 0 ] || continue\n if [[ \"${a[0]}\" == \"$dir/bin/cli-proxy-api\"* ]] || { [[ \"${a[0]}\" == */bash || \"${a[0]}\" == bash ]] && [ \"${a[1]-}\" = \"$dir/run.sh\" ]; }; then\n found+=(\"$p\"); continue\n fi\n if [[ \"${a[0]}\" == *sf-agent ]]; then\n for ((i = 1; i + 1 < ${#a[@]}; i++)); do\n if [ \"${a[i]}\" = cli-proxy-filter ] && [ \"${a[i+1]}\" = --listen ]; then found+=(\"$p\"); break; fi\n done\n fi\n done\n [ \"${#found[@]}\" -gt 0 ] || return 0\n kill -TERM \"${found[@]}\" 2>/dev/null || true\n for ((i = 0; i < 50; i++)); do\n left=()\n for p in \"${found[@]}\"; do\n a=(); mapfile -d '' -t a 2>/dev/null < \"/proc/$p/cmdline\" || continue\n [ \"${#a[@]}\" -gt 0 ] && left+=(\"$p\")\n done\n [ \"${#left[@]}\" -gt 0 ] || return 0\n sleep 0.1\n done\n kill -KILL \"${left[@]}\" 2>/dev/null || true\n return 0\n}\nexec 9>\"$dir/start.lock\"; flock -w 90 9 || { echo \"a start of CLIProxyAPI is still running in the hub\" >&2; exit 6; }\nalive() {\n [ -s \"$dir/run.pid\" ] || return 1\n pid=$(cat \"$dir/run.pid\")\n [[ \"$pid\" =~ ^[0-9]+$ ]] || return 1\n cmd=$(tr '\\0' ' ' 2>/dev/null < \"/proc/$pid/cmdline\") || return 1\n [[ \"$cmd\" == *\"$dir/run.sh\"* ]]\n}\ndate -u +%FT%TZ > \"$dir/exported\"\nif [ -f \"$dir/run.sh\" ]; then mv \"$dir/run.sh\" \"$dir/run.sh.exported\"; fi\nif alive; then\n kill -TERM -- \"-$pid\" 2>/dev/null || true\n for _ in $(seq 50); do alive || break; sleep 0.1; done\n if alive; then kill -KILL -- \"-$pid\" 2>/dev/null || true; fi\nfi\nrm -f \"$dir/run.pid\"\nclear_stale\nfor _ in $(seq 50); do\n if ! curl -fsS -m 1 -o /dev/null http://127.0.0.1:8317/healthz 2>/dev/null && ! curl -fsS -m 1 -o /dev/null http://127.0.0.1:8318/healthz 2>/dev/null; then echo stopped; exit 0; fi\n sleep 0.1\ndone\necho \"CLIProxyAPI still answers in the hub\" >&2\nexit 5\n";
|
|
187
|
+
/**
|
|
188
|
+
* The hub's management key, created once: under key.lock, a missing or malformed management.key becomes the key on
|
|
189
|
+
* standard input (0600, atomic). Prints `created` when it wrote one, then the key in use.
|
|
190
|
+
*/
|
|
191
|
+
export declare const KEY_SCRIPT = "set -euo pipefail\ndir=/home/user/.cli-proxy-hub; mkdir -p \"$dir\"\nexec 7>\"$dir/key.lock\"; flock -w 60 7 || { echo \"another device is setting up the hub's key\" >&2; exit 6; }\nIFS= read -r proposed || true\nif ! grep -qxE '[0-9a-f]{64}' \"$dir/management.key\" 2>/dev/null; then\n [[ \"$proposed\" =~ ^[0-9a-f]{64}$ ]] || { echo \"no valid key was proposed\" >&2; exit 7; }\n umask 077; printf '%s\\n' \"$proposed\" > \"$dir/management.key.tmp\"; mv \"$dir/management.key.tmp\" \"$dir/management.key\"\n echo created\nfi\ncat \"$dir/management.key\"\n";
|
|
192
|
+
/**
|
|
193
|
+
* Adds one auth file to the hub only when that path is free (a hard link cannot replace a file), so two devices
|
|
194
|
+
* importing at once never overwrite each other's login or the hub's newer tokens. $1 is the path relative to the auth
|
|
195
|
+
* directory, the content comes on standard input. Prints `added` or `exists`.
|
|
196
|
+
*/
|
|
197
|
+
export declare const ADD_AUTH_SCRIPT = "set -euo pipefail\na=/home/user/.cli-proxy-api; rel=$1\ncase \"$rel\" in ''|/*|*..*) echo \"not a path inside the auth folder\" >&2; exit 2 ;; esac\nmkdir -p -m 0700 \"$(dirname \"$a/$rel\")\"\ntmp=$(mktemp \"$a/.adding.XXXXXX\"); chmod 0600 \"$tmp\"; cat > \"$tmp\"\nif ln \"$tmp\" \"$a/$rel\" 2>/dev/null; then echo added; else echo exists; fi\nrm -f \"$tmp\"\n";
|
|
198
|
+
/** Sets the hub's logins aside after an export copied them out (the hub holds none until the next import). */
|
|
199
|
+
export declare const SET_ASIDE_SCRIPT = "set -euo pipefail\na=/home/user/.cli-proxy-api; [ -d \"$a\" ] || exit 0\nto=\"$a.exported-$(date -u +%Y%m%dT%H%M%SZ)\"\nmv \"$a\" \"$to\"; mkdir -m 0700 \"$a\"; chmod 0700 \"$to\"\necho \"$to\"\n";
|
|
200
|
+
export interface EnsureOptions {
|
|
201
|
+
/** The local config directory (hub info file). */
|
|
202
|
+
dir: string;
|
|
203
|
+
apiUrl: string;
|
|
204
|
+
/**
|
|
205
|
+
* The user's own CLIProxyAPI config (import): the hub's config is made from it when the hub has none yet, or only
|
|
206
|
+
* the defaults. A hub already configured from a user config keeps it.
|
|
207
|
+
*/
|
|
208
|
+
userConfig?: string | null;
|
|
209
|
+
/** Only start the existing hub again (the forwarder): never create a workspace or replace the callback URL. */
|
|
210
|
+
repairOnly?: boolean;
|
|
211
|
+
/**
|
|
212
|
+
* With repairOnly: replace a callback URL the edge no longer accepts (`callback_invalid`: the API key that created it
|
|
213
|
+
* was revoked), which a probe of the stored URL shows. An import always probes before it reuses a stored URL.
|
|
214
|
+
*/
|
|
215
|
+
renewInvalid?: boolean;
|
|
216
|
+
/** The probe of a stored callback URL (the cloud's when omitted). */
|
|
217
|
+
probe?: (url: string) => Promise<CallbackProbe>;
|
|
218
|
+
progress?: (message: string) => void;
|
|
219
|
+
/** Random management key (tests). */
|
|
220
|
+
randomKey?: () => string;
|
|
221
|
+
}
|
|
222
|
+
export interface EnsureResult {
|
|
223
|
+
info: HubInfo;
|
|
224
|
+
created: boolean;
|
|
225
|
+
/** `running` (nothing to do), `started` or `restarted`. */
|
|
226
|
+
process: 'running' | 'started' | 'restarted';
|
|
227
|
+
callbackReplaced: boolean;
|
|
228
|
+
/** The config written now; null when the hub kept its own. */
|
|
229
|
+
config: HubConfig | null;
|
|
230
|
+
}
|
|
231
|
+
/** The hub could not be made ready (exit 1, code `cli_proxy_hub`). */
|
|
232
|
+
export declare class HubError extends CodedError {
|
|
233
|
+
name: string;
|
|
234
|
+
constructor(message: string, hint?: string);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Makes sure the account's hub exists, runs the pinned CLIProxyAPI on port 8317 and has a callback URL, and saves the
|
|
238
|
+
* hub info locally (0600) and in the hub. Idempotent: a running, healthy hub with a valid callback URL is left as it is.
|
|
239
|
+
*/
|
|
240
|
+
export declare function ensureCliProxyHub(cloud: Shardflux | HubCloud, opts: EnsureOptions): Promise<EnsureResult>;
|
|
241
|
+
/** The callback URL as the hub stores it (`callback.json`, 0600): its secret is shown only when it is created. */
|
|
242
|
+
export interface StoredCallback {
|
|
243
|
+
callback_url: string;
|
|
244
|
+
callback_created_at: string;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The hub's one callback URL. Under the hub's callback lock: the stored one is kept while the port's callback still
|
|
248
|
+
* has its creation time; otherwise one is created and stored before the lock is released. Every device and every
|
|
249
|
+
* import reads the stored one, so concurrent imports never replace each other's URL (and the VMs' copy).
|
|
250
|
+
*/
|
|
251
|
+
export declare function hubCallback(ws: HubWorkspace, repairOnly?: boolean, portNumber?: number, probe?: (url: string) => Promise<CallbackProbe>): Promise<{
|
|
252
|
+
callback: StoredCallback;
|
|
253
|
+
replaced: boolean;
|
|
254
|
+
}>;
|
|
255
|
+
export type CallbackProbe = 'ok' | 'invalid' | 'unknown';
|
|
256
|
+
/**
|
|
257
|
+
* Whether the edge still accepts a callback URL: `invalid` only for its own 401 `callback_invalid` refusal; any other
|
|
258
|
+
* answer (CLIProxyAPI's, a waking or starting hub) means the URL itself works. `unknown` when nothing answered.
|
|
259
|
+
*/
|
|
260
|
+
export declare function probeCallback(url: string, timeoutMs?: number): Promise<CallbackProbe>;
|
|
261
|
+
/** Where the hub stores a port's callback URL: callback.json (8317, management), callback-vm.json (8318, VMs). */
|
|
262
|
+
export declare function callbackFile(port: number): string;
|
|
263
|
+
/**
|
|
264
|
+
* Holds the hub's callback lock (flock on callback.lock, in the hub) while the client decides: prints the stored
|
|
265
|
+
* callback (base64 JSON, or -), then reads one line: KEEP, ABORT, or NEW <base64 JSON>, which it stores atomically.
|
|
266
|
+
*/
|
|
267
|
+
export declare const CALLBACK_SCRIPT = "set -euo pipefail\ndir=/home/user/.cli-proxy-hub; mkdir -p \"$dir\"; f=${1:-callback.json}\ncase \"$f\" in callback.json|callback-vm.json) ;; *) echo \"unknown callback file\" >&2; exit 2 ;; esac\nexec 8>\"$dir/callback.lock\"\nflock -w 120 8 || { echo \"another device is setting up the hub's callback URL\" >&2; exit 6; }\nif [ -s \"$dir/$f\" ]; then printf 'STORED %s\\n' \"$(base64 -w0 < \"$dir/$f\")\"; else echo 'STORED -'; fi\nIFS= read -r -t 150 reply || { echo \"no answer from the device\" >&2; exit 7; }\ncase \"$reply\" in\n KEEP|ABORT) exit 0 ;;\n NEW\\ *) umask 077; printf '%s' \"${reply#NEW }\" | base64 -d > \"$dir/$f.tmp\"; mv \"$dir/$f.tmp\" \"$dir/$f\"; echo SAVED ;;\n *) echo \"unexpected answer\" >&2; exit 8 ;;\nesac\n";
|
|
268
|
+
/**
|
|
269
|
+
* Holds a hub-side lock (flock on <name>.lock) while the device works: prints LOCKED, then waits for a line (or the
|
|
270
|
+
* end of standard input) and releases it.
|
|
271
|
+
*/
|
|
272
|
+
export declare const LOCK_SCRIPT = "set -euo pipefail\ndir=/home/user/.cli-proxy-hub; mkdir -p \"$dir\"; name=${1:?}\n[[ \"$name\" =~ ^[a-z-]+$ ]] || { echo \"bad lock name\" >&2; exit 2; }\nexec 8>\"$dir/$name.lock\"\nflock -w 120 8 || { echo \"another device holds the hub's $name lock\" >&2; exit 6; }\necho LOCKED\nIFS= read -r -t 600 _ || true\necho RELEASED\n";
|
|
273
|
+
export declare function parseStoredCallback(text: string): StoredCallback | null;
|