@shardflux/cli 0.28.2 → 0.28.3
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 +20 -0
- package/README.md +34 -3
- package/dist/args.d.ts +1 -1
- package/dist/args.js +12 -1
- package/dist/board-args.js +2 -1
- package/dist/board-commands.js +5 -2
- package/dist/commands.d.ts +2 -0
- package/dist/commands.js +5 -5
- package/dist/credential-device-capability.d.ts +8 -0
- package/dist/credential-device-capability.js +21 -0
- package/dist/credential-setup.d.ts +13 -0
- package/dist/credential-setup.js +170 -0
- package/dist/credential-sources.d.ts +34 -0
- package/dist/credential-sources.js +400 -0
- package/dist/device-bridge.d.ts +8 -1
- package/dist/device-bridge.js +45 -2
- package/dist/device-helper.js +15 -7
- package/dist/device-mode.d.ts +7 -0
- package/dist/device-mode.js +13 -0
- package/dist/device-outcomes.d.ts +3 -0
- package/dist/device-outcomes.js +19 -0
- package/dist/handler-loader.js +2 -1
- package/dist/handlers.js +2 -1
- package/dist/land.d.ts +4 -1
- package/dist/land.js +32 -3
- package/dist/machine-setup.d.ts +1 -0
- package/dist/machine-setup.js +5 -2
- package/dist/main.js +30 -2
- package/dist/orchestrator-args.js +11 -1
- package/dist/orchestrator-commands.js +44 -3
- package/dist/task-auto-land.d.ts +22 -0
- package/dist/task-auto-land.js +162 -0
- package/dist/task-land-hub.py +6 -0
- package/dist/vault.js +3 -8
- package/package.json +6 -6
package/dist/handler-loader.js
CHANGED
|
@@ -9,7 +9,7 @@ const groups = [
|
|
|
9
9
|
[["cli-proxy", "cli-proxy import", "cli-proxy export", "cli-proxy use", "cli-proxy forward"], async () => (await import("./cli-proxy.js")).CLI_PROXY_HANDLERS],
|
|
10
10
|
[["agents computers"], async () => (await import("./thread-on-computer.js")).THREAD_HANDLERS],
|
|
11
11
|
[["conversations resume", "conversations reader", "conversations list", "conversations feed", "conversations get", "conversations permissions", "conversations answer", "conversations watch", "conversations send", "conversations start", "conversations wait", "conversations land", "files pull", "conversations move", "conversations archive", "conversations unarchive", "conversations seen", "conversations delete"], async () => (await import("./conversation-commands.js")).CONVERSATION_HANDLERS],
|
|
12
|
-
[["orchestrator say", "orchestrator list", "orchestrator retry", "orchestrator dismiss", "orchestrator restore"], async () => (await import("./orchestrator-commands.js")).ORCHESTRATOR_HANDLERS],
|
|
12
|
+
[["orchestrator show", "orchestrator say", "orchestrator list", "orchestrator retry", "orchestrator dismiss", "orchestrator restore"], async () => (await import("./orchestrator-commands.js")).ORCHESTRATOR_HANDLERS],
|
|
13
13
|
[["board", "task add", "task show", "task edit", "task rm", "task steer", "task land", "task clear-done", "needs-you", "needs-you answer", "approvals", "approvals set", "approvals allow", "approvals remove"], async () => (await import("./board-commands.js")).BOARD_HANDLERS],
|
|
14
14
|
[["conversations transcript", "conversations events"], async () => (await import("./conversation-transcript.js")).TRANSCRIPT_HANDLERS],
|
|
15
15
|
[["conversations try"], async () => (await import("./conversation-local.js")).CONVERSATION_HANDLERS],
|
|
@@ -25,6 +25,7 @@ const groups = [
|
|
|
25
25
|
[["agents targets", "agents terminal-run", "agents open claude-desktop", "agents open codex-app", "agents open t3", "agents open web", "ssh-proxy"], async () => (await import("./agent-target-commands.js")).TARGET_HANDLERS],
|
|
26
26
|
[["agents merge", "agents keep", "agents discard", "agents worktree", "agents restore"], async () => (await import("./agent-git-commands.js")).AGENT_GIT_HANDLERS],
|
|
27
27
|
[["menubar start", "menubar stop", "menubar status", "menubar account"], async () => (await import("./menubar.js")).MENUBAR_HANDLERS],
|
|
28
|
+
[["credentials setup"], async () => (await import("./credential-setup.js")).CREDENTIAL_SETUP_HANDLERS],
|
|
28
29
|
[["machine", "machine open", "machine save", "machine rollback", "template", "template save", "template rollback", "template delete"], async () => (await import("./filesystem-commands.js")).FILESYSTEM_HANDLERS],
|
|
29
30
|
[["machine setup"], async () => (await import("./machine-setup.js")).MACHINE_SETUP_HANDLERS],
|
|
30
31
|
[["settings"], async () => (await import("./client-settings.js")).SETTINGS_HANDLERS],
|
package/dist/handlers.js
CHANGED
|
@@ -30,8 +30,9 @@ import { ACCOUNT_POOL_HANDLERS, AGENT_MODELS_HANDLERS } from "./account-pool.js"
|
|
|
30
30
|
import { MOVE_HANDLERS } from "./move.js";
|
|
31
31
|
import { AGENT_CONFIG_HANDLERS } from "./agent-config-sync.js";
|
|
32
32
|
import { MACHINE_SETUP_HANDLERS } from "./machine-setup.js";
|
|
33
|
+
import { CREDENTIAL_SETUP_HANDLERS } from "./credential-setup.js";
|
|
33
34
|
import { SETTINGS_HANDLERS } from "./client-settings.js";
|
|
34
35
|
import { PROJECT_PREPARATION_HANDLERS } from "./project-preparation.js";
|
|
35
36
|
import { DRIVE_HANDLERS } from "./drive-commands.js";
|
|
36
37
|
import { BRAIN_HANDLERS } from "./brain-commands.js";
|
|
37
|
-
export const HANDLERS = { ...PROJECT_PREPARATION_HANDLERS, ...PROJECT_LINK_HANDLERS, ...MOVE_HANDLERS, ...DEVICE_HANDLERS, ...CODEX_CLOUD_HANDLERS, ...CLI_PROXY_HANDLERS, ...THREAD_HANDLERS, ...CONVERSATION_HANDLERS, ...ORCHESTRATOR_HANDLERS, ...BOARD_HANDLERS, ...TRANSCRIPT_HANDLERS, ...LOCAL_CONVERSATION_HANDLERS, ...TWIN_HANDLERS, ...CREDENTIAL_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...ACCOUNT_POOL_HANDLERS, ...AGENT_MODELS_HANDLERS, ...KEY_HANDLERS, ...ACCOUNT_HANDLERS, ...AGENT_HANDLERS, ...CENTER_HANDLERS, ...TARGET_HANDLERS, ...AGENT_GIT_HANDLERS, ...MENUBAR_HANDLERS, ...FILESYSTEM_HANDLERS, ...MACHINE_SETUP_HANDLERS, ...SETTINGS_HANDLERS, ...UPDATE_HANDLERS, ...AGENT_CONFIG_HANDLERS, ...ROUTER_HANDLERS, ...TRANSCRIBE_HANDLERS, ...DRIVE_HANDLERS, ...BRAIN_HANDLERS };
|
|
38
|
+
export const HANDLERS = { ...PROJECT_PREPARATION_HANDLERS, ...PROJECT_LINK_HANDLERS, ...MOVE_HANDLERS, ...DEVICE_HANDLERS, ...CODEX_CLOUD_HANDLERS, ...CLI_PROXY_HANDLERS, ...THREAD_HANDLERS, ...CONVERSATION_HANDLERS, ...ORCHESTRATOR_HANDLERS, ...BOARD_HANDLERS, ...TRANSCRIPT_HANDLERS, ...LOCAL_CONVERSATION_HANDLERS, ...TWIN_HANDLERS, ...CREDENTIAL_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...ACCOUNT_POOL_HANDLERS, ...AGENT_MODELS_HANDLERS, ...KEY_HANDLERS, ...ACCOUNT_HANDLERS, ...AGENT_HANDLERS, ...CENTER_HANDLERS, ...TARGET_HANDLERS, ...AGENT_GIT_HANDLERS, ...MENUBAR_HANDLERS, ...FILESYSTEM_HANDLERS, ...MACHINE_SETUP_HANDLERS, ...CREDENTIAL_SETUP_HANDLERS, ...SETTINGS_HANDLERS, ...UPDATE_HANDLERS, ...AGENT_CONFIG_HANDLERS, ...ROUTER_HANDLERS, ...TRANSCRIBE_HANDLERS, ...DRIVE_HANDLERS, ...BRAIN_HANDLERS };
|
package/dist/land.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { TwinCell } from './worktree-twin.js';
|
|
|
4
4
|
/** The hub side of a landing, shipped next to this module (src in the monorepo, dist when published). */
|
|
5
5
|
export declare function hubScript(): string;
|
|
6
6
|
export interface HubLanding {
|
|
7
|
-
state: 'landed' | 'conflict' | 'unrelated' | 'missing' | 'raced' | 'error';
|
|
7
|
+
state: 'landed' | 'conflict' | 'unrelated' | 'missing' | 'unready' | 'raced' | 'error';
|
|
8
8
|
into: string;
|
|
9
9
|
before?: string | null;
|
|
10
10
|
after?: string;
|
|
@@ -18,6 +18,7 @@ export interface HubLanding {
|
|
|
18
18
|
source?: string;
|
|
19
19
|
message?: string;
|
|
20
20
|
reason?: string;
|
|
21
|
+
unintegrated?: string[];
|
|
21
22
|
}
|
|
22
23
|
/** Merges `result` (made from `sources` when missing) into the hub's default branch, in a VM that reads the hub. */
|
|
23
24
|
export declare function landOnHub(cell: TwinCell, input: {
|
|
@@ -26,6 +27,7 @@ export declare function landOnHub(cell: TwinCell, input: {
|
|
|
26
27
|
title: string;
|
|
27
28
|
into?: string;
|
|
28
29
|
hub?: string;
|
|
30
|
+
requireResult?: boolean;
|
|
29
31
|
}, signal?: AbortSignal): Promise<HubLanding>;
|
|
30
32
|
export interface FolderLanding {
|
|
31
33
|
path: string;
|
|
@@ -114,6 +116,7 @@ export declare function landedText(o: LandOutcome): string;
|
|
|
114
116
|
/** `shard task land <task>`: the task's result into the project's default branch and the linked folder, then recorded. */
|
|
115
117
|
export declare function landTask(d: LandDeps, taskId: string, opts?: {
|
|
116
118
|
path?: string;
|
|
119
|
+
automatic?: boolean;
|
|
117
120
|
}): Promise<LandOutcome>;
|
|
118
121
|
/** `shard conversations land <id> merge`: the conversation's branch into the default branch and its folder, then recorded. */
|
|
119
122
|
export declare function landConversation(d: LandDeps, id: string, opts?: {
|
package/dist/land.js
CHANGED
|
@@ -24,12 +24,15 @@ import { selectedProjects } from "./project-preparation.js";
|
|
|
24
24
|
import { projectIdentity } from "./project-identity.js";
|
|
25
25
|
import { HUB_PATH, backupRoot, deviceId, git, gitText, inspectWorktree, landingRound, listWorktrees } from "./worktree-twin.js";
|
|
26
26
|
import { projectHubCell } from "./worktree-twin-engine.js";
|
|
27
|
+
import { createHash } from 'node:crypto';
|
|
28
|
+
import { withTargetLock } from "./target-lock.js";
|
|
29
|
+
import { TwinLedger } from "./worktree-twin.js";
|
|
27
30
|
let script;
|
|
28
31
|
/** The hub side of a landing, shipped next to this module (src in the monorepo, dist when published). */
|
|
29
32
|
export function hubScript() { return script ??= readFileSync(new URL('./task-land-hub.py', import.meta.url), 'utf8'); }
|
|
30
33
|
/** Merges `result` (made from `sources` when missing) into the hub's default branch, in a VM that reads the hub. */
|
|
31
34
|
export async function landOnHub(cell, input, signal) {
|
|
32
|
-
const body = { hub: input.hub ?? HUB_PATH, result: input.result, sources: input.sources, title: input.title, ...(input.into ? { default: input.into } : {}) };
|
|
35
|
+
const body = { hub: input.hub ?? HUB_PATH, result: input.result, sources: input.sources, title: input.title, ...(input.into ? { default: input.into } : {}), ...(input.requireResult ? { require_result: true } : {}) };
|
|
33
36
|
const r = await cell.exec.run(['python3', '-c', hubScript()], { stdin: JSON.stringify(body), timeoutMs: 300_000, maxOutputBytes: 1 << 20, ...(signal ? { signal } : {}) });
|
|
34
37
|
const line = r.stdout.trim().split('\n').at(-1) ?? '';
|
|
35
38
|
if (r.exitCode !== 0 || !line.startsWith('{'))
|
|
@@ -133,10 +136,14 @@ export function landDeps(ctx) {
|
|
|
133
136
|
async function land(d, l, onConflict) {
|
|
134
137
|
const vm = await d.hubCell(l.projectId, l.workspaceIds);
|
|
135
138
|
try {
|
|
136
|
-
const input = { result: l.result, sources: l.sources.map(s => s.branch), title: l.title, ...(d.hub ? { hub: d.hub } : {}) };
|
|
139
|
+
const input = { result: l.result, sources: l.sources.map(s => s.branch), title: l.title, ...(d.hub ? { hub: d.hub } : {}), ...(l.testedResult ? { requireResult: true } : {}) };
|
|
137
140
|
let hub = await landOnHub(vm.cell, input, d.signal);
|
|
138
141
|
if (hub.state === 'raced')
|
|
139
142
|
hub = await landOnHub(vm.cell, input, d.signal);
|
|
143
|
+
if (hub.state === 'unready') {
|
|
144
|
+
await onConflict(hub);
|
|
145
|
+
throw new CodedError('land_result_unready', 'The task result is waiting for its master to merge, test and push every worker branch.', undefined, { reason: hub.reason, unintegrated: hub.unintegrated });
|
|
146
|
+
}
|
|
140
147
|
if (hub.state === 'conflict') {
|
|
141
148
|
await onConflict(hub);
|
|
142
149
|
throw new CodedError('land_conflict', `Not landed: ${list(hub.conflicts ?? [])} changed on both sides (${hub.stage === 'integrate' ? `${hub.source} and ${l.result}` : `${l.result} and ${hub.into}`}).`, 'its master merges them and asks to land again', { stage: hub.stage, into: hub.into, conflicts: hub.conflicts ?? [] });
|
|
@@ -210,9 +217,17 @@ function currentResult(d) {
|
|
|
210
217
|
const resultBranch = (taskId) => `shard/${taskId}`;
|
|
211
218
|
/** `shard task land <task>`: the task's result into the project's default branch and the linked folder, then recorded. */
|
|
212
219
|
export async function landTask(d, taskId, opts = {}) {
|
|
220
|
+
// The desktop board, service and a terminal can notice the same result together. Serialize their real merge and
|
|
221
|
+
// re-read its current done report inside the lock; Git and the API still check the commit/report on every device.
|
|
222
|
+
const key = createHash('sha256').update(taskId).digest('hex');
|
|
223
|
+
return withTargetLock(join(d.stateDir, 'task-land-locks', key), () => landTaskUnlocked(d, taskId, opts), d.signal);
|
|
224
|
+
}
|
|
225
|
+
async function landTaskUnlocked(d, taskId, opts) {
|
|
213
226
|
const client = d.client, signal = d.signal ? { signal: d.signal } : {};
|
|
214
227
|
const detail = await client.tasks.get(taskId, signal);
|
|
215
228
|
const task = detail.task, title = task.title ?? task.text.split('\n')[0].slice(0, 80);
|
|
229
|
+
if (opts.automatic && new TwinLedger(d.stateDir).mode(task.project_id) !== 'auto')
|
|
230
|
+
throw new CodedError('land_automatic_off', 'Automatic landing is off on this computer or for this project.');
|
|
216
231
|
const report = currentResult(detail);
|
|
217
232
|
if (!report || (task.state !== 'done' && task.state !== 'unverified'))
|
|
218
233
|
throw new CodedError('task_not_done', 'This task has no finished result to land yet.', 'land it after its master’s done report', { state: task.state });
|
|
@@ -227,11 +242,25 @@ export async function landTask(d, taskId, opts = {}) {
|
|
|
227
242
|
// The repository the result is of: its workers' (or its master's), so it lands only in a folder of that repository.
|
|
228
243
|
const repository = workers.find(w => w.repo_remote)?.repo_remote ?? (task.master_id ? (await client.conversations.get(task.master_id).catch(() => null))?.repo_remote : undefined) ?? null;
|
|
229
244
|
const folder = opts.path ?? d.folder(task.project_id, undefined, repository ?? undefined);
|
|
245
|
+
// A task created on the phone has no local conversation record. Wait for its actual linked repository; never
|
|
246
|
+
// wake a VM or mark a hub-only merge as delivery to an arbitrary folder on this computer.
|
|
247
|
+
if (opts.automatic && !folder)
|
|
248
|
+
throw new CodedError('land_device_unlinked', 'This task has no linked repository folder on this computer.');
|
|
230
249
|
const done = await land(d, { projectId: task.project_id, title, name: taskId, result: resultBranch(taskId), sources: workers.map(w => ({ branch: w.branch, conversation_id: w.id })),
|
|
231
|
-
base: workers.find(w => w.base_commit)?.base_commit ?? null, folder, repository, workspaceIds: workers.map(w => w.workspace_id).filter(Boolean) }, async (hub) => {
|
|
250
|
+
base: workers.find(w => w.base_commit)?.base_commit ?? null, folder, repository, workspaceIds: workers.map(w => w.workspace_id).filter(Boolean), testedResult: opts.automatic }, async (hub) => {
|
|
251
|
+
if (hub.state === 'unready') {
|
|
252
|
+
await client.tasks.steer(taskId, `Auto land is waiting for the tested task result: ${resultBranch(taskId)} ${hub.reason === 'result_missing' ? 'has not been pushed' : `does not contain ${list(hub.unintegrated ?? [])}`}. Merge every worker branch into it, resolve conflicts, run the tests on that merged result, push it to the project hub and seal done again.`, signal);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
232
255
|
const what = hub.stage === 'integrate' ? `${hub.source} into ${resultBranch(taskId)}` : `${resultBranch(taskId)} into ${hub.into}`;
|
|
233
256
|
await client.tasks.steer(taskId, `Land could not merge ${what}: ${list(hub.conflicts ?? [])} changed on both sides. Merge ${hub.into} from the project hub and every worker branch into ${resultBranch(taskId)}, resolve the conflicts so both changes work (ask the person only if the choice is theirs), run the tests, push ${resultBranch(taskId)} to the hub and seal done again.`, signal);
|
|
234
257
|
});
|
|
258
|
+
if (opts.automatic && done.folder?.state !== 'landed' && done.folder?.state !== 'already') {
|
|
259
|
+
const conflict = done.folder?.state === 'conflict';
|
|
260
|
+
throw new CodedError(conflict ? 'land_conflict' : 'land_folder_busy', conflict
|
|
261
|
+
? `Not landed in your folder: ${list(done.folder?.conflicts ?? [])} changed on both sides.`
|
|
262
|
+
: 'The linked folder is busy; its finished task will land when it is ready.');
|
|
263
|
+
}
|
|
235
264
|
let recorded = false, record_error;
|
|
236
265
|
try {
|
|
237
266
|
await client.tasks.recordLanding(taskId, { sha: done.result.sha, into: done.hub.into, branch: done.result.branch, reportId: report, conversationIds: done.contained }, signal);
|
package/dist/machine-setup.d.ts
CHANGED
package/dist/machine-setup.js
CHANGED
|
@@ -707,6 +707,8 @@ export async function machineSetup(ctx, values, deps = {}) {
|
|
|
707
707
|
const claim = claimRun(store, ctx.apiUrl, org, values['if-needed'] === true, startRec);
|
|
708
708
|
if (!claim.claimed) {
|
|
709
709
|
const { doc, text } = await status();
|
|
710
|
+
if (claim.record.state === 'ready')
|
|
711
|
+
await deps.credentials?.(ctx);
|
|
710
712
|
return output(ctx, { ...doc, skipped: true }, claim.record.state === 'running' ? `${text} (already running)` : text);
|
|
711
713
|
}
|
|
712
714
|
const phase = phaseWriter(ctx);
|
|
@@ -781,6 +783,7 @@ export async function machineSetup(ctx, values, deps = {}) {
|
|
|
781
783
|
writeRecord(store, ctx.apiUrl, org, rec);
|
|
782
784
|
phase('ready', line);
|
|
783
785
|
const row = machineRow(saved, rec);
|
|
786
|
+
await deps.credentials?.(ctx);
|
|
784
787
|
return output(ctx, { ...statusDoc(row, rec), workspace_key: ws.key }, rowText(row));
|
|
785
788
|
}
|
|
786
789
|
catch (error) {
|
|
@@ -808,7 +811,7 @@ export function startMachineSetupInBackground(ctx) {
|
|
|
808
811
|
mkdirSync(ctx.store.dir, { recursive: true, mode: 0o700 });
|
|
809
812
|
const out = openSync(join(ctx.store.dir, 'machine-setup.out'), 'w', 0o600);
|
|
810
813
|
try {
|
|
811
|
-
const child = spawn(ctx.runtime.node, [ctx.runtime.script, 'machine', 'setup', '--from-this-computer', '--if-needed', '--json'], {
|
|
814
|
+
const child = spawn(ctx.runtime.node, [ctx.runtime.script, 'machine', 'setup', '--from-this-computer', '--if-needed', '--with-credentials', '--json'], {
|
|
812
815
|
detached: true, stdio: ['ignore', out, out],
|
|
813
816
|
env: { ...process.env, ...ctx.io.env, SHARDFLUX_CONFIG_DIR: ctx.store.dir, SHARDFLUX_API_URL: ctx.apiUrl, SHARDFLUX_NO_UPDATE_CHECK: '1' }
|
|
814
817
|
});
|
|
@@ -825,5 +828,5 @@ export function startMachineSetupInBackground(ctx) {
|
|
|
825
828
|
}
|
|
826
829
|
}
|
|
827
830
|
export const MACHINE_SETUP_HANDLERS = {
|
|
828
|
-
'machine setup': (ctx, values) => machineSetup(ctx, values),
|
|
831
|
+
'machine setup': async (ctx, values) => machineSetup(ctx, values, values['with-credentials'] === true ? { credentials: (await import("./credential-setup.js")).credentialSetupAfterMachine } : {}),
|
|
829
832
|
};
|
package/dist/main.js
CHANGED
|
@@ -403,6 +403,24 @@ function feedbackHint(info, credentialHint) {
|
|
|
403
403
|
}
|
|
404
404
|
return `the feedback was not sent; email it to ${FEEDBACK_EMAIL} instead`;
|
|
405
405
|
}
|
|
406
|
+
function credentialFailure(info) {
|
|
407
|
+
const status = info.status;
|
|
408
|
+
const known = {
|
|
409
|
+
400: ['validation_failed', 'Check the credential name and scope, then try again.'],
|
|
410
|
+
401: ['unauthenticated', 'Sign in again, then retry the credential operation.'],
|
|
411
|
+
403: ['forbidden', 'This account cannot change credentials in that scope.'],
|
|
412
|
+
404: ['not_found', 'The credential or its request is no longer available.'],
|
|
413
|
+
409: ['conflict', 'The credential already exists or its request was answered. Refresh and try again.'],
|
|
414
|
+
413: ['validation_failed', 'The credential exceeds the secret store’s existing value size.'],
|
|
415
|
+
422: ['validation_failed', 'Check the credential name and scope, then try again.'],
|
|
416
|
+
429: ['rate_limited', 'The credential service is busy. Try again shortly.'],
|
|
417
|
+
};
|
|
418
|
+
const [code, message] = status === undefined ? ['credential_failed', 'The credential operation could not finish. Try again.']
|
|
419
|
+
: known[status] ?? ['dependency_unavailable', 'The credential service could not finish the operation. Try again shortly.'];
|
|
420
|
+
return { code, message, ...(status !== undefined ? { status } : {}),
|
|
421
|
+
...(info.request_id && /^req-[A-Za-z0-9_-]+$/.test(info.request_id) ? { request_id: info.request_id } : {}),
|
|
422
|
+
...(typeof info.retryable === 'boolean' ? { retryable: info.retryable } : {}) };
|
|
423
|
+
}
|
|
406
424
|
export async function run(argv, io, opts = {}) {
|
|
407
425
|
const signal = opts.signal ?? new AbortController().signal;
|
|
408
426
|
await registerInstalledT3(io.env, configDir(io.env), signal);
|
|
@@ -410,6 +428,7 @@ export async function run(argv, io, opts = {}) {
|
|
|
410
428
|
const rawKey = io.env.SHARDFLUX_API_KEY;
|
|
411
429
|
// Every credential this command loaded or received: redacted from anything printed on failure.
|
|
412
430
|
const secrets = [rawKey ?? '', io.env.SHARDFLUX_SESSION_TOKEN ?? ''].filter((s) => s.trim().length >= 8).map((s) => s.trim());
|
|
431
|
+
let writeOnlyValueLoaded = false;
|
|
413
432
|
const wantsJson = argv.slice(0, argv.includes('--') ? argv.indexOf('--') : argv.length).includes('--json');
|
|
414
433
|
const cred = { keySource: null, sessionSource: null, sessionRejected: false, keyRejected: false, email: null };
|
|
415
434
|
const store = new CredentialStore(configDir(io.env), (m) => void io.stderr.write(`${m}\n`));
|
|
@@ -421,7 +440,10 @@ export async function run(argv, io, opts = {}) {
|
|
|
421
440
|
let command = undefined;
|
|
422
441
|
const report = (err) => {
|
|
423
442
|
const code = exitCodeFor(err, signal.aborted);
|
|
424
|
-
const
|
|
443
|
+
const described = describeError(err);
|
|
444
|
+
// A proxy or validation error can echo any submitted value, including a one-byte credential or JSON escapes.
|
|
445
|
+
// Keep structured output valid and preserve the status/request id without copying remote prose or details.
|
|
446
|
+
const info = writeOnlyValueLoaded ? credentialFailure(described) : described;
|
|
425
447
|
if (code === EXIT.interrupted && info.code !== 'interrupted') {
|
|
426
448
|
info.code = 'interrupted';
|
|
427
449
|
info.message = 'Interrupted.';
|
|
@@ -439,7 +461,7 @@ export async function run(argv, io, opts = {}) {
|
|
|
439
461
|
}
|
|
440
462
|
}
|
|
441
463
|
// An error that knows the command to continue with (an interrupted or still running execution) brings its own hint.
|
|
442
|
-
const own = ownHint(err) ?? ((err instanceof InterruptedError || err instanceof ExecutionPendingError) && err.hint ? err.hint : undefined);
|
|
464
|
+
const own = writeOnlyValueLoaded ? undefined : ownHint(err) ?? ((err instanceof InterruptedError || err instanceof ExecutionPendingError) && err.hint ? err.hint : undefined);
|
|
443
465
|
const hint = command === 'feedback' ? feedbackHint(info, own ?? hintFor(info, cred, store.file, command)) : (own ?? hintFor(info, cred, store.file, command));
|
|
444
466
|
// --timing on a failure: the failed call's timing (err.timing, else the last recorded), before the error; with
|
|
445
467
|
// --json a `timing` field next to `error`.
|
|
@@ -669,6 +691,12 @@ export async function run(argv, io, opts = {}) {
|
|
|
669
691
|
if (value.length >= 8)
|
|
670
692
|
secrets.push(value);
|
|
671
693
|
},
|
|
694
|
+
addWriteOnlyValue(value) {
|
|
695
|
+
if (value.length)
|
|
696
|
+
writeOnlyValueLoaded = true;
|
|
697
|
+
if (value.length >= 8)
|
|
698
|
+
secrets.push(value);
|
|
699
|
+
},
|
|
672
700
|
async versionStatus(force) {
|
|
673
701
|
const check = new UpdateCheck({ dir: store.dir, apiUrl, distribution, fetch: base, userAgent, registry: npmRegistry(io.env) });
|
|
674
702
|
if (!force && updateCheckDisabled(io.env)) {
|
|
@@ -3,11 +3,21 @@ const ORG = {
|
|
|
3
3
|
};
|
|
4
4
|
const WHO = 'Your orchestrator is yours: one agent on its own Shardflux computer, the same on every device. Uses your signed-in session, or SHARDFLUX_API_KEY when it is set (a project key you created: a new orchestrator is filed under its project).';
|
|
5
5
|
export const ORCHESTRATOR_COMMANDS = [
|
|
6
|
+
{
|
|
7
|
+
path: ['orchestrator', 'show'],
|
|
8
|
+
summary: 'Read an input and its device-request outcomes, failures first',
|
|
9
|
+
positionals: [{ name: 'input-id', required: true }],
|
|
10
|
+
options: ORG,
|
|
11
|
+
description: 'Shows the input and durable requests made by its thread, including what ran, local and remote paths, exit codes and recorded errors. Reading starts no computer. A thread can also hear follow-ups; each JSON request identifies its conversation. With --json: {input, device_requests}.',
|
|
12
|
+
examples: ['shard orchestrator show in_abcdefghijklmnopqrstuvwxyz', 'shard orchestrator show in_abcdefghijklmnopqrstuvwxyz --json'],
|
|
13
|
+
},
|
|
6
14
|
{
|
|
7
15
|
path: ['orchestrator', 'say'],
|
|
8
16
|
summary: 'Say something to your orchestrator; one of its threads hears each input at once (0.27.0+)',
|
|
9
|
-
positionals: [{ name: 'words', variadic: true, words: true, required:
|
|
17
|
+
positionals: [{ name: 'words', variadic: true, words: true, required: false }],
|
|
10
18
|
options: {
|
|
19
|
+
file: { type: 'string', multiple: true, value: '<path|->', description: 'Attach a file (repeatable); - streams standard input as a file.' },
|
|
20
|
+
'file-name': { type: 'string', value: '<name>', description: 'The name of the standard-input file (default stdin).' },
|
|
11
21
|
task: { type: 'string', value: '<task-id>', description: 'Point at a task (task_…).' },
|
|
12
22
|
report: { type: 'string', value: '<report-id>', description: 'Point at a report (rep_…).' },
|
|
13
23
|
'needs-you': { type: 'string', value: '<item-id>', description: 'Point at something that waits on you (perm_…, oq_…).' },
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* a session) reaches the orchestrator of the member who created that key (person-scope.ts). Text output is one line
|
|
7
7
|
* per input, tab-separated, id first; --json prints the API's objects.
|
|
8
8
|
*/
|
|
9
|
+
import { mkdtemp, open, unlink, rmdir } from 'node:fs/promises';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join, basename } from 'node:path';
|
|
9
12
|
import { inputSettled } from '@shardflux/sdk';
|
|
10
13
|
import { parseDuration } from "./args.js";
|
|
11
14
|
import { out, str } from "./commands.js";
|
|
@@ -13,6 +16,7 @@ import { EXIT, UsageError } from "./errors.js";
|
|
|
13
16
|
import { humanBytes } from "./files-mirror.js";
|
|
14
17
|
import { inScope } from "./person-scope.js";
|
|
15
18
|
import { progress } from "./progress.js";
|
|
19
|
+
import { deviceOutcomeLines } from "./device-outcomes.js";
|
|
16
20
|
/** --wait's default: how long `say --wait` follows an input before it stops (the input goes on). */
|
|
17
21
|
export const SAY_WAIT_MS = 10 * 60_000;
|
|
18
22
|
const oneLine = (s, max) => {
|
|
@@ -198,17 +202,54 @@ const idOf = (id) => {
|
|
|
198
202
|
return id;
|
|
199
203
|
};
|
|
200
204
|
export const ORCHESTRATOR_HANDLERS = {
|
|
205
|
+
async 'orchestrator show'(ctx, values, [id]) {
|
|
206
|
+
const detail = await inScope(ctx, values, (client, scope) => client.orchestrator.input(idOf(id), { ...orgOnly(scope), signal: ctx.signal }));
|
|
207
|
+
out(ctx, [`${detail.input.id}\t${stateLabel(detail.input)}`, resultText(detail.input), ...deviceOutcomeLines(detail.device_requests)].filter(Boolean).join('\n') + '\n', detail);
|
|
208
|
+
return EXIT.ok;
|
|
209
|
+
},
|
|
201
210
|
async 'orchestrator say'(ctx, values, words) {
|
|
202
211
|
const text = words.join(' ');
|
|
203
|
-
|
|
204
|
-
|
|
212
|
+
const paths = Array.isArray(values.file) ? values.file : typeof values.file === 'string' ? [values.file] : [];
|
|
213
|
+
if (!text.trim() && !paths.length)
|
|
214
|
+
throw new UsageError('missing <words> for "shard orchestrator say"');
|
|
205
215
|
const selection = selectionOf(values);
|
|
206
216
|
const wait = values.wait === true;
|
|
207
217
|
const timeoutMs = str(values.timeout) === undefined ? SAY_WAIT_MS : parseDuration(str(values.timeout), 'timeout');
|
|
208
218
|
if (!wait && str(values.timeout) !== undefined)
|
|
209
219
|
throw new UsageError('--timeout goes with --wait.');
|
|
210
220
|
return inScope(ctx, values, async (client, scope) => {
|
|
211
|
-
|
|
221
|
+
let spool;
|
|
222
|
+
let input;
|
|
223
|
+
const files = [];
|
|
224
|
+
try {
|
|
225
|
+
for (const path of paths) {
|
|
226
|
+
if (path !== '-') {
|
|
227
|
+
files.push({ path, name: basename(path) });
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (!spool) {
|
|
231
|
+
spool = await mkdtemp(join(ctx.io.env.HOME ?? tmpdir(), 'shard-input-'));
|
|
232
|
+
const sink = await open(join(spool, 'stdin'), 'wx', 0o600);
|
|
233
|
+
try {
|
|
234
|
+
for await (const chunk of ctx.io.stdin) {
|
|
235
|
+
ctx.signal.throwIfAborted();
|
|
236
|
+
await sink.writeFile(chunk);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
await sink.close();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
files.push({ path: join(spool, 'stdin'), name: str(values['file-name']) ?? 'stdin' });
|
|
244
|
+
}
|
|
245
|
+
input = await client.orchestrator.say({ text, files, ...orgOnly(scope), ...(selection ? { selection } : {}), signal: ctx.signal });
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
if (spool) {
|
|
249
|
+
await unlink(join(spool, 'stdin')).catch(() => undefined);
|
|
250
|
+
await rmdir(spool);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
212
253
|
if (!wait) {
|
|
213
254
|
out(ctx, `${input.id}\t${stateLabel(input)}\n`, { input });
|
|
214
255
|
return EXIT.ok;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { NeedsYouItem } from '@shardflux/sdk';
|
|
2
|
+
import type { Ctx } from './commands.js';
|
|
3
|
+
import type { LandDeps } from './land.js';
|
|
4
|
+
interface TaskAutoLandOptions {
|
|
5
|
+
needs(signal: AbortSignal): Promise<NeedsYouItem[]>;
|
|
6
|
+
land: LandDeps;
|
|
7
|
+
enabled(project: string): boolean;
|
|
8
|
+
log(line: string): void;
|
|
9
|
+
now?: () => number;
|
|
10
|
+
stateDir?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class TaskAutoLand {
|
|
13
|
+
#private;
|
|
14
|
+
readonly options: TaskAutoLandOptions;
|
|
15
|
+
constructor(options: TaskAutoLandOptions);
|
|
16
|
+
/** One serial pass. A conflict waits for a new result; a disconnected/busy folder or failed receipt retries. */
|
|
17
|
+
tick(signal: AbortSignal): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/** Only projects actually linked on this computer; a task's repository is checked again before any VM is opened. */
|
|
20
|
+
export declare function taskLandProjects(ctx: Ctx): string[];
|
|
21
|
+
export declare function runTaskAutoLand(ctx: Ctx): Promise<void>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/** Finished board tasks reach their linked folders even with the desktop board closed (next CLI release). */
|
|
2
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
|
+
import { conversationClient } from "./conversation-commands.js";
|
|
4
|
+
import { localConversations } from "./conversations.js";
|
|
5
|
+
import { landDeps, landTask } from "./land.js";
|
|
6
|
+
import { selectedProjects } from "./project-preparation.js";
|
|
7
|
+
import { TwinLedger } from "./worktree-twin.js";
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { writePrivateFile } from "./credentials.js";
|
|
11
|
+
export class TaskAutoLand {
|
|
12
|
+
#attempts = new Map();
|
|
13
|
+
options;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.options = options;
|
|
16
|
+
if (options.stateDir) {
|
|
17
|
+
try {
|
|
18
|
+
const rows = JSON.parse(readFileSync(join(options.stateDir, 'task-auto-land.json'), 'utf8'));
|
|
19
|
+
for (const [id, row] of Object.entries(rows))
|
|
20
|
+
if (typeof row.key === 'string' && Number.isSafeInteger(row.tries)
|
|
21
|
+
&& Number.isFinite(row.retryAt) && typeof row.blocked === 'boolean')
|
|
22
|
+
this.#attempts.set(id, row);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error.code !== 'ENOENT')
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
#save() {
|
|
31
|
+
if (this.options.stateDir)
|
|
32
|
+
writePrivateFile(this.options.stateDir, join(this.options.stateDir, 'task-auto-land.json'), JSON.stringify(Object.fromEntries(this.#attempts)) + '\n');
|
|
33
|
+
}
|
|
34
|
+
/** One serial pass. A conflict waits for a new result; a disconnected/busy folder or failed receipt retries. */
|
|
35
|
+
async tick(signal) {
|
|
36
|
+
const now = this.options.now ?? Date.now, needs = await this.options.needs(signal);
|
|
37
|
+
// Keep the result key through disconnects and service restarts. A missing discovery page is not permission to
|
|
38
|
+
// repeat a conflict's steer or an already recorded landing. A master's next done report has a new key.
|
|
39
|
+
for (const item of needs) {
|
|
40
|
+
if (signal.aborted)
|
|
41
|
+
return;
|
|
42
|
+
if (item.kind !== 'land' || !item.task_id || !item.project_id || !this.options.enabled(item.project_id))
|
|
43
|
+
continue;
|
|
44
|
+
const key = `${item.id}@${item.since}`, prior = this.#attempts.get(item.id);
|
|
45
|
+
if (prior?.key === key && (prior.blocked || prior.retryAt > now()))
|
|
46
|
+
continue;
|
|
47
|
+
try {
|
|
48
|
+
const result = await landTask({ ...this.options.land, signal }, item.task_id, { automatic: true });
|
|
49
|
+
if (!result.recorded)
|
|
50
|
+
throw new Error(result.record_error ?? 'The task landing receipt was not recorded.');
|
|
51
|
+
this.#attempts.set(item.id, { key, tries: 0, retryAt: 0, blocked: true });
|
|
52
|
+
this.#save();
|
|
53
|
+
this.options.log(result.message);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (signal.aborted)
|
|
57
|
+
return;
|
|
58
|
+
const code = error.code;
|
|
59
|
+
const tries = prior?.key === key ? prior.tries + 1 : 1;
|
|
60
|
+
const blocked = code === 'land_conflict' || code === 'land_unrelated' || code === 'land_result_unready';
|
|
61
|
+
this.#attempts.set(item.id, { key, tries, blocked, retryAt: now() + Math.min(600_000, 1000 * 2 ** Math.min(tries, 9)) });
|
|
62
|
+
this.#save();
|
|
63
|
+
// Off and a folder on another computer need no warning. The existing one-per-task land item stays intact.
|
|
64
|
+
if (code !== 'land_automatic_off' && code !== 'land_device_unlinked')
|
|
65
|
+
this.options.log(`Task ${item.task_id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Only projects actually linked on this computer; a task's repository is checked again before any VM is opened. */
|
|
71
|
+
export function taskLandProjects(ctx) {
|
|
72
|
+
const profile = ctx.store.profile(ctx.apiUrl), fallback = profile.project_id ?? profile.api_key?.project_id;
|
|
73
|
+
return [...new Set([
|
|
74
|
+
...selectedProjects(ctx).map(p => p.project_id ?? fallback),
|
|
75
|
+
...Object.values(localConversations(ctx)).filter(c => !c.handed_off).map(c => c.project_id),
|
|
76
|
+
].filter((p) => Boolean(p)))];
|
|
77
|
+
}
|
|
78
|
+
export async function runTaskAutoLand(ctx) {
|
|
79
|
+
if (ctx.signal.aborted)
|
|
80
|
+
return;
|
|
81
|
+
const client = conversationClient(ctx), ledger = new TwinLedger(ctx.store.dir);
|
|
82
|
+
const profile = ctx.store.profile(ctx.apiUrl);
|
|
83
|
+
const key = Boolean(ctx.io.env.SHARDFLUX_API_KEY?.trim()) || profile.api_key !== undefined;
|
|
84
|
+
const log = (line) => { ctx.io.stderr.write(line + '\n'); };
|
|
85
|
+
const needs = new Map();
|
|
86
|
+
const watches = new Map();
|
|
87
|
+
const engine = new TaskAutoLand({
|
|
88
|
+
land: landDeps(ctx), enabled: project => ledger.mode(project) === 'auto', log, stateDir: ctx.store.dir,
|
|
89
|
+
needs: () => Promise.resolve([...needs.values()].flat()),
|
|
90
|
+
});
|
|
91
|
+
let landing, again = false;
|
|
92
|
+
const kick = () => {
|
|
93
|
+
again = true;
|
|
94
|
+
if (!landing)
|
|
95
|
+
landing = (async () => {
|
|
96
|
+
while (again && !ctx.signal.aborted) {
|
|
97
|
+
again = false;
|
|
98
|
+
await engine.tick(ctx.signal);
|
|
99
|
+
}
|
|
100
|
+
})().catch(error => { if (!ctx.signal.aborted)
|
|
101
|
+
log(`Task landing: ${error instanceof Error ? error.message : String(error)}`); })
|
|
102
|
+
.finally(() => { landing = undefined; });
|
|
103
|
+
return landing;
|
|
104
|
+
};
|
|
105
|
+
const follow = async (projectId, signal) => {
|
|
106
|
+
let failures = 0;
|
|
107
|
+
while (!signal.aborted) {
|
|
108
|
+
try {
|
|
109
|
+
const organizationId = key ? undefined : (await ctx.account().projects.get(projectId)).organization_id;
|
|
110
|
+
const scope = { projectId, ...(organizationId ? { organizationId } : {}), signal };
|
|
111
|
+
// Read once (including results that finished while offline), then use the held feed. No repeated whole-board
|
|
112
|
+
// reads while idle, and a slow project's feed cannot delay another project's landing or device heartbeats.
|
|
113
|
+
const board = await client.board.get(scope);
|
|
114
|
+
needs.set(projectId, board.needs_you);
|
|
115
|
+
void kick();
|
|
116
|
+
failures = 0;
|
|
117
|
+
for await (const update of client.board.watch({ ...scope, cursor: board.cursor })) {
|
|
118
|
+
if (update.reset)
|
|
119
|
+
needs.set(projectId, update.board.needs_you);
|
|
120
|
+
else if (update.needs_you !== null)
|
|
121
|
+
needs.set(projectId, update.needs_you);
|
|
122
|
+
void kick();
|
|
123
|
+
}
|
|
124
|
+
if (!signal.aborted)
|
|
125
|
+
throw new Error('The board feed ended before its device helper stopped.');
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (signal.aborted)
|
|
129
|
+
return;
|
|
130
|
+
log(`Task landing discovery: ${error instanceof Error ? error.message : String(error)}`);
|
|
131
|
+
await delay(Math.min(60_000, 1000 * 2 ** Math.min(failures++, 6)), undefined, { signal }).catch(() => undefined);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
try {
|
|
136
|
+
while (!ctx.signal.aborted) {
|
|
137
|
+
const projects = new Set(taskLandProjects(ctx).filter(p => ledger.mode(p) === 'auto'));
|
|
138
|
+
for (const [project, watch] of watches)
|
|
139
|
+
if (!projects.has(project)) {
|
|
140
|
+
watch.abort.abort();
|
|
141
|
+
await watch.work;
|
|
142
|
+
watches.delete(project);
|
|
143
|
+
needs.delete(project);
|
|
144
|
+
}
|
|
145
|
+
for (const project of projects)
|
|
146
|
+
if (!watches.has(project)) {
|
|
147
|
+
const abort = new AbortController(), signal = AbortSignal.any([ctx.signal, abort.signal]);
|
|
148
|
+
const work = follow(project, signal);
|
|
149
|
+
watches.set(project, { abort, work });
|
|
150
|
+
}
|
|
151
|
+
// The local setting and deferred folder/receipt retries do not wait for a remote board event.
|
|
152
|
+
await kick();
|
|
153
|
+
await delay(2000, undefined, { signal: ctx.signal }).catch(() => undefined);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
for (const watch of watches.values())
|
|
158
|
+
watch.abort.abort();
|
|
159
|
+
await Promise.all([...watches.values()].map(w => w.work));
|
|
160
|
+
await landing;
|
|
161
|
+
}
|
|
162
|
+
}
|
package/dist/task-land-hub.py
CHANGED
|
@@ -106,6 +106,12 @@ def land(a):
|
|
|
106
106
|
have = {b: tip(work, 'refs/heads/' + b) for b in wanted}
|
|
107
107
|
before = have.get(into)
|
|
108
108
|
t, created = have.get(result), False
|
|
109
|
+
if a.get('require_result'):
|
|
110
|
+
if not t:
|
|
111
|
+
return {'state': 'unready', 'into': into, 'reason': 'result_missing'}
|
|
112
|
+
missing = [s for s in sources if not have.get(s) or not ancestor(work, have[s], t)]
|
|
113
|
+
if missing:
|
|
114
|
+
return {'state': 'unready', 'into': into, 'reason': 'workers_not_integrated', 'unintegrated': missing}
|
|
109
115
|
if not t:
|
|
110
116
|
# A master that did not merge its workers: merge them here, each only when it merges cleanly.
|
|
111
117
|
for s in sources:
|
package/dist/vault.js
CHANGED
|
@@ -28,20 +28,15 @@ export function vaultOutput(vault, name, options = {}) {
|
|
|
28
28
|
return new Promise((resolve, reject) => {
|
|
29
29
|
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'ignore'], env: options.env, signal: options.signal });
|
|
30
30
|
const chunks = [];
|
|
31
|
-
let size = 0;
|
|
32
31
|
let failed = false;
|
|
33
32
|
const timer = setTimeout(() => {
|
|
34
33
|
failed = true;
|
|
35
34
|
child.kill('SIGKILL');
|
|
36
35
|
}, 30_000);
|
|
37
36
|
child.stdout.on('data', (chunk) => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
child.kill('SIGKILL');
|
|
42
|
-
}
|
|
43
|
-
else
|
|
44
|
-
chunks.push(chunk);
|
|
37
|
+
// Provider JSON includes metadata as well as the selected value. Enforce the secret store's value limit
|
|
38
|
+
// after extraction; a transport cap here rejected valid small values in larger items.
|
|
39
|
+
chunks.push(chunk);
|
|
45
40
|
});
|
|
46
41
|
child.on('error', () => {
|
|
47
42
|
clearTimeout(timer);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardflux/cli",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "shard: the Shardflux command line. Create an account, sign in, set up keys and billing, then open, run commands in, move files to, suspend, resume and fork serverless agent workspaces.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"snappyjs": "0.7.0",
|
|
54
54
|
"ws": "8.22.0",
|
|
55
55
|
"yaml": "2.9.1",
|
|
56
|
-
"@shardflux/sdk": "^0.30.
|
|
56
|
+
"@shardflux/sdk": "^0.30.3"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@eslint/js": "10.0.1",
|
|
@@ -65,10 +65,10 @@
|
|
|
65
65
|
"typescript-eslint": "8.70.1"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
|
-
"@shardflux/desktop-darwin-arm64": "0.9.
|
|
69
|
-
"@shardflux/desktop-darwin-x64": "0.9.
|
|
70
|
-
"@shardflux/desktop-linux-arm64": "0.9.
|
|
71
|
-
"@shardflux/desktop-linux-x64": "0.9.
|
|
68
|
+
"@shardflux/desktop-darwin-arm64": "0.9.4",
|
|
69
|
+
"@shardflux/desktop-darwin-x64": "0.9.4",
|
|
70
|
+
"@shardflux/desktop-linux-arm64": "0.9.4",
|
|
71
|
+
"@shardflux/desktop-linux-x64": "0.9.4"
|
|
72
72
|
},
|
|
73
73
|
"scripts": {
|
|
74
74
|
"build": "node scripts/build.mjs",
|