@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/device-helper.js
CHANGED
|
@@ -4,16 +4,17 @@ import { ConversationMirrorState, mirrorProjectConversations } from "./conversat
|
|
|
4
4
|
import { FilesMirrorState, mirrorFilesOnce } from "./files-mirror.js";
|
|
5
5
|
import { withTargetLock } from "./target-lock.js";
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
-
import { conversationsUnavailable } from "./conversations.js";
|
|
8
|
-
import { localConversations } from "./conversations.js";
|
|
7
|
+
import { completeDeferredPreparation, conversationsUnavailable, localConversations } from "./conversations.js";
|
|
9
8
|
import { randomUUID } from 'node:crypto';
|
|
10
9
|
import { readFileSync } from 'node:fs';
|
|
11
10
|
import { writePrivateFile } from "./credentials.js";
|
|
12
11
|
import { conversationClient } from "./conversation-commands.js";
|
|
13
12
|
import { Readable } from 'node:stream';
|
|
14
13
|
import { runWorktreeTwin } from "./worktree-twin-engine.js";
|
|
15
|
-
import {
|
|
14
|
+
import { AgentConfigReturnState, runAgentConfigLoop } from "./agent-config-return.js";
|
|
16
15
|
import { askLocalDeviceApproval } from "./device-approvals.js";
|
|
16
|
+
import { resumeCloudStartup, startCarried } from "./codex-cloud.js";
|
|
17
|
+
import { runProjectPreparation } from "./project-preparation.js";
|
|
17
18
|
/** The menu bar owns this helper and its stdin; VM data can request actions but cannot answer approvals. */
|
|
18
19
|
export async function runDeviceHelper(ctx, options = {}) {
|
|
19
20
|
const abort = new AbortController(), signal = AbortSignal.any([ctx.signal, abort.signal]);
|
|
@@ -68,33 +69,51 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
68
69
|
}
|
|
69
70
|
const executor = new DeviceExecutor(ctx, ask, deviceId);
|
|
70
71
|
const report = (e) => { ctx.io.stderr.write(`Device bridge: ${e instanceof Error ? e.message : String(e)}\n`); };
|
|
71
|
-
let mirroring, twin, returning;
|
|
72
|
+
let mirroring, twin, returning, preparing, projects;
|
|
72
73
|
try {
|
|
73
74
|
return await withTargetLock(join(ctx.store.dir, 'device-helper-lock'), async () => {
|
|
74
75
|
// Finished conversations land in their local branches (worktree-twin-engine.ts); its own loop and backoff.
|
|
75
76
|
twin = runWorktreeTwin(ctx, deviceId, signal).catch(report);
|
|
76
77
|
// Skills, memories and CLAUDE.md lines saved in the cloud come back on their own cadence (agent-config-return.ts):
|
|
77
|
-
// never behind a files-mirror pass, which takes minutes for an account with hundreds of conversations.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
// never behind a files-mirror pass, which takes minutes for an account with hundreds of conversations. Every 2 s
|
|
79
|
+
// while one of this user's conversations is active, backing off to 60 s when none is.
|
|
80
|
+
returning = runAgentConfigLoop(ctx, config, signal).catch(report);
|
|
81
|
+
projects = runProjectPreparation(ctx).catch(report);
|
|
82
|
+
// Starts outlive the foreground command (0.22.0+): an accepted Codex Cloud start resumes with its own revision
|
|
83
|
+
// and idempotent admission and message; large media that followed a first prompt land while the VM is awake.
|
|
84
|
+
preparing = (async () => {
|
|
85
|
+
const failures = new Map();
|
|
81
86
|
while (!signal.aborted) {
|
|
82
|
-
let
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
let deferred = false;
|
|
88
|
+
for (const local of Object.values(localConversations(ctx))) {
|
|
89
|
+
if (local.conversation.ended_at || (failures.get(local.conversation.id) ?? 0) > Date.now())
|
|
90
|
+
continue;
|
|
91
|
+
try {
|
|
92
|
+
// A start a foreground command still carries is its own: resumed only once nobody does.
|
|
93
|
+
if (local.cloud_start && local.cloud_start.state !== 'active' && !local.cloud_start.attachments_pending) {
|
|
94
|
+
if (!startCarried(local.cloud_start))
|
|
95
|
+
await resumeCloudStartup(ctx, local.conversation);
|
|
96
|
+
}
|
|
97
|
+
else if (local.preparation?.state === 'deferred') {
|
|
98
|
+
// A parked VM is never woken for this: its media land the next time it is awake.
|
|
99
|
+
if (await completeDeferredPreparation(ctx, local.conversation))
|
|
100
|
+
failures.delete(local.conversation.id);
|
|
101
|
+
else {
|
|
102
|
+
deferred = true;
|
|
103
|
+
failures.set(local.conversation.id, Date.now() + 15_000);
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
failures.delete(local.conversation.id);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (signal.aborted)
|
|
111
|
+
break;
|
|
112
|
+
report(error);
|
|
113
|
+
failures.set(local.conversation.id, Date.now() + 60_000);
|
|
95
114
|
}
|
|
96
115
|
}
|
|
97
|
-
await delay(
|
|
116
|
+
await delay(deferred ? 5_000 : 30_000, undefined, { signal }).catch(() => undefined);
|
|
98
117
|
}
|
|
99
118
|
})();
|
|
100
119
|
// A slow or failed transcript copy must not delay heartbeats, request polling or approvals.
|
|
@@ -161,6 +180,8 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
161
180
|
await returning;
|
|
162
181
|
await config.folding;
|
|
163
182
|
await twin;
|
|
183
|
+
await preparing;
|
|
184
|
+
await projects;
|
|
164
185
|
await input.catch(() => undefined);
|
|
165
186
|
}
|
|
166
187
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckoutStatus, ExecSession, ExecutionResult } from '@shardflux/sdk';
|
|
1
|
+
import type { RateLimitInfo, CheckoutStatus, ExecSession, ExecutionResult } from '@shardflux/sdk';
|
|
2
2
|
export declare const EXIT: {
|
|
3
3
|
readonly ok: 0;
|
|
4
4
|
/** Any other failure: API refusal (conflict, quota, ...), network, protocol. */
|
|
@@ -58,9 +58,14 @@ export declare class TermsNotAcceptedError extends CodedError {
|
|
|
58
58
|
export declare class AuthConfigError extends HintedError {
|
|
59
59
|
name: string;
|
|
60
60
|
}
|
|
61
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* A workspace key that no workspace of this project has (or another name that resolves to nothing). `details` keeps the
|
|
63
|
+
* API's reason when shard rewords its 404 (0.20.1+: `filesystem_not_found` from shard template delete).
|
|
64
|
+
*/
|
|
62
65
|
export declare class NotFoundError extends HintedError {
|
|
63
66
|
name: string;
|
|
67
|
+
readonly details: Record<string, unknown> | undefined;
|
|
68
|
+
constructor(message: string, hint?: string, details?: Record<string, unknown>);
|
|
64
69
|
}
|
|
65
70
|
/**
|
|
66
71
|
* `shard machine setup --from-this-computer` (0.20.0+) ran but did not save a machine: the agent failed, ran past its
|
|
@@ -140,6 +145,7 @@ export interface ErrorInfo {
|
|
|
140
145
|
execution_id?: string;
|
|
141
146
|
/** The refusal's Retry-After in seconds (0.9.0+), e.g. 429 quota_exceeded from the cell gateway. */
|
|
142
147
|
retry_after_seconds?: number;
|
|
148
|
+
rate_limit?: RateLimitInfo;
|
|
143
149
|
}
|
|
144
150
|
/**
|
|
145
151
|
* Removes anything shaped like a Shardflux API key or user session token from text, and each of the given secrets
|
|
@@ -147,6 +153,27 @@ export interface ErrorInfo {
|
|
|
147
153
|
*/
|
|
148
154
|
export declare function redact(text: string, ...secrets: (string | null | undefined)[]): string;
|
|
149
155
|
/** The `hint:` a CLI error carries itself, if any. */
|
|
156
|
+
/**
|
|
157
|
+
* After a save that failed (0.20.2+): the version in use and the source VM are as they were, so the same save can run
|
|
158
|
+
* again; the build's log says what happened.
|
|
159
|
+
*/
|
|
160
|
+
export declare function saveRecovery(level: 'machine' | 'template', inUse: number, buildId?: string, failure?: SaveFailure): string;
|
|
161
|
+
/** What a failed save reports about its build (SDK 0.26.0+ on an API that carries them). */
|
|
162
|
+
export interface SaveFailure {
|
|
163
|
+
code?: string | undefined;
|
|
164
|
+
retryable?: boolean | undefined;
|
|
165
|
+
details?: Record<string, unknown> | undefined;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* A save larger than the plan's workspace disk (0.21.1+): resource_limit with details.limit template_org_bytes_max;
|
|
169
|
+
* `limit` is the plan's workspace disk and `actual` the version's size, in bytes.
|
|
170
|
+
*/
|
|
171
|
+
export declare function planDiskLimit(failure: SaveFailure | undefined): {
|
|
172
|
+
limit: number;
|
|
173
|
+
actual: number;
|
|
174
|
+
} | undefined;
|
|
175
|
+
/** Bytes as the plan names a disk: `2 GiB`, `2.5 GiB`. */
|
|
176
|
+
export declare const gib: (bytes: number) => string;
|
|
150
177
|
export declare function ownHint(err: unknown): string | undefined;
|
|
151
178
|
export declare function describeError(err: unknown): ErrorInfo;
|
|
152
179
|
/** Maps an error to the documented exit code. */
|
package/dist/errors.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* stderr with the API's error code, message and request id; the process exit
|
|
4
4
|
* code tells scripts what kind of failure it was (README "Exit codes").
|
|
5
5
|
*/
|
|
6
|
-
import { CheckoutTimeoutError, DurabilityLostError, ExecStartError, FilesystemSaveFailedError, FilesystemSaveTimeoutError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TemplateBuildTimeoutError, TemplateFileError, TemplateUploadError } from '@shardflux/sdk';
|
|
6
|
+
import { budgetHint, CheckoutTimeoutError, DurabilityLostError, ExecStartError, FilesystemSaveFailedError, FilesystemSaveTimeoutError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TemplateBuildTimeoutError, TemplateFileError, TemplateUploadError } from '@shardflux/sdk';
|
|
7
7
|
export const EXIT = {
|
|
8
8
|
ok: 0,
|
|
9
9
|
/** Any other failure: API refusal (conflict, quota, ...), network, protocol. */
|
|
@@ -83,9 +83,17 @@ export class TermsNotAcceptedError extends CodedError {
|
|
|
83
83
|
export class AuthConfigError extends HintedError {
|
|
84
84
|
name = 'AuthConfigError';
|
|
85
85
|
}
|
|
86
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* A workspace key that no workspace of this project has (or another name that resolves to nothing). `details` keeps the
|
|
88
|
+
* API's reason when shard rewords its 404 (0.20.1+: `filesystem_not_found` from shard template delete).
|
|
89
|
+
*/
|
|
87
90
|
export class NotFoundError extends HintedError {
|
|
88
91
|
name = 'NotFoundError';
|
|
92
|
+
details;
|
|
93
|
+
constructor(message, hint, details) {
|
|
94
|
+
super(message, hint);
|
|
95
|
+
this.details = details;
|
|
96
|
+
}
|
|
89
97
|
}
|
|
90
98
|
/**
|
|
91
99
|
* `shard machine setup --from-this-computer` (0.20.0+) ran but did not save a machine: the agent failed, ran past its
|
|
@@ -175,8 +183,38 @@ export function redact(text, ...secrets) {
|
|
|
175
183
|
return out;
|
|
176
184
|
}
|
|
177
185
|
/** The `hint:` a CLI error carries itself, if any. */
|
|
186
|
+
/**
|
|
187
|
+
* After a save that failed (0.20.2+): the version in use and the source VM are as they were, so the same save can run
|
|
188
|
+
* again; the build's log says what happened.
|
|
189
|
+
*/
|
|
190
|
+
export function saveRecovery(level, inUse, buildId, failure) {
|
|
191
|
+
const kept = inUse > 0 ? `version ${inUse} stays in use and ` : '';
|
|
192
|
+
const vm = level === 'machine' ? 'machine' : 'project';
|
|
193
|
+
// Larger than the plan's workspace disk (0.21.1+): the same save fails again until the VM or the plan changes.
|
|
194
|
+
const disk = planDiskLimit(failure);
|
|
195
|
+
if (disk) {
|
|
196
|
+
const open = level === 'machine' ? ' (shard machine open)' : '';
|
|
197
|
+
return `${kept}your ${vm} VM is untouched; delete what it does not need there${open}, such as caches and build output, then run shard ${level} save --wait; or upgrade to a plan with more than ${gib(disk.limit)} of disk: shard billing plans`;
|
|
198
|
+
}
|
|
199
|
+
if (failure?.retryable === false)
|
|
200
|
+
return `${kept}your ${vm} VM is untouched; change what the message names in it, then run shard ${level} save --wait`;
|
|
201
|
+
const log = buildId ? `; build log: shard templates builds log ${buildId}` : '';
|
|
202
|
+
return `${kept}your ${vm} VM is untouched; run shard ${level} save --wait again (it saves from the same VM)${log}`;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* A save larger than the plan's workspace disk (0.21.1+): resource_limit with details.limit template_org_bytes_max;
|
|
206
|
+
* `limit` is the plan's workspace disk and `actual` the version's size, in bytes.
|
|
207
|
+
*/
|
|
208
|
+
export function planDiskLimit(failure) {
|
|
209
|
+
const d = failure?.details;
|
|
210
|
+
return failure?.code === 'resource_limit' && d?.limit === 'template_org_bytes_max' && typeof d.value === 'number' && typeof d.actual === 'number' ? { limit: d.value, actual: d.actual } : undefined;
|
|
211
|
+
}
|
|
212
|
+
/** Bytes as the plan names a disk: `2 GiB`, `2.5 GiB`. */
|
|
213
|
+
export const gib = (bytes) => `${Number((bytes / 2 ** 30).toFixed(2))} GiB`;
|
|
178
214
|
export function ownHint(err) {
|
|
179
|
-
|
|
215
|
+
if (err instanceof ShardfluxApiError && err.reason === 'codex_chatgpt_login_required')
|
|
216
|
+
return 'Sign in to Codex with ChatGPT using codex login, then copy your Codex login with shard setup again.';
|
|
217
|
+
return budgetHint(err) ?? (err instanceof HintedError ? err.hint : undefined);
|
|
180
218
|
}
|
|
181
219
|
function isNamed(err, name) {
|
|
182
220
|
return typeof err === 'object' && err !== null && err.name === name;
|
|
@@ -202,6 +240,7 @@ export function describeError(err) {
|
|
|
202
240
|
...(err.operationId ? { operation_id: err.operationId } : {}),
|
|
203
241
|
...(err.details ? { details: err.details } : {}),
|
|
204
242
|
source: err.source,
|
|
243
|
+
...(err.rateLimit ? { rate_limit: err.rateLimit } : {}),
|
|
205
244
|
...(err.retryAfterSeconds !== undefined ? { retry_after_seconds: err.retryAfterSeconds } : {}),
|
|
206
245
|
};
|
|
207
246
|
}
|
|
@@ -260,10 +299,14 @@ export function describeError(err) {
|
|
|
260
299
|
if (err instanceof TemplateBuildTimeoutError)
|
|
261
300
|
return { code: 'timeout', message: `${err.message}; follow it with: shard templates builds get ${err.build.id}`, last_state: err.build.state };
|
|
262
301
|
// machine/template save --wait (0.13.3+): the save ended without its version, or is still building at the deadline.
|
|
263
|
-
|
|
264
|
-
|
|
302
|
+
// 0.20.2+: details.build_id, and a failure's recovery as `fix` (the hint line).
|
|
303
|
+
// 0.21.1+: the build's failure (error_code and its details, e.g. limit/value/actual) and whether the same save can
|
|
304
|
+
// succeed (retryable false: larger than the plan's workspace disk, or another refusal that repeats until the VM changes).
|
|
305
|
+
if (err instanceof FilesystemSaveFailedError) {
|
|
306
|
+
return { code: 'filesystem_save_failed', message: err.message, retryable: err.retryable, fix: saveRecovery(err.state.level, err.state.version, err.buildId, err), details: { ...(err.details ?? {}), ...(err.code ? { error_code: err.code } : {}), level: err.state.level, version: err.version, ...(err.buildId ? { build_id: err.buildId } : {}), ...(err.progress ? { last_progress: err.progress } : {}) } };
|
|
307
|
+
}
|
|
265
308
|
if (err instanceof FilesystemSaveTimeoutError)
|
|
266
|
-
return { code: 'timeout', message: `${err.message}; check with shard ${err.state.level}`, last_state: err.state.state, details: { level: err.state.level, version: err.version } };
|
|
309
|
+
return { code: 'timeout', message: `${err.message}; check with shard ${err.state.level}`, last_state: err.state.state, details: { level: err.state.level, version: err.version, ...(err.buildId ? { build_id: err.buildId } : {}), ...(err.progress ? { last_progress: err.progress } : {}) } };
|
|
267
310
|
if (err instanceof TemplateUploadError)
|
|
268
311
|
return { code: 'upload_failed', message: err.message, ...(err.status ? { status: err.status } : {}), details: { sha256: err.sha256, ...(err.code ? { storage_code: err.code } : {}) } };
|
|
269
312
|
if (err instanceof AuthConfigError)
|
|
@@ -271,7 +314,7 @@ export function describeError(err) {
|
|
|
271
314
|
if (err instanceof MfaPendingError)
|
|
272
315
|
return { code: 'mfa_required', message: err.message };
|
|
273
316
|
if (err instanceof NotFoundError)
|
|
274
|
-
return { code: 'not_found', message: err.message };
|
|
317
|
+
return { code: 'not_found', message: err.message, ...(err.details ? { details: err.details } : {}) };
|
|
275
318
|
if (err instanceof MachineSetupError)
|
|
276
319
|
return { code: 'machine_setup_failed', message: err.message, retryable: true };
|
|
277
320
|
if (err instanceof InterruptedError) {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import type { AgentFilesystem, Workspace } from '@shardflux/sdk';
|
|
1
|
+
import type { AgentFilesystem, AgentFilesystemDeleted, Workspace } from '@shardflux/sdk';
|
|
2
2
|
import type { Ctx, Handler } from './commands.js';
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* One line per status: what is in use and what is saving or failed, its platform base and setup cache. A project
|
|
5
|
+
* template unused for 30 days (rebase_deferred, 0.20.1+) follows the machine when the next conversation starts.
|
|
6
|
+
*/
|
|
4
7
|
export declare function filesystemLine(state: AgentFilesystem & {
|
|
5
8
|
cache_scope?: string;
|
|
6
9
|
}): string;
|
|
@@ -10,15 +13,19 @@ export interface BuildQueue {
|
|
|
10
13
|
slots: number;
|
|
11
14
|
expected_wait_seconds: number;
|
|
12
15
|
}
|
|
13
|
-
/**
|
|
14
|
-
export declare function
|
|
15
|
-
/**
|
|
16
|
-
export declare
|
|
17
|
-
/** How often a waited save reads its build's place in the queue. */
|
|
18
|
-
export declare const QUEUE_POLL_MS = 2500;
|
|
16
|
+
/** The pending build's queue, if it waits for a slot; nothing when the API predates it or the build cannot be read. */
|
|
17
|
+
export declare function pendingQueue(ctx: Ctx, state: AgentFilesystem): Promise<BuildQueue | undefined>;
|
|
18
|
+
/** Within one phase of a waited save, a changed counter line is printed at most this often (0.20.2+). */
|
|
19
|
+
export declare const SAVE_PROGRESS_LINE_MS = 30000;
|
|
19
20
|
/** The save's one line about software under the home that went into the image (0.18.0+), as the in-VM shard says it. */
|
|
20
21
|
export declare function movedLine(state: AgentFilesystem, level: 'machine' | 'template'): string;
|
|
21
|
-
/**
|
|
22
|
+
/** `shard template delete` (0.20.1+): what went, in one line. A binding-only delete has no template. */
|
|
23
|
+
export declare function deletedLine(deleted: AgentFilesystemDeleted): string;
|
|
24
|
+
/**
|
|
25
|
+
* The VM `shard machine save` saves: the machine VM of the version in use, else the one the last save came from. A
|
|
26
|
+
* deleted one is not there (0.20.2+: the machine row offers Save again only while it is).
|
|
27
|
+
*/
|
|
28
|
+
export declare function machineSaveSource(ctx: Ctx, state: AgentFilesystem): Promise<Workspace | null>;
|
|
22
29
|
/** The account's machine VM, created or resumed; `sync: false` skips the first-open config sync (an account sign-in). */
|
|
23
30
|
export declare function openMachine(ctx: Ctx, state?: AgentFilesystem, opts?: {
|
|
24
31
|
sync?: boolean;
|
|
@@ -1,18 +1,46 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { FilesystemSaveFailedError, FilesystemSaveTimeoutError, SavePhases, ShardfluxApiError, savePreflightText, saveProgressText } from '@shardflux/sdk';
|
|
1
3
|
import { codingAgentShell } from "./agent-setup.js";
|
|
2
4
|
import { waitTimeout } from "./commands.js";
|
|
3
|
-
import { UsageError } from "./errors.js";
|
|
5
|
+
import { NotFoundError, UsageError } from "./errors.js";
|
|
4
6
|
import { projectIdentity, normalizeRemote } from "./project-identity.js";
|
|
5
7
|
import { AGENT_HANDLERS } from "./agent-commands.js";
|
|
6
8
|
import { readSetupProgress } from "./assisted-setup.js";
|
|
7
9
|
import { syncAgentConfig } from "./agent-config-sync.js";
|
|
8
10
|
import { progress } from "./progress.js";
|
|
11
|
+
import { getConversation, localConversations } from "./conversations.js";
|
|
9
12
|
function remote() {
|
|
10
13
|
const project = projectIdentity('./');
|
|
11
14
|
return { project, repoRemote: project.remote ?? normalizeRemote(`file://${project.folder}`) };
|
|
12
15
|
}
|
|
13
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* The VM this computer saves a project's template from (0.23.0+): the newest conversation this checkout started whose
|
|
18
|
+
* VM is there now, else the project's workspace (`<project>/main`, setups from before conversations). Each
|
|
19
|
+
* conversation's VM is read from the API: a restore by any client moves it to a new VM. In a VM, `shard template save`
|
|
20
|
+
* saves that VM itself.
|
|
21
|
+
*/
|
|
22
|
+
async function templateSource(ctx, project) {
|
|
23
|
+
const gone = (error) => { if (error instanceof ShardfluxApiError && error.status === 404)
|
|
24
|
+
return undefined; throw error; };
|
|
25
|
+
const started = Object.values(localConversations(ctx)).filter(r => r.folder === project.folder && r.api_url === ctx.apiUrl)
|
|
26
|
+
.sort((a, b) => Date.parse(b.conversation.created_at) - Date.parse(a.conversation.created_at));
|
|
27
|
+
for (const r of started) {
|
|
28
|
+
const c = await getConversation(ctx, r.conversation.id).catch(gone);
|
|
29
|
+
if (!c || c.ended_at || !c.workspace_id)
|
|
30
|
+
continue;
|
|
31
|
+
const ws = await ctx.cloud().workspaces.get(c.workspace_id, { signal: ctx.signal }).catch(gone);
|
|
32
|
+
if (ws && !ws.data.deleted_at)
|
|
33
|
+
return ws;
|
|
34
|
+
}
|
|
35
|
+
return await ctx.cloud().workspaces.findByKey(`${project.slug}/main`, { signal: ctx.signal }) ?? undefined;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* One line per status: what is in use and what is saving or failed, its platform base and setup cache. A project
|
|
39
|
+
* template unused for 30 days (rebase_deferred, 0.20.1+) follows the machine when the next conversation starts.
|
|
40
|
+
*/
|
|
14
41
|
export function filesystemLine(state) {
|
|
15
|
-
const
|
|
42
|
+
const rebase = state.rebase_deferred ? ', follows your machine on next use' : state.rebase_pending ? ', rebase pending' : '';
|
|
43
|
+
const base = state.platform_base ? `; platform ${state.platform_base.slug}@${state.platform_base.version}${rebase}` : '';
|
|
16
44
|
return usageLine(state) + base + (state.cache_scope ? `; cache: ${state.cache_scope}` : '');
|
|
17
45
|
}
|
|
18
46
|
function usageLine(state) {
|
|
@@ -29,21 +57,8 @@ function output(ctx, state) {
|
|
|
29
57
|
ctx.io.stdout.write(ctx.json ? JSON.stringify(state) + '\n' : `${filesystemLine(state)}\n`);
|
|
30
58
|
return 0;
|
|
31
59
|
}
|
|
32
|
-
/** "about 40 s" / "about 3 min": the queue's estimate, rounded the way people say it. */
|
|
33
|
-
export function aboutWait(seconds) {
|
|
34
|
-
if (seconds < 90)
|
|
35
|
-
return `about ${Math.max(10, Math.round(seconds / 10) * 10)} s`;
|
|
36
|
-
return `about ${Math.round(seconds / 60)} min`;
|
|
37
|
-
}
|
|
38
|
-
/** One line for a build that waits for a slot (0.20.0+): "Queued behind 3 builds · about 40 s" (running ones + those ahead). */
|
|
39
|
-
export function queueLine(q) {
|
|
40
|
-
const behind = q.slots + q.position - 1;
|
|
41
|
-
return `Queued behind ${behind} build${behind === 1 ? '' : 's'} · ${aboutWait(q.expected_wait_seconds)}`;
|
|
42
|
-
}
|
|
43
|
-
/** How often a waited save reads its build's place in the queue. */
|
|
44
|
-
export const QUEUE_POLL_MS = 2_500;
|
|
45
60
|
/** The pending build's queue, if it waits for a slot; nothing when the API predates it or the build cannot be read. */
|
|
46
|
-
async function pendingQueue(ctx, state) {
|
|
61
|
+
export async function pendingQueue(ctx, state) {
|
|
47
62
|
if (!state.pending)
|
|
48
63
|
return undefined;
|
|
49
64
|
try {
|
|
@@ -54,53 +69,74 @@ async function pendingQueue(ctx, state) {
|
|
|
54
69
|
return undefined;
|
|
55
70
|
}
|
|
56
71
|
}
|
|
72
|
+
/** Within one phase of a waited save, a changed counter line is printed at most this often (0.20.2+). */
|
|
73
|
+
export const SAVE_PROGRESS_LINE_MS = 30_000;
|
|
57
74
|
/**
|
|
58
|
-
* Save output:
|
|
59
|
-
*
|
|
75
|
+
* Save output: the preflight line (what the save copies and about how long it takes, 0.20.2+), then one line naming
|
|
76
|
+
* the version being saved and its state (its place in the queue while the plan's build slots are in use, 0.20.0+), or
|
|
77
|
+
* with --wait one stderr line per phase with its counters, and the saved version. --json: one document at the end
|
|
78
|
+
* (`phases` with --wait, `preflight` when known), the progress as {"event":"save_progress"} lines on stderr.
|
|
60
79
|
*/
|
|
61
|
-
async function saved(ctx, values, save, level) {
|
|
80
|
+
async function saved(ctx, values, save, preflight, level) {
|
|
62
81
|
if (values.wait !== true && values.timeout !== undefined)
|
|
63
82
|
throw new UsageError('--timeout needs --wait');
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
83
|
+
const pf = await preflight();
|
|
84
|
+
if (pf && !ctx.json)
|
|
85
|
+
progress(ctx, savePreflightText(pf));
|
|
86
|
+
else if (pf && values.wait === true)
|
|
87
|
+
ctx.io.stderr.write(`${JSON.stringify({ event: 'save_preflight', ...pf })}\n`);
|
|
88
|
+
const phases = new SavePhases();
|
|
89
|
+
// The first line names the version; later lines say what changed: a new phase or place in the queue at once, newer
|
|
90
|
+
// counters within a phase at most every SAVE_PROGRESS_LINE_MS.
|
|
91
|
+
let announced = false;
|
|
92
|
+
let last = { line: '', milestone: '', at: 0 };
|
|
93
|
+
const onProgress = (p, final = false) => {
|
|
94
|
+
phases.update(p);
|
|
95
|
+
const line = saveProgressText(p);
|
|
96
|
+
const milestone = JSON.stringify([p.state, p.progress?.phase ?? null, p.queue?.position ?? null, p.queue?.slots ?? null]);
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
if (announced && (line === last.line || (!final && milestone === last.milestone && now - last.at < SAVE_PROGRESS_LINE_MS)))
|
|
69
99
|
return;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
progress(ctx, line);
|
|
77
|
-
last = line;
|
|
78
|
-
await new Promise(resolve => {
|
|
79
|
-
const timer = setTimeout(resolve, QUEUE_POLL_MS);
|
|
80
|
-
stop.signal.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
})();
|
|
100
|
+
if (ctx.json)
|
|
101
|
+
ctx.io.stderr.write(`${JSON.stringify({ event: 'save_progress', ...p })}\n`);
|
|
102
|
+
else
|
|
103
|
+
progress(ctx, announced ? line : `${level}: saving version ${p.version} · ${line}`);
|
|
104
|
+
announced = true;
|
|
105
|
+
last = { line, milestone, at: now };
|
|
84
106
|
};
|
|
85
|
-
const wait = values.wait === true ? { timeoutMs: waitTimeout(values, 600_000), onChange: (state) => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
107
|
+
const wait = values.wait === true ? { timeoutMs: waitTimeout(values, 600_000), onProgress, onChange: (state) => {
|
|
108
|
+
// An API that does not show the build: the version alone.
|
|
109
|
+
if (!announced && state.state === 'building' && state.pending) {
|
|
110
|
+
// The filesystem says saving; the unavailable build view cannot tell us queued versus running.
|
|
111
|
+
if (ctx.json)
|
|
112
|
+
ctx.io.stderr.write(`${JSON.stringify({ event: 'save_started', level, version: state.pending.version, build_id: state.pending.build_id })}\n`);
|
|
113
|
+
else
|
|
114
|
+
progress(ctx, `${level}: saving version ${state.pending.version}`);
|
|
115
|
+
announced = true;
|
|
89
116
|
}
|
|
90
117
|
} } : undefined;
|
|
118
|
+
const document = (state, extra = {}) => ({ ...state, ...extra, ...(wait ? { phases: phases.list() } : {}), ...(pf ? { preflight: pf } : {}) });
|
|
91
119
|
let state;
|
|
92
120
|
try {
|
|
93
121
|
state = await save({ signal: ctx.signal, ...(wait ? { wait } : {}) });
|
|
94
122
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
123
|
+
catch (err) {
|
|
124
|
+
if ((err instanceof FilesystemSaveTimeoutError || err instanceof FilesystemSaveFailedError) && err.progress)
|
|
125
|
+
onProgress(err.progress, true);
|
|
126
|
+
// --json: stdout still gets its one document, with how far the save got (still building at a timeout).
|
|
127
|
+
if (ctx.json && (err instanceof FilesystemSaveTimeoutError || err instanceof FilesystemSaveFailedError)) {
|
|
128
|
+
if (err instanceof FilesystemSaveFailedError)
|
|
129
|
+
phases.end();
|
|
130
|
+
ctx.io.stdout.write(`${JSON.stringify(document(err.state))}\n`);
|
|
131
|
+
}
|
|
132
|
+
throw err;
|
|
98
133
|
}
|
|
134
|
+
phases.end();
|
|
99
135
|
const queue = wait ? undefined : await pendingQueue(ctx, state);
|
|
100
136
|
if (ctx.json)
|
|
101
|
-
return output(ctx, queue ? {
|
|
137
|
+
return output(ctx, document(state, queue ? { queue } : {}));
|
|
102
138
|
const moved = movedLine(state, level);
|
|
103
|
-
const queued = queue ? `; ${
|
|
139
|
+
const queued = queue ? `; ${saveProgressText({ version: state.pending?.version ?? 0, build_id: state.pending?.build_id ?? '', state: 'queued', queue })}` : '';
|
|
104
140
|
ctx.io.stdout.write(wait ? `${level}: version ${state.version} saved${moved}\n` : state.pending ? `${filesystemLine(state)}${queued}; shard ${level} shows when it is ready${moved}\n` : `${filesystemLine(state)}${moved}\n`);
|
|
105
141
|
return 0;
|
|
106
142
|
}
|
|
@@ -111,6 +147,12 @@ export function movedLine(state, level) {
|
|
|
111
147
|
return '';
|
|
112
148
|
return `; ${moved.length} large tool folder${moved.length === 1 ? '' : 's'} went into the ${level} image (${moved.join(', ')}); they keep working at the same paths`;
|
|
113
149
|
}
|
|
150
|
+
/** `shard template delete` (0.20.1+): what went, in one line. A binding-only delete has no template. */
|
|
151
|
+
export function deletedLine(deleted) {
|
|
152
|
+
const versions = deleted.template ? ` (${deleted.template.versions} saved version${deleted.template.versions === 1 ? '' : 's'})` : '';
|
|
153
|
+
const builds = deleted.canceled_builds > 0 ? `, stopped ${deleted.canceled_builds} build${deleted.canceled_builds === 1 ? '' : 's'}` : '';
|
|
154
|
+
return `template: deleted${versions}${builds}; new conversations start from your machine until setup saves a new one`;
|
|
155
|
+
}
|
|
114
156
|
const rollbackTo = (value) => {
|
|
115
157
|
if (value === undefined)
|
|
116
158
|
return undefined;
|
|
@@ -119,7 +161,17 @@ const rollbackTo = (value) => {
|
|
|
119
161
|
throw new UsageError('--to must be a saved version number');
|
|
120
162
|
return n;
|
|
121
163
|
};
|
|
122
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* The VM `shard machine save` saves: the machine VM of the version in use, else the one the last save came from. A
|
|
166
|
+
* deleted one is not there (0.20.2+: the machine row offers Save again only while it is).
|
|
167
|
+
*/
|
|
168
|
+
export async function machineSaveSource(ctx, state) {
|
|
169
|
+
const cloud = ctx.cloud();
|
|
170
|
+
const ws = await cloud.workspaces.findByKey(`machine/${state.organization_id}/v${state.version}`, { includeDeleted: false, signal: ctx.signal }) ??
|
|
171
|
+
(state.workspace_id ? await cloud.workspaces.get(state.workspace_id).catch((e) => { if (e instanceof ShardfluxApiError && e.status === 404)
|
|
172
|
+
return null; throw e; }) : null);
|
|
173
|
+
return ws && !ws.data.deleted_at ? ws : null;
|
|
174
|
+
}
|
|
123
175
|
/** The account's machine VM, created or resumed; `sync: false` skips the first-open config sync (an account sign-in). */
|
|
124
176
|
export async function openMachine(ctx, state, opts = {}) {
|
|
125
177
|
const cloud = ctx.cloud();
|
|
@@ -161,12 +213,10 @@ export const FILESYSTEM_HANDLERS = {
|
|
|
161
213
|
},
|
|
162
214
|
async 'machine save'(ctx, values) {
|
|
163
215
|
const cloud = ctx.cloud();
|
|
164
|
-
const
|
|
165
|
-
const ws = await cloud.workspaces.findByKey(`machine/${state.organization_id}/v${state.version}`, { signal: ctx.signal }) ??
|
|
166
|
-
(state.workspace_id ? await cloud.workspaces.get(state.workspace_id) : null);
|
|
216
|
+
const ws = await machineSaveSource(ctx, await cloud.machine.get({ signal: ctx.signal }));
|
|
167
217
|
if (!ws)
|
|
168
218
|
throw new UsageError('Open your machine and change it first, then run shard machine save', 'shard machine in a terminal, or shard machine open --json for an agent (0.17.1+)');
|
|
169
|
-
return saved(ctx, values, options => cloud.machine.save({ workspaceId: ws.id }, options), 'machine');
|
|
219
|
+
return saved(ctx, values, options => cloud.machine.save({ workspaceId: ws.id }, options), () => cloud.machine.preflight({ workspaceId: ws.id }, { signal: ctx.signal }), 'machine');
|
|
170
220
|
},
|
|
171
221
|
async 'machine rollback'(ctx, values) {
|
|
172
222
|
return output(ctx, await ctx.cloud().machine.rollback({ to: rollbackTo(values.to) }, { signal: ctx.signal }));
|
|
@@ -174,7 +224,7 @@ export const FILESYSTEM_HANDLERS = {
|
|
|
174
224
|
async template(ctx) {
|
|
175
225
|
const { project, repoRemote } = remote();
|
|
176
226
|
const state = await ctx.cloud().templates.getProject(repoRemote, { signal: ctx.signal });
|
|
177
|
-
const ws = await
|
|
227
|
+
const ws = await templateSource(ctx, project);
|
|
178
228
|
const progress = ws ? await readSetupProgress(ctx, ws.data) : undefined;
|
|
179
229
|
const attachments = ws ? await ctx.cloud().volumes.listAttached(ws.id) : undefined;
|
|
180
230
|
const mounted = attachments?.data.some((a) => a.mount_path === '/mnt/shardflux-cache' && a.mounted && a.volume.name === 'shardflux-setup-cache' && a.volume.org_shared);
|
|
@@ -182,12 +232,38 @@ export const FILESYSTEM_HANDLERS = {
|
|
|
182
232
|
},
|
|
183
233
|
async 'template save'(ctx, values) {
|
|
184
234
|
const { project, repoRemote } = remote();
|
|
185
|
-
const ws = await
|
|
235
|
+
const ws = await templateSource(ctx, project);
|
|
186
236
|
if (!ws)
|
|
187
|
-
throw new UsageError('
|
|
188
|
-
return saved(ctx, values, options => ctx.cloud().templates.saveProject({ repoRemote, workspaceId: ws.id }, options), 'template');
|
|
237
|
+
throw new UsageError('Start a conversation in this project with shard ./ before saving its template', 'or run shard template save in a conversation\'s VM');
|
|
238
|
+
return saved(ctx, values, options => ctx.cloud().templates.saveProject({ repoRemote, workspaceId: ws.id }, options), () => ctx.cloud().templates.preflightProject({ workspaceId: ws.id }, { signal: ctx.signal }), 'template');
|
|
189
239
|
},
|
|
190
240
|
async 'template rollback'(ctx, values) {
|
|
191
241
|
return output(ctx, await ctx.cloud().templates.rollbackProject({ repoRemote: remote().repoRemote, to: rollbackTo(values.to) }, { signal: ctx.signal }));
|
|
192
242
|
},
|
|
243
|
+
/** `shard template delete` (0.20.1+): asks first in a terminal; elsewhere it needs --yes. */
|
|
244
|
+
async 'template delete'(ctx, values) {
|
|
245
|
+
const { repoRemote } = remote();
|
|
246
|
+
if (values.yes !== true) {
|
|
247
|
+
if (!ctx.input.interactive)
|
|
248
|
+
throw new UsageError(`refusing to delete without --yes (the project template of ${repoRemote} and its saved versions are deleted; new conversations start from your machine until setup saves a new one)`, 'run: shard template delete --yes');
|
|
249
|
+
const answer = (await ctx.input.answer(`Delete the project template for ${repoRemote}? Its saved versions go; new conversations start from your machine until setup saves a new one. [y/N] `))?.trim() ?? '';
|
|
250
|
+
if (!/^y(es)?$/i.test(answer)) {
|
|
251
|
+
ctx.io.stderr.write('Kept the project template.\n');
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
let deleted;
|
|
256
|
+
try {
|
|
257
|
+
// One key per run: a retry after a dropped connection replays the same delete.
|
|
258
|
+
deleted = await ctx.cloud().templates.deleteProject({ repoRemote }, { idempotencyKey: randomUUID(), signal: ctx.signal });
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
if (err instanceof ShardfluxApiError && err.status === 404 && err.details?.reason === 'filesystem_not_found') {
|
|
262
|
+
throw new NotFoundError(`template: nothing saved yet for ${repoRemote}, so there is nothing to delete`, 'setup saves it after shard ./, or run shard template save', { reason: 'filesystem_not_found', repo_remote: repoRemote });
|
|
263
|
+
}
|
|
264
|
+
throw err;
|
|
265
|
+
}
|
|
266
|
+
ctx.io.stdout.write(ctx.json ? JSON.stringify(deleted) + '\n' : `${deletedLine(deleted)}\n`);
|
|
267
|
+
return 0;
|
|
268
|
+
},
|
|
193
269
|
};
|
package/dist/format.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentSession, ApiKey, AuditEvent, AuthSessionInfo, BillingCatalog, BillingInvoicePage, BillingSubscription, DataExport, DraftState, ExecutionChange, ExecutionResult, FileInfo, EgressPolicyVersion, Invitation, Member, Organization, Project, ProjectEgressPolicy, Volume, VolumeAttachment, WorkspaceEgressPolicy, FileList, FileSearchResponse, Operation, Secret, SecretAccessEvent, SecretVersion, SpendPolicy, SuspendRequest, TemplateBuild, TemplateDetail, TemplateDiffEntry, TemplateDraft, TemplateFileEntry, TemplateLanguages, TemplatePackagePage, TemplateSettings, TemplateSummary, TemplateVersion, WorkspaceChange, WorkspaceSecretBindings, WorkspaceMemory, WorkspaceView, MemoryGrow, BurstSummary, ResizeResult, ExposePortResult, ExposedPort } from '@shardflux/sdk';
|
|
1
|
+
import type { AgentSession, ApiKey, AuditEvent, AuthSessionInfo, BillingCatalog, BillingInvoicePage, BillingSubscription, DataExport, DraftState, ExecutionChange, ExecutionResult, FileInfo, EgressPolicyVersion, Invitation, Member, Organization, Project, ProjectEgressPolicy, Volume, VolumeAttachment, WorkspaceEgressPolicy, FileList, FileSearchResponse, Operation, Secret, SecretAccessEvent, SecretVersion, SpendPolicy, SuspendRequest, SuspendProgress, TemplateBuild, TemplateDetail, TemplateDiffEntry, TemplateDraft, TemplateFileEntry, TemplateLanguages, TemplatePackagePage, TemplateSettings, TemplateSummary, TemplateVersion, WorkspaceChange, WorkspaceSecretBindings, WorkspaceMemory, WorkspaceView, MemoryGrow, BurstSummary, ResizeResult, ExposePortResult, ExposedPort } from '@shardflux/sdk';
|
|
2
2
|
import type { ErrorInfo } from './errors.js';
|
|
3
3
|
/** A workspace as the port commands name it. */
|
|
4
4
|
interface PortWorkspace {
|
|
@@ -70,6 +70,7 @@ export declare function operationTable(ops: readonly Operation[]): string;
|
|
|
70
70
|
*/
|
|
71
71
|
export declare function durableText(op: Operation): string;
|
|
72
72
|
export declare function operationDetail(op: Operation): string;
|
|
73
|
+
export declare function suspendProgressText(p: SuspendProgress): string;
|
|
73
74
|
export declare function sessionTable(sessions: readonly AgentSession[]): string;
|
|
74
75
|
/** `files stat`: one path's metadata; the revision only when requested and returned. */
|
|
75
76
|
export declare function fileInfoText(f: FileInfo): string;
|
package/dist/format.js
CHANGED
|
@@ -300,6 +300,15 @@ export function operationDetail(op) {
|
|
|
300
300
|
['updated', time(op.updated_at)],
|
|
301
301
|
['completed', time(op.completed_at)],
|
|
302
302
|
['error', err ? `${scalar(err.code)}${err.message ? `: ${scalar(err.message)}` : ''}` : '-'],
|
|
303
|
+
]) + (op.progress ? `\n${suspendProgressText(op.progress)}` : '');
|
|
304
|
+
}
|
|
305
|
+
export function suspendProgressText(p) {
|
|
306
|
+
return kv([
|
|
307
|
+
['operation', p.operation_id], ['phase', p.phase], ['operation state', p.state], ['workspace state', p.workspace_state],
|
|
308
|
+
['cancelable', p.cancelable ? 'yes' : 'no'], ['progress supported', p.supported ? 'yes' : 'unavailable'], ['next action', p.safe_action],
|
|
309
|
+
...(p.phase_started_at ? [['phase started', p.phase_started_at]] : []),
|
|
310
|
+
...(p.deadline_at ? [['host deadline', p.deadline_at]] : []),
|
|
311
|
+
...(p.accepted === true ? [['cancel request', 'accepted; use the operation state to confirm recovery']] : []),
|
|
303
312
|
]);
|
|
304
313
|
}
|
|
305
314
|
export function sessionTable(sessions) {
|
|
@@ -546,7 +555,7 @@ export function feedbackCommand(e) {
|
|
|
546
555
|
if (SHELL_SAFE.test(code))
|
|
547
556
|
parts.push(`--error-code ${code}`);
|
|
548
557
|
parts.push('"<what you expected>"');
|
|
549
|
-
return parts.join(' ');
|
|
558
|
+
return `${parts.join(' ')} # preview only; show the message and context, then use --yes only after explicit user authorization`;
|
|
550
559
|
}
|
|
551
560
|
/**
|
|
552
561
|
* One stderr line: `error: <message> [code=... status=... request_id=...]` plus a hint when there is one, and the
|