@shardflux/cli 0.28.2 → 0.28.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/README.md +64 -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/conversation-commands.js +2 -0
- package/dist/credential-device-capability.d.ts +8 -0
- package/dist/credential-device-capability.js +21 -0
- package/dist/credential-setup.d.ts +12 -0
- package/dist/credential-setup.js +181 -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-folders.d.ts +5 -0
- package/dist/device-folders.js +33 -9
- 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 +25 -1
- package/dist/orchestrator-commands.js +59 -4
- 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/device-helper.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
2
|
import { DeviceExecutor, askConnectedAcp, askDeviceApprover } from "./device-bridge.js";
|
|
3
|
+
import { credentialHeartbeat } from "./credential-device-capability.js";
|
|
3
4
|
import { ShardfluxApiError } from '@shardflux/sdk';
|
|
4
5
|
import { allOrgs, allProjects } from "./account-scope.js";
|
|
5
6
|
import { ConversationMirrorState, mirrorProjectConversations } from "./conversation-stash.js";
|
|
@@ -17,13 +18,16 @@ import { AgentConfigReturnState, runAgentConfigLoop } from "./agent-config-retur
|
|
|
17
18
|
import { askLocalDeviceApproval } from "./device-approvals.js";
|
|
18
19
|
import { resumeCloudStartup, startCarried } from "./codex-cloud.js";
|
|
19
20
|
import { runProjectPreparation } from "./project-preparation.js";
|
|
21
|
+
import { runTaskAutoLand } from "./task-auto-land.js";
|
|
20
22
|
/** The menu bar owns this helper and its stdin; VM data can request actions but cannot answer approvals. */
|
|
21
23
|
export async function runDeviceHelper(ctx, options = {}) {
|
|
22
24
|
const abort = new AbortController(), signal = AbortSignal.any([ctx.signal, abort.signal]);
|
|
23
25
|
ctx = { ...ctx, signal };
|
|
24
26
|
const pending = new Map();
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
// A service never reads approvals from stdin. In particular the Linux fallback may start after its command has
|
|
28
|
+
// already finished: creating an unconsumed stream with that aborted signal emits an uncaught AbortError.
|
|
29
|
+
const inputSource = options.service ? undefined : Readable.from(ctx.io.stdin, { objectMode: false, signal });
|
|
30
|
+
const input = inputSource ? (async () => {
|
|
27
31
|
let tail = '';
|
|
28
32
|
for await (const bytes of inputSource) {
|
|
29
33
|
tail += Buffer.from(bytes).toString();
|
|
@@ -40,7 +44,7 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
}
|
|
43
|
-
})().finally(() => abort.abort());
|
|
47
|
+
})().finally(() => abort.abort()) : Promise.resolve();
|
|
44
48
|
void input.catch(() => abort.abort());
|
|
45
49
|
// `until` ends one question early: the person chose a mode that lets the request run by itself (device-mode.ts).
|
|
46
50
|
const ask = async (approval, until) => {
|
|
@@ -81,11 +85,14 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
81
85
|
// sign-in change later renews again and a refusal right after a renewal stops (never a loop).
|
|
82
86
|
let renewedAt = 0;
|
|
83
87
|
const report = (e) => { ctx.io.stderr.write(`Device bridge: ${e instanceof Error ? e.message : String(e)}\n`); };
|
|
84
|
-
let mirroring, twin, returning, preparing, projects;
|
|
88
|
+
let mirroring, twin, returning, preparing, projects, taskLanding;
|
|
85
89
|
try {
|
|
86
90
|
return await withTargetLock(join(ctx.store.dir, 'device-helper-lock'), async () => {
|
|
87
91
|
// Finished conversations land in their local branches (worktree-twin-engine.ts); its own loop and backoff.
|
|
88
92
|
twin = runWorktreeTwin(ctx, deviceId, signal).catch(report);
|
|
93
|
+
// Board tasks (including those delegated from the phone) have no locally started conversation. Consume their
|
|
94
|
+
// finished results independently, through the exact same merge/check/receipt path as `shard task land`.
|
|
95
|
+
taskLanding = runTaskAutoLand(ctx).catch(report);
|
|
89
96
|
// Skills, memories and CLAUDE.md lines saved in the cloud come back on their own cadence (agent-config-return.ts):
|
|
90
97
|
// never behind a files-mirror pass, which takes minutes for an account with hundreds of conversations. Every 2 s
|
|
91
98
|
// while one of this user's conversations is active, backing off to 60 s when none is.
|
|
@@ -161,13 +168,13 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
161
168
|
// This device's local conversations of a project (started here or opened here): their requests come here first.
|
|
162
169
|
const linked = (projectId) => Object.values(localConversations(ctx)).filter(r => !r.handed_off && (!projectId || r.project_id === projectId))
|
|
163
170
|
.sort((a, b) => (b.conversation.created_at ?? '').localeCompare(a.conversation.created_at ?? '')).slice(0, 500).map(r => r.conversation.id);
|
|
164
|
-
const beat = (projectId) =>
|
|
171
|
+
const beat = (projectId) => credentialHeartbeat(ctx, { ...(projectId ? { projectId } : {}), deviceId, leaseSeconds: 30, ...(links ? { conversationIds: linked(projectId) } : {}) })
|
|
165
172
|
.catch((e) => {
|
|
166
173
|
// An API before device links (0.25.0+ clients) refuses the list: register without it from then on.
|
|
167
174
|
if (!links || !(e instanceof ShardfluxApiError && [400, 422].includes(e.status)))
|
|
168
175
|
throw e;
|
|
169
176
|
links = false;
|
|
170
|
-
return
|
|
177
|
+
return credentialHeartbeat(ctx, { ...(projectId ? { projectId } : {}), deviceId, leaseSeconds: 30 });
|
|
171
178
|
});
|
|
172
179
|
const take = (rows) => {
|
|
173
180
|
for (const r of rows) {
|
|
@@ -261,7 +268,7 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
261
268
|
}
|
|
262
269
|
finally {
|
|
263
270
|
abort.abort();
|
|
264
|
-
inputSource
|
|
271
|
+
inputSource?.destroy();
|
|
265
272
|
for (const resolve of pending.values())
|
|
266
273
|
resolve('deny');
|
|
267
274
|
pending.clear();
|
|
@@ -273,6 +280,7 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
273
280
|
await twin;
|
|
274
281
|
await preparing;
|
|
275
282
|
await projects;
|
|
283
|
+
await taskLanding;
|
|
276
284
|
await input.catch(() => undefined);
|
|
277
285
|
}
|
|
278
286
|
}
|
package/dist/device-mode.d.ts
CHANGED
|
@@ -38,7 +38,14 @@ export interface DeviceActivity {
|
|
|
38
38
|
local_path?: string;
|
|
39
39
|
exit_code?: number;
|
|
40
40
|
finished_at?: string;
|
|
41
|
+
error?: string;
|
|
41
42
|
}
|
|
42
43
|
export declare function recordDeviceActivity(dir: string, entry: DeviceActivity): void;
|
|
44
|
+
/** Persist the result before posting it: a stopped helper or failed API call must not erase the actual device error. */
|
|
45
|
+
export declare function recordDeviceResult(dir: string, entry: DeviceActivity, result: {
|
|
46
|
+
state: DeviceActivity['state'];
|
|
47
|
+
exit_code?: number;
|
|
48
|
+
stderr_b64?: string;
|
|
49
|
+
}): void;
|
|
43
50
|
/** What ran without asking, newest first: one entry per request with its latest state. */
|
|
44
51
|
export declare function deviceActivity(dir: string, limit?: number): DeviceActivity[];
|
package/dist/device-mode.js
CHANGED
|
@@ -87,6 +87,19 @@ export function recordDeviceActivity(dir, entry) {
|
|
|
87
87
|
}
|
|
88
88
|
catch { /* the request's own result still reaches its conversation */ }
|
|
89
89
|
}
|
|
90
|
+
/** Persist the result before posting it: a stopped helper or failed API call must not erase the actual device error. */
|
|
91
|
+
export function recordDeviceResult(dir, entry, result) {
|
|
92
|
+
const bytes = result.stderr_b64 ? Buffer.from(result.stderr_b64, 'base64') : null;
|
|
93
|
+
let error;
|
|
94
|
+
if (bytes && bytes.toString('base64').replace(/=+$/, '') === result.stderr_b64.replace(/=+$/, '')) {
|
|
95
|
+
try {
|
|
96
|
+
error = new TextDecoder('utf-8', { fatal: true }).decode(bytes) || undefined;
|
|
97
|
+
}
|
|
98
|
+
catch { /* no readable error text */ }
|
|
99
|
+
}
|
|
100
|
+
recordDeviceActivity(dir, { ...entry, at: new Date().toISOString(), state: result.state,
|
|
101
|
+
...(result.exit_code !== undefined ? { exit_code: result.exit_code } : {}), ...(error !== undefined ? { error } : {}) });
|
|
102
|
+
}
|
|
90
103
|
/** What ran without asking, newest first: one entry per request with its latest state. */
|
|
91
104
|
export function deviceActivity(dir, limit = 20) {
|
|
92
105
|
let text;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** The actual request and its outcome, never shortened to a count or a generic failure. */
|
|
2
|
+
export function deviceOutcomeLines(requests) {
|
|
3
|
+
if (!requests.length)
|
|
4
|
+
return [];
|
|
5
|
+
const ordered = [...requests].sort((a, b) => Number(b.failed) - Number(a.failed) || Date.parse(b.created_at) - Date.parse(a.created_at) || b.id.localeCompare(a.id));
|
|
6
|
+
return ['', 'Device requests', ...ordered.flatMap(r => {
|
|
7
|
+
const state = r.failed ? 'failed' : r.state;
|
|
8
|
+
const lines = [`${r.id}\t${state}${typeof r.exit_code === 'number' ? ` (exit ${r.exit_code})` : ''}\t${r.what}`];
|
|
9
|
+
if (r.path)
|
|
10
|
+
lines.push(` Local: ${r.path}`);
|
|
11
|
+
if (r.remote_path)
|
|
12
|
+
lines.push(` Remote: ${r.remote_path}`);
|
|
13
|
+
if (r.error)
|
|
14
|
+
lines.push(` ${r.error}`);
|
|
15
|
+
else if (r.failed)
|
|
16
|
+
lines.push(' Error text unavailable');
|
|
17
|
+
return lines;
|
|
18
|
+
})];
|
|
19
|
+
}
|
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 capabilities", "orchestrator show", "orchestrator say", "orchestrator list", "orchestrator history", "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,35 @@ 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', 'capabilities'],
|
|
8
|
+
summary: 'Read current input file support for your recipient',
|
|
9
|
+
positionals: [], options: ORG,
|
|
10
|
+
description: 'Reads your recipient metadata without creating or waking a computer. With --json: {capabilities: {input_files}}.',
|
|
11
|
+
examples: ['shard orchestrator capabilities --json'],
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
path: ['orchestrator', 'history'],
|
|
15
|
+
summary: 'Page through every input, including old and dismissed answers',
|
|
16
|
+
positionals: [],
|
|
17
|
+
options: { cursor: { type: 'string', value: '<cursor>', description: 'The previous page’s next_cursor.' }, limit: { type: 'string', value: '<n>', description: 'Page size, 1-200 (default 100).' }, ...ORG },
|
|
18
|
+
description: 'Reads one chronological page without starting work or waking a computer. Follow next_cursor to read earlier inputs. With --json: {inputs, next_cursor}.',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
path: ['orchestrator', 'show'],
|
|
22
|
+
summary: 'Read an input and its device-request outcomes, failures first',
|
|
23
|
+
positionals: [{ name: 'input-id', required: true }],
|
|
24
|
+
options: ORG,
|
|
25
|
+
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}.',
|
|
26
|
+
examples: ['shard orchestrator show in_abcdefghijklmnopqrstuvwxyz', 'shard orchestrator show in_abcdefghijklmnopqrstuvwxyz --json'],
|
|
27
|
+
},
|
|
6
28
|
{
|
|
7
29
|
path: ['orchestrator', 'say'],
|
|
8
30
|
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:
|
|
31
|
+
positionals: [{ name: 'words', variadic: true, words: true, required: false }],
|
|
10
32
|
options: {
|
|
33
|
+
file: { type: 'string', multiple: true, value: '<path|->', description: 'Attach a file (repeatable); - streams standard input as a file.' },
|
|
34
|
+
'file-name': { type: 'string', value: '<name>', description: 'The name of the standard-input file (default stdin).' },
|
|
11
35
|
task: { type: 'string', value: '<task-id>', description: 'Point at a task (task_…).' },
|
|
12
36
|
report: { type: 'string', value: '<report-id>', description: 'Point at a report (rep_…).' },
|
|
13
37
|
'needs-you': { type: 'string', value: '<item-id>', description: 'Point at something that waits on you (perm_…, oq_…).' },
|
|
@@ -6,13 +6,17 @@
|
|
|
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
|
-
import { out, str } from "./commands.js";
|
|
14
|
+
import { listLimit, out, str } from "./commands.js";
|
|
12
15
|
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,59 @@ const idOf = (id) => {
|
|
|
198
202
|
return id;
|
|
199
203
|
};
|
|
200
204
|
export const ORCHESTRATOR_HANDLERS = {
|
|
205
|
+
async 'orchestrator capabilities'(ctx, values) {
|
|
206
|
+
const capabilities = await inScope(ctx, values, (client, scope) => client.orchestrator.capabilities({ ...orgOnly(scope), signal: ctx.signal }));
|
|
207
|
+
out(ctx, `input_files\t${capabilities.input_files}\n`, { capabilities });
|
|
208
|
+
return EXIT.ok;
|
|
209
|
+
},
|
|
210
|
+
async 'orchestrator show'(ctx, values, [id]) {
|
|
211
|
+
const detail = await inScope(ctx, values, (client, scope) => client.orchestrator.input(idOf(id), { ...orgOnly(scope), signal: ctx.signal }));
|
|
212
|
+
out(ctx, [`${detail.input.id}\t${stateLabel(detail.input)}`, resultText(detail.input), ...deviceOutcomeLines(detail.device_requests)].filter(Boolean).join('\n') + '\n', detail);
|
|
213
|
+
return EXIT.ok;
|
|
214
|
+
},
|
|
201
215
|
async 'orchestrator say'(ctx, values, words) {
|
|
202
216
|
const text = words.join(' ');
|
|
203
|
-
|
|
204
|
-
|
|
217
|
+
const paths = Array.isArray(values.file) ? values.file : typeof values.file === 'string' ? [values.file] : [];
|
|
218
|
+
if (!text.trim() && !paths.length)
|
|
219
|
+
throw new UsageError('missing <words> for "shard orchestrator say"');
|
|
205
220
|
const selection = selectionOf(values);
|
|
206
221
|
const wait = values.wait === true;
|
|
207
222
|
const timeoutMs = str(values.timeout) === undefined ? SAY_WAIT_MS : parseDuration(str(values.timeout), 'timeout');
|
|
208
223
|
if (!wait && str(values.timeout) !== undefined)
|
|
209
224
|
throw new UsageError('--timeout goes with --wait.');
|
|
210
225
|
return inScope(ctx, values, async (client, scope) => {
|
|
211
|
-
|
|
226
|
+
let spool;
|
|
227
|
+
let input;
|
|
228
|
+
const files = [];
|
|
229
|
+
try {
|
|
230
|
+
for (const path of paths) {
|
|
231
|
+
if (path !== '-') {
|
|
232
|
+
files.push({ path, name: basename(path) });
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (!spool) {
|
|
236
|
+
spool = await mkdtemp(join(ctx.io.env.HOME ?? tmpdir(), 'shard-input-'));
|
|
237
|
+
const sink = await open(join(spool, 'stdin'), 'wx', 0o600);
|
|
238
|
+
try {
|
|
239
|
+
for await (const chunk of ctx.io.stdin) {
|
|
240
|
+
ctx.signal.throwIfAborted();
|
|
241
|
+
await sink.writeFile(chunk);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
await sink.close();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
files.push({ path: join(spool, 'stdin'), name: str(values['file-name']) ?? 'stdin' });
|
|
249
|
+
}
|
|
250
|
+
input = await client.orchestrator.say({ text, files, ...orgOnly(scope), ...(selection ? { selection } : {}), signal: ctx.signal });
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
if (spool) {
|
|
254
|
+
await unlink(join(spool, 'stdin')).catch(() => undefined);
|
|
255
|
+
await rmdir(spool);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
212
258
|
if (!wait) {
|
|
213
259
|
out(ctx, `${input.id}\t${stateLabel(input)}\n`, { input });
|
|
214
260
|
return EXIT.ok;
|
|
@@ -242,6 +288,15 @@ export const ORCHESTRATOR_HANDLERS = {
|
|
|
242
288
|
out(ctx, human, { inputs: page.inputs ?? [], cursor: page.cursor });
|
|
243
289
|
return EXIT.ok;
|
|
244
290
|
},
|
|
291
|
+
async 'orchestrator history'(ctx, values) {
|
|
292
|
+
const limit = listLimit(values, 200);
|
|
293
|
+
const page = await inScope(ctx, values, (client, scope) => client.orchestrator.history({ ...orgOnly(scope), cursor: str(values.cursor), ...(limit !== undefined ? { limit } : {}), signal: ctx.signal }));
|
|
294
|
+
const lines = page.inputs.map(inputLine);
|
|
295
|
+
if (page.nextCursor)
|
|
296
|
+
lines.push(`Next page: shard orchestrator history --cursor ${page.nextCursor}`);
|
|
297
|
+
out(ctx, lines.length ? `${lines.join('\n')}\n` : 'No input history yet.\n', { inputs: page.inputs, next_cursor: page.nextCursor });
|
|
298
|
+
return EXIT.ok;
|
|
299
|
+
},
|
|
245
300
|
async 'orchestrator retry'(ctx, values, [id]) {
|
|
246
301
|
const input = await inScope(ctx, values, (client, scope) => client.orchestrator.retry(idOf(id), { ...orgOnly(scope), signal: ctx.signal }));
|
|
247
302
|
out(ctx, `${input.id}\t${stateLabel(input)}\n`, { input });
|
|
@@ -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 {};
|