@profoundlogic/coderflow-cli 0.13.11-dev.20260807233136.g074f26b1 → 0.13.11
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/README.md +19 -5
- package/lib/commands/export.js +53 -5
- package/lib/commands/import.js +88 -3
- package/lib/help.js +33 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,7 +106,7 @@ available. Runtime-only server credential configuration is not included.
|
|
|
106
106
|
Import an export package or a plain local workspace directory:
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
|
-
# Restore
|
|
109
|
+
# Restore the task in the state it was exported in (default)
|
|
110
110
|
coder import ./task-backup.zip
|
|
111
111
|
|
|
112
112
|
# Package a directory and restore it into an environment
|
|
@@ -117,10 +117,24 @@ coder import ./task-backup.zip --mode=objective
|
|
|
117
117
|
coder import ./task-backup.zip --mode=running
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
Import modes are `
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
Import modes are `preserve` (the default), `objective`, `staged`, `running`,
|
|
121
|
+
and `completed`. `preserve` reproduces the exported state: a finished task
|
|
122
|
+
imports as a finished task, an objective as an objective, and anything else —
|
|
123
|
+
including a plain workspace directory, which carries no state — as a staged
|
|
124
|
+
task. Use `--environment`, `--name`, `--agent`, or `--instructions` to override
|
|
125
|
+
package metadata. In the Web UI, **Import** creates an objective from the
|
|
126
|
+
Objectives view and preserves the exported state everywhere else.
|
|
127
|
+
|
|
128
|
+
A restored task keeps its transcript, its follow-up history, and its changed
|
|
129
|
+
files: the changed-files list is recomputed against the destination's own
|
|
130
|
+
repositories once the workspace is restored, so the Changed Files tab and the
|
|
131
|
+
approve dialog are populated as soon as the import finishes.
|
|
132
|
+
|
|
133
|
+
Continuing an imported conversation replays it rather than resuming it. An
|
|
134
|
+
agent session belongs to the container that created it, and the destination
|
|
135
|
+
container has never run the agent, so the first message after an import hands
|
|
136
|
+
the agent the imported transcript as context — the same mechanism a forked task
|
|
137
|
+
uses.
|
|
124
138
|
|
|
125
139
|
## Configuration and Profiles
|
|
126
140
|
|
package/lib/commands/export.js
CHANGED
|
@@ -10,9 +10,14 @@ import { pipeline } from 'node:stream/promises';
|
|
|
10
10
|
import { getApiKey, getServerUrl } from '../config.js';
|
|
11
11
|
import { buildHttpError } from '../http-client.js';
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
const WORKSPACE_MODES = ['delta', 'full', 'none'];
|
|
14
|
+
|
|
15
|
+
const USAGE = 'Usage: coder export <task-id> [--output <file.zip>] [--workspace delta|full|none]';
|
|
16
|
+
|
|
17
|
+
export function parseExportArgs(args) {
|
|
14
18
|
let taskId = null;
|
|
15
19
|
let output = null;
|
|
20
|
+
let workspace = null;
|
|
16
21
|
|
|
17
22
|
for (let index = 0; index < args.length; index += 1) {
|
|
18
23
|
const arg = args[index];
|
|
@@ -20,6 +25,10 @@ function parseExportArgs(args) {
|
|
|
20
25
|
output = args[++index];
|
|
21
26
|
} else if (arg.startsWith('--output=')) {
|
|
22
27
|
output = arg.slice('--output='.length);
|
|
28
|
+
} else if (arg === '--workspace') {
|
|
29
|
+
workspace = args[++index];
|
|
30
|
+
} else if (arg.startsWith('--workspace=')) {
|
|
31
|
+
workspace = arg.slice('--workspace='.length);
|
|
23
32
|
} else if (!arg.startsWith('-') && !taskId) {
|
|
24
33
|
taskId = arg;
|
|
25
34
|
} else {
|
|
@@ -27,8 +36,45 @@ function parseExportArgs(args) {
|
|
|
27
36
|
}
|
|
28
37
|
}
|
|
29
38
|
|
|
30
|
-
if (!taskId) throw new Error(
|
|
31
|
-
|
|
39
|
+
if (!taskId) throw new Error(`Task ID required. ${USAGE}`);
|
|
40
|
+
if (workspace != null && !WORKSPACE_MODES.includes(workspace)) {
|
|
41
|
+
throw new Error(`--workspace must be ${WORKSPACE_MODES.join(', ')}`);
|
|
42
|
+
}
|
|
43
|
+
return { taskId, output, workspace };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Describe what the server's capture actually carried.
|
|
48
|
+
*
|
|
49
|
+
* The archive body is a zip, so the capture summary travels in headers. An
|
|
50
|
+
* export that came back without a workspace says why, because "the delta was
|
|
51
|
+
* over the size cap" and "the task changed nothing" are not the same news.
|
|
52
|
+
*/
|
|
53
|
+
export function describeExportWorkspace(headers) {
|
|
54
|
+
const mode = headers.get('x-coderflow-workspace-mode') || 'none';
|
|
55
|
+
const reasonHeader = headers.get('x-coderflow-workspace-reason');
|
|
56
|
+
let reason = null;
|
|
57
|
+
if (reasonHeader) {
|
|
58
|
+
try {
|
|
59
|
+
reason = decodeURIComponent(reasonHeader);
|
|
60
|
+
} catch {
|
|
61
|
+
reason = reasonHeader;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (mode === 'none') {
|
|
66
|
+
return reason ? `Workspace: not included (${reason})` : 'Workspace: not included';
|
|
67
|
+
}
|
|
68
|
+
if (mode !== 'delta') {
|
|
69
|
+
return 'Workspace: full capture';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const files = Number(headers.get('x-coderflow-workspace-files')) || 0;
|
|
73
|
+
const bytes = Number(headers.get('x-coderflow-workspace-bytes')) || 0;
|
|
74
|
+
const deleted = Number(headers.get('x-coderflow-workspace-deleted')) || 0;
|
|
75
|
+
const parts = [`${files} changed file${files === 1 ? '' : 's'} (${bytes} bytes)`];
|
|
76
|
+
if (deleted > 0) parts.push(`${deleted} deleted path${deleted === 1 ? '' : 's'}`);
|
|
77
|
+
return `Workspace: delta - ${parts.join(', ')}`;
|
|
32
78
|
}
|
|
33
79
|
|
|
34
80
|
function getResponseFilename(response, taskId) {
|
|
@@ -55,10 +101,11 @@ export async function writeExportStream(source, destination) {
|
|
|
55
101
|
}
|
|
56
102
|
|
|
57
103
|
export async function exportTask(args = []) {
|
|
58
|
-
const { taskId, output } = parseExportArgs(args);
|
|
104
|
+
const { taskId, output, workspace } = parseExportArgs(args);
|
|
59
105
|
const serverUrl = await getServerUrl();
|
|
60
106
|
const apiKey = await getApiKey();
|
|
61
|
-
const
|
|
107
|
+
const query = workspace ? `?workspace=${encodeURIComponent(workspace)}` : '';
|
|
108
|
+
const response = await fetch(`${serverUrl}/tasks/${encodeURIComponent(taskId)}/export${query}`, {
|
|
62
109
|
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
|
|
63
110
|
});
|
|
64
111
|
|
|
@@ -74,5 +121,6 @@ export async function exportTask(args = []) {
|
|
|
74
121
|
await fs.mkdir(path.dirname(destination), { recursive: true });
|
|
75
122
|
await writeExportStream(Readable.fromWeb(response.body), destination);
|
|
76
123
|
console.log(`Task ${taskId} exported to ${destination}`);
|
|
124
|
+
console.log(describeExportWorkspace(response.headers));
|
|
77
125
|
return destination;
|
|
78
126
|
}
|
package/lib/commands/import.js
CHANGED
|
@@ -21,13 +21,26 @@ function takeOption(args, index, name) {
|
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// `preserve` is not a state of its own: it asks the server to land the task in
|
|
25
|
+
// whatever state it was exported from, which is what makes a round trip a round
|
|
26
|
+
// trip. The explicit modes force a state instead.
|
|
27
|
+
export const IMPORT_MODES = ['preserve', 'objective', 'staged', 'running', 'completed'];
|
|
28
|
+
|
|
24
29
|
export function parseImportArgs(args) {
|
|
25
30
|
let source = null;
|
|
26
|
-
const options = { mode: '
|
|
31
|
+
const options = { mode: 'preserve' };
|
|
27
32
|
const names = ['mode', 'environment', 'name', 'agent', 'instructions'];
|
|
28
33
|
|
|
29
34
|
for (let index = 0; index < args.length;) {
|
|
30
35
|
const arg = args[index];
|
|
36
|
+
// A delta records the base each repository came from. By default a
|
|
37
|
+
// differing base is reported and the import proceeds; this asks the server
|
|
38
|
+
// to refuse instead.
|
|
39
|
+
if (arg === '--require-matching-base') {
|
|
40
|
+
options.requireMatchingBase = 'true';
|
|
41
|
+
index += 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
31
44
|
let matched = false;
|
|
32
45
|
for (const name of names) {
|
|
33
46
|
const parsed = takeOption(args, index, name);
|
|
@@ -47,12 +60,78 @@ export function parseImportArgs(args) {
|
|
|
47
60
|
}
|
|
48
61
|
|
|
49
62
|
if (!source) throw new Error('Path required. Usage: coder import <directory-or-zip> [options]');
|
|
50
|
-
if (!
|
|
51
|
-
throw new Error(
|
|
63
|
+
if (!IMPORT_MODES.includes(options.mode)) {
|
|
64
|
+
throw new Error(`--mode must be one of ${IMPORT_MODES.join(', ')}`);
|
|
52
65
|
}
|
|
53
66
|
return { source, options };
|
|
54
67
|
}
|
|
55
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Say which state the import landed in, and — for a preserved import — that it
|
|
71
|
+
* was the package that decided.
|
|
72
|
+
*/
|
|
73
|
+
export function describeImportState(data = {}) {
|
|
74
|
+
const state = data.mode === 'objective' ? 'objective' : `${data.status} task`;
|
|
75
|
+
const preserved = data.requestedMode === 'preserve' && data.importedStatus
|
|
76
|
+
? ` (preserved from the exported ${data.importedStatus} task)`
|
|
77
|
+
: '';
|
|
78
|
+
return `Imported as ${state}${preserved}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* An imported conversation is replayed, not resumed: the destination container
|
|
83
|
+
* has never run this agent, so there is no session to attach to. Say so, or a
|
|
84
|
+
* user reading "completed" will expect `--continue` semantics they will not get.
|
|
85
|
+
*/
|
|
86
|
+
export function describeImportConversation(data = {}) {
|
|
87
|
+
if (data.conversationMode === 'history_replay') {
|
|
88
|
+
return 'Conversation: the imported transcript replays into a fresh agent session on your next message.';
|
|
89
|
+
}
|
|
90
|
+
if (data.conversationMode === 'instructions_replay') {
|
|
91
|
+
return 'Conversation: no transcript travelled with this package, so the next message replays the '
|
|
92
|
+
+ 'imported instructions and follow-up history into a fresh agent session.';
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Say what the import will do to the destination workspace: merge a delta into
|
|
99
|
+
* it, or replace it outright.
|
|
100
|
+
*/
|
|
101
|
+
export function describeImportWorkspace(data = {}) {
|
|
102
|
+
if (!data.workspaceIncluded) {
|
|
103
|
+
// "not included" alone reads as a task that changed nothing. If the export
|
|
104
|
+
// said why it carried no workspace, say it here too.
|
|
105
|
+
return data.workspaceUnavailableReason
|
|
106
|
+
? `Workspace: not included (${data.workspaceUnavailableReason})`
|
|
107
|
+
: 'Workspace: not included';
|
|
108
|
+
}
|
|
109
|
+
if (data.workspaceMode !== 'delta' || !data.workspaceDelta) {
|
|
110
|
+
return 'Workspace: full archive (replaces the initialized workspace)';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const { fileCount = 0, byteCount = 0, deletedPathCount = 0, repositories = [] } = data.workspaceDelta;
|
|
114
|
+
const parts = [`${fileCount} changed file${fileCount === 1 ? '' : 's'} (${byteCount} bytes)`];
|
|
115
|
+
if (deletedPathCount > 0) parts.push(`${deletedPathCount} deleted path${deletedPathCount === 1 ? '' : 's'}`);
|
|
116
|
+
if (repositories.length > 0) parts.push(`repos: ${repositories.join(', ')}`);
|
|
117
|
+
return `Workspace: delta merged into the initialized workspace - ${parts.join(', ')}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The base check cannot run before the server answers: it compares against
|
|
122
|
+
* repositories that only exist once the destination container has finished
|
|
123
|
+
* cloning them, which happens after the import is accepted. Say so, rather than
|
|
124
|
+
* letting a printed success imply the check already passed.
|
|
125
|
+
*/
|
|
126
|
+
export function describeBaseCheck(data = {}) {
|
|
127
|
+
if (!data.requireMatchingBase) return null;
|
|
128
|
+
if (!data.workspaceIncluded || data.workspaceMode !== 'delta') {
|
|
129
|
+
return 'Base match: requested, but this package carries no delta to check.';
|
|
130
|
+
}
|
|
131
|
+
return 'Base match: required. The check runs once the destination repositories are cloned; '
|
|
132
|
+
+ `if the base differs the import stops and task ${data.taskId} is marked failed.`;
|
|
133
|
+
}
|
|
134
|
+
|
|
56
135
|
async function zipDirectory(sourceDir) {
|
|
57
136
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'coderflow-cli-import-'));
|
|
58
137
|
const zipPath = path.join(tempDir, 'workspace.zip');
|
|
@@ -108,6 +187,12 @@ export async function importTask(args = []) {
|
|
|
108
187
|
if (data.containerId) console.log(`Container: ${data.containerId}`);
|
|
109
188
|
if (data.queuePosition) console.log(`Queue position: ${data.queuePosition}`);
|
|
110
189
|
console.log(`Environment: ${data.environment}`);
|
|
190
|
+
console.log(describeImportState(data));
|
|
191
|
+
console.log(describeImportWorkspace(data));
|
|
192
|
+
const conversation = describeImportConversation(data);
|
|
193
|
+
if (conversation) console.log(conversation);
|
|
194
|
+
const baseCheck = describeBaseCheck(data);
|
|
195
|
+
if (baseCheck) console.log(baseCheck);
|
|
111
196
|
return data;
|
|
112
197
|
} finally {
|
|
113
198
|
if (tempDir) await fs.rm(tempDir, { recursive: true, force: true });
|
package/lib/help.js
CHANGED
|
@@ -186,39 +186,66 @@ Examples:
|
|
|
186
186
|
Usage: coder export <task-id> [options]
|
|
187
187
|
|
|
188
188
|
Download a portable task package containing task output, logs, metadata, and
|
|
189
|
-
the
|
|
189
|
+
the workspace changes the task made.
|
|
190
|
+
|
|
191
|
+
By default the package carries a workspace *delta*: only the files the task
|
|
192
|
+
added or modified, plus the list of paths it deleted. Importing it merges that
|
|
193
|
+
into the destination environment's own initialized workspace, so dependencies
|
|
194
|
+
and untouched sources are neither shipped nor overwritten.
|
|
190
195
|
|
|
191
196
|
Arguments:
|
|
192
197
|
task-id Task ID to export (required)
|
|
193
198
|
|
|
194
199
|
Options:
|
|
195
200
|
--output, -o <file> Destination zip path (defaults to the server filename)
|
|
201
|
+
--workspace=<mode> delta (default), full, or none.
|
|
202
|
+
full archives the entire /workspace and replaces the
|
|
203
|
+
destination workspace on import; none omits it.
|
|
196
204
|
|
|
197
205
|
Examples:
|
|
198
206
|
coder export 1759542727986-3uoyf48lr
|
|
199
207
|
coder export 1759542727986-3uoyf48lr --output ./task-backup.zip
|
|
208
|
+
coder export 1759542727986-3uoyf48lr --workspace=full
|
|
200
209
|
`,
|
|
201
210
|
|
|
202
211
|
import: `
|
|
203
212
|
Usage: coder import <directory-or-zip> [options]
|
|
204
213
|
|
|
205
|
-
|
|
214
|
+
Recreate a task from a CoderFlow export package, or create one from a plain
|
|
206
215
|
workspace directory. Plain directories are packaged before upload.
|
|
207
216
|
|
|
217
|
+
By default the import reproduces the state the task was exported from: a
|
|
218
|
+
finished task comes back finished, with its transcript and its changed files,
|
|
219
|
+
ready to be continued. Its container has never run the agent, so the first
|
|
220
|
+
message replays the imported conversation into a fresh session rather than
|
|
221
|
+
resuming one.
|
|
222
|
+
|
|
208
223
|
Arguments:
|
|
209
224
|
directory-or-zip Export zip or local workspace directory (required)
|
|
210
225
|
|
|
211
226
|
Options:
|
|
212
|
-
--mode=<mode> objective, staged,
|
|
227
|
+
--mode=<mode> preserve (default), objective, staged, running, or
|
|
228
|
+
completed. preserve reproduces the exported state;
|
|
229
|
+
the others force one. A plain workspace directory has
|
|
230
|
+
no exported state, so preserve stages it.
|
|
213
231
|
--environment=<name> Override/select the destination environment
|
|
214
232
|
--name=<name> Override the imported task name
|
|
215
233
|
--agent=<agent> Override the imported/default agent
|
|
216
234
|
--instructions=<text> Override instructions used when the task runs
|
|
235
|
+
--require-matching-base
|
|
236
|
+
Refuse a workspace delta whose repositories were
|
|
237
|
+
exported from a different base commit, remote, or
|
|
238
|
+
branch. Without it the mismatch is reported and the
|
|
239
|
+
delta is applied anyway. The check needs the
|
|
240
|
+
destination's repositories, so it runs after the
|
|
241
|
+
import is accepted: a refusal shows up as a failed
|
|
242
|
+
task, not as a failed command.
|
|
217
243
|
|
|
218
244
|
Examples:
|
|
219
245
|
coder import ./task-backup.zip
|
|
220
246
|
coder import ./workspace --environment=hello --mode=staged
|
|
221
247
|
coder import ./task-backup.zip --mode=objective
|
|
248
|
+
coder import ./task-backup.zip --mode=completed
|
|
222
249
|
coder import ./task-backup.zip --mode=running --agent=codex
|
|
223
250
|
`,
|
|
224
251
|
|
|
@@ -367,7 +394,7 @@ Commands:
|
|
|
367
394
|
coder apply [task-id] Apply patches from completed task to local repos
|
|
368
395
|
coder discard [--env=<environment>] [--yes] Discard applied changes from repos
|
|
369
396
|
coder attach [container-or-task-id] [options] Connect to a running container
|
|
370
|
-
coder export <task-id> [--
|
|
397
|
+
coder export <task-id> [--workspace=delta|full] Export task contents and logs
|
|
371
398
|
coder import <directory-or-zip> [options] Import an objective or task container
|
|
372
399
|
|
|
373
400
|
Setup:
|
|
@@ -431,7 +458,7 @@ Task Management:
|
|
|
431
458
|
coder logs <task-id> [--tail=N] Show task logs
|
|
432
459
|
coder list [--status=...] [--environment=...] List tasks with optional filters
|
|
433
460
|
coder reject <task-id> [--cleanup] Reject task results
|
|
434
|
-
coder export <task-id> [--
|
|
461
|
+
coder export <task-id> [--workspace=delta|full] Export task contents and logs
|
|
435
462
|
coder import <directory-or-zip> [options] Restore an objective or task
|
|
436
463
|
|
|
437
464
|
Interactive Sessions:
|
|
@@ -490,7 +517,7 @@ Task Management (Web UI alternative):
|
|
|
490
517
|
coder results 1759542727986-3uoyf48lr # Get task results
|
|
491
518
|
coder list --status=completed # List completed tasks
|
|
492
519
|
coder export 1759542727986-3uoyf48lr # Download a portable task package
|
|
493
|
-
coder import ./task-export.zip
|
|
520
|
+
coder import ./task-export.zip # Restore it in the state it was exported in
|
|
494
521
|
|
|
495
522
|
Interactive Sessions (Web UI alternative):
|
|
496
523
|
coder start hello # Start AI agent in environment
|
package/package.json
CHANGED