@profoundlogic/coderflow-cli 0.13.10 → 0.13.11-dev.20260809021314.g778dadcc
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 +46 -0
- package/coder.js +10 -0
- package/lib/commands/export.js +126 -0
- package/lib/commands/import.js +200 -0
- package/lib/help.js +73 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -90,6 +90,52 @@ visibility.
|
|
|
90
90
|
When supplying a container ID directly, use the full ID or an unambiguous
|
|
91
91
|
prefix of at least 12 hexadecimal characters.
|
|
92
92
|
|
|
93
|
+
## Exporting and Importing Tasks
|
|
94
|
+
|
|
95
|
+
Export a task as a portable ZIP package:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
coder export <task-id>
|
|
99
|
+
coder export <task-id> --output ./task-backup.zip
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The package contains persisted task output and activity history, task metadata,
|
|
103
|
+
container logs, and a `/workspace` snapshot when the retained container is
|
|
104
|
+
available. Runtime-only server credential configuration is not included.
|
|
105
|
+
|
|
106
|
+
Import an export package or a plain local workspace directory:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Restore the task in the state it was exported in (default)
|
|
110
|
+
coder import ./task-backup.zip
|
|
111
|
+
|
|
112
|
+
# Package a directory and restore it into an environment
|
|
113
|
+
coder import ./my-workspace --environment=myproject
|
|
114
|
+
|
|
115
|
+
# Store the package as a reusable objective, or launch immediately
|
|
116
|
+
coder import ./task-backup.zip --mode=objective
|
|
117
|
+
coder import ./task-backup.zip --mode=running
|
|
118
|
+
```
|
|
119
|
+
|
|
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.
|
|
138
|
+
|
|
93
139
|
## Configuration and Profiles
|
|
94
140
|
|
|
95
141
|
Manage connection settings:
|
package/coder.js
CHANGED
|
@@ -20,6 +20,8 @@ import { attachToContainer } from './lib/commands/attach.js';
|
|
|
20
20
|
import { handleConfig } from './lib/commands/config.js';
|
|
21
21
|
import { handleProfile } from './lib/commands/profile.js';
|
|
22
22
|
import { runTest } from './lib/commands/test.js';
|
|
23
|
+
import { exportTask } from './lib/commands/export.js';
|
|
24
|
+
import { importTask } from './lib/commands/import.js';
|
|
23
25
|
import { setCliProfileOverride } from './lib/config.js';
|
|
24
26
|
import { readFileSync } from 'fs';
|
|
25
27
|
import { fileURLToPath } from 'url';
|
|
@@ -167,6 +169,14 @@ async function main() {
|
|
|
167
169
|
await runTest(args.slice(1));
|
|
168
170
|
break;
|
|
169
171
|
|
|
172
|
+
case 'export':
|
|
173
|
+
await exportTask(args.slice(1));
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case 'import':
|
|
177
|
+
await importTask(args.slice(1));
|
|
178
|
+
break;
|
|
179
|
+
|
|
170
180
|
case 'start':
|
|
171
181
|
await startInteractive(args.slice(1));
|
|
172
182
|
break;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: coder export - Download a portable task package.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createWriteStream, promises as fs } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { Readable } from 'node:stream';
|
|
9
|
+
import { pipeline } from 'node:stream/promises';
|
|
10
|
+
import { getApiKey, getServerUrl } from '../config.js';
|
|
11
|
+
import { buildHttpError } from '../http-client.js';
|
|
12
|
+
|
|
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) {
|
|
18
|
+
let taskId = null;
|
|
19
|
+
let output = null;
|
|
20
|
+
let workspace = null;
|
|
21
|
+
|
|
22
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
23
|
+
const arg = args[index];
|
|
24
|
+
if (arg === '--output' || arg === '-o') {
|
|
25
|
+
output = args[++index];
|
|
26
|
+
} else if (arg.startsWith('--output=')) {
|
|
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);
|
|
32
|
+
} else if (!arg.startsWith('-') && !taskId) {
|
|
33
|
+
taskId = arg;
|
|
34
|
+
} else {
|
|
35
|
+
throw new Error(`Unexpected export argument: ${arg}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
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(', ')}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getResponseFilename(response, taskId) {
|
|
81
|
+
const disposition = response.headers.get('content-disposition') || '';
|
|
82
|
+
const match = disposition.match(/filename="([^"]+)"/i);
|
|
83
|
+
return match?.[1] || `coderflow-task-${taskId}.zip`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function writeExportStream(source, destination) {
|
|
87
|
+
const directory = path.dirname(destination);
|
|
88
|
+
const temporaryPath = path.join(
|
|
89
|
+
directory,
|
|
90
|
+
`.coderflow-export-${process.pid}-${randomUUID()}.tmp`
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await pipeline(source, createWriteStream(temporaryPath, { flags: 'wx' }));
|
|
95
|
+
// Publishing with a hard link is atomic and fails with EEXIST instead of
|
|
96
|
+
// replacing a destination another process (or the user) already owns.
|
|
97
|
+
await fs.link(temporaryPath, destination);
|
|
98
|
+
} finally {
|
|
99
|
+
await fs.rm(temporaryPath, { force: true }).catch(() => {});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function exportTask(args = []) {
|
|
104
|
+
const { taskId, output, workspace } = parseExportArgs(args);
|
|
105
|
+
const serverUrl = await getServerUrl();
|
|
106
|
+
const apiKey = await getApiKey();
|
|
107
|
+
const query = workspace ? `?workspace=${encodeURIComponent(workspace)}` : '';
|
|
108
|
+
const response = await fetch(`${serverUrl}/tasks/${encodeURIComponent(taskId)}/export${query}`, {
|
|
109
|
+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
const contentType = response.headers.get('content-type') || '';
|
|
114
|
+
const body = contentType.includes('application/json')
|
|
115
|
+
? await response.json()
|
|
116
|
+
: await response.text();
|
|
117
|
+
throw buildHttpError(response.status, body);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const destination = path.resolve(output || getResponseFilename(response, taskId));
|
|
121
|
+
await fs.mkdir(path.dirname(destination), { recursive: true });
|
|
122
|
+
await writeExportStream(Readable.fromWeb(response.body), destination);
|
|
123
|
+
console.log(`Task ${taskId} exported to ${destination}`);
|
|
124
|
+
console.log(describeExportWorkspace(response.headers));
|
|
125
|
+
return destination;
|
|
126
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: coder import - Import a task export or local workspace directory.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createWriteStream, openAsBlob, promises as fs } from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { ZipArchive } from 'archiver';
|
|
9
|
+
import { getApiKey, getServerUrl } from '../config.js';
|
|
10
|
+
import { buildHttpError } from '../http-client.js';
|
|
11
|
+
|
|
12
|
+
function takeOption(args, index, name) {
|
|
13
|
+
const arg = args[index];
|
|
14
|
+
if (arg === `--${name}`) {
|
|
15
|
+
if (!args[index + 1]) throw new Error(`--${name} requires a value`);
|
|
16
|
+
return { value: args[index + 1], consumed: 2 };
|
|
17
|
+
}
|
|
18
|
+
if (arg.startsWith(`--${name}=`)) {
|
|
19
|
+
return { value: arg.slice(name.length + 3), consumed: 1 };
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
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
|
+
|
|
29
|
+
export function parseImportArgs(args) {
|
|
30
|
+
let source = null;
|
|
31
|
+
const options = { mode: 'preserve' };
|
|
32
|
+
const names = ['mode', 'environment', 'name', 'agent', 'instructions'];
|
|
33
|
+
|
|
34
|
+
for (let index = 0; index < args.length;) {
|
|
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
|
+
}
|
|
44
|
+
let matched = false;
|
|
45
|
+
for (const name of names) {
|
|
46
|
+
const parsed = takeOption(args, index, name);
|
|
47
|
+
if (!parsed) continue;
|
|
48
|
+
options[name] = parsed.value;
|
|
49
|
+
index += parsed.consumed;
|
|
50
|
+
matched = true;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
if (matched) continue;
|
|
54
|
+
if (!arg.startsWith('-') && !source) {
|
|
55
|
+
source = arg;
|
|
56
|
+
index += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`Unexpected import argument: ${arg}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!source) throw new Error('Path required. Usage: coder import <directory-or-zip> [options]');
|
|
63
|
+
if (!IMPORT_MODES.includes(options.mode)) {
|
|
64
|
+
throw new Error(`--mode must be one of ${IMPORT_MODES.join(', ')}`);
|
|
65
|
+
}
|
|
66
|
+
return { source, options };
|
|
67
|
+
}
|
|
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
|
+
|
|
135
|
+
async function zipDirectory(sourceDir) {
|
|
136
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'coderflow-cli-import-'));
|
|
137
|
+
const zipPath = path.join(tempDir, 'workspace.zip');
|
|
138
|
+
await new Promise((resolve, reject) => {
|
|
139
|
+
const output = createWriteStream(zipPath, { flags: 'wx' });
|
|
140
|
+
const archive = new ZipArchive({ zlib: { level: 6 } });
|
|
141
|
+
output.on('close', resolve);
|
|
142
|
+
output.on('error', reject);
|
|
143
|
+
archive.on('error', reject);
|
|
144
|
+
archive.pipe(output);
|
|
145
|
+
archive.directory(sourceDir, false);
|
|
146
|
+
archive.finalize().catch(reject);
|
|
147
|
+
});
|
|
148
|
+
return { zipPath, tempDir };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function importTask(args = []) {
|
|
152
|
+
const { source, options } = parseImportArgs(args);
|
|
153
|
+
const sourcePath = path.resolve(source);
|
|
154
|
+
const sourceStats = await fs.stat(sourcePath).catch(() => null);
|
|
155
|
+
if (!sourceStats) throw new Error(`Import path does not exist: ${sourcePath}`);
|
|
156
|
+
|
|
157
|
+
let archivePath = sourcePath;
|
|
158
|
+
let tempDir = null;
|
|
159
|
+
if (sourceStats.isDirectory()) {
|
|
160
|
+
console.log(`Packaging workspace directory ${sourcePath}...`);
|
|
161
|
+
({ zipPath: archivePath, tempDir } = await zipDirectory(sourcePath));
|
|
162
|
+
} else if (path.extname(sourcePath).toLowerCase() !== '.zip') {
|
|
163
|
+
throw new Error('Import path must be a directory or .zip file');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const form = new FormData();
|
|
168
|
+
form.append('archive', await openAsBlob(archivePath, { type: 'application/zip' }), path.basename(archivePath));
|
|
169
|
+
for (const [key, value] of Object.entries(options)) {
|
|
170
|
+
if (value != null && value !== '') form.append(key, value);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const serverUrl = await getServerUrl();
|
|
174
|
+
const apiKey = await getApiKey();
|
|
175
|
+
const response = await fetch(`${serverUrl}/tasks/import`, {
|
|
176
|
+
method: 'POST',
|
|
177
|
+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
|
|
178
|
+
body: form
|
|
179
|
+
});
|
|
180
|
+
const contentType = response.headers.get('content-type') || '';
|
|
181
|
+
const data = contentType.includes('application/json')
|
|
182
|
+
? await response.json()
|
|
183
|
+
: await response.text();
|
|
184
|
+
if (!response.ok) throw buildHttpError(response.status, data);
|
|
185
|
+
|
|
186
|
+
console.log(`Task import created ${data.status} ${data.taskId}`);
|
|
187
|
+
if (data.containerId) console.log(`Container: ${data.containerId}`);
|
|
188
|
+
if (data.queuePosition) console.log(`Queue position: ${data.queuePosition}`);
|
|
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);
|
|
196
|
+
return data;
|
|
197
|
+
} finally {
|
|
198
|
+
if (tempDir) await fs.rm(tempDir, { recursive: true, force: true });
|
|
199
|
+
}
|
|
200
|
+
}
|
package/lib/help.js
CHANGED
|
@@ -182,6 +182,73 @@ Examples:
|
|
|
182
182
|
coder logs 1759542727986-3uoyf48lr --tail=100
|
|
183
183
|
`,
|
|
184
184
|
|
|
185
|
+
export: `
|
|
186
|
+
Usage: coder export <task-id> [options]
|
|
187
|
+
|
|
188
|
+
Download a portable task package containing task output, logs, metadata, and
|
|
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.
|
|
195
|
+
|
|
196
|
+
Arguments:
|
|
197
|
+
task-id Task ID to export (required)
|
|
198
|
+
|
|
199
|
+
Options:
|
|
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.
|
|
204
|
+
|
|
205
|
+
Examples:
|
|
206
|
+
coder export 1759542727986-3uoyf48lr
|
|
207
|
+
coder export 1759542727986-3uoyf48lr --output ./task-backup.zip
|
|
208
|
+
coder export 1759542727986-3uoyf48lr --workspace=full
|
|
209
|
+
`,
|
|
210
|
+
|
|
211
|
+
import: `
|
|
212
|
+
Usage: coder import <directory-or-zip> [options]
|
|
213
|
+
|
|
214
|
+
Recreate a task from a CoderFlow export package, or create one from a plain
|
|
215
|
+
workspace directory. Plain directories are packaged before upload.
|
|
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
|
+
|
|
223
|
+
Arguments:
|
|
224
|
+
directory-or-zip Export zip or local workspace directory (required)
|
|
225
|
+
|
|
226
|
+
Options:
|
|
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.
|
|
231
|
+
--environment=<name> Override/select the destination environment
|
|
232
|
+
--name=<name> Override the imported task name
|
|
233
|
+
--agent=<agent> Override the imported/default agent
|
|
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.
|
|
243
|
+
|
|
244
|
+
Examples:
|
|
245
|
+
coder import ./task-backup.zip
|
|
246
|
+
coder import ./workspace --environment=hello --mode=staged
|
|
247
|
+
coder import ./task-backup.zip --mode=objective
|
|
248
|
+
coder import ./task-backup.zip --mode=completed
|
|
249
|
+
coder import ./task-backup.zip --mode=running --agent=codex
|
|
250
|
+
`,
|
|
251
|
+
|
|
185
252
|
list: `
|
|
186
253
|
Usage: coder list [options]
|
|
187
254
|
|
|
@@ -327,6 +394,8 @@ Commands:
|
|
|
327
394
|
coder apply [task-id] Apply patches from completed task to local repos
|
|
328
395
|
coder discard [--env=<environment>] [--yes] Discard applied changes from repos
|
|
329
396
|
coder attach [container-or-task-id] [options] Connect to a running container
|
|
397
|
+
coder export <task-id> [--workspace=delta|full] Export task contents and logs
|
|
398
|
+
coder import <directory-or-zip> [options] Import an objective or task container
|
|
330
399
|
|
|
331
400
|
Setup:
|
|
332
401
|
coder login [--sso] Authenticate with server
|
|
@@ -389,6 +458,8 @@ Task Management:
|
|
|
389
458
|
coder logs <task-id> [--tail=N] Show task logs
|
|
390
459
|
coder list [--status=...] [--environment=...] List tasks with optional filters
|
|
391
460
|
coder reject <task-id> [--cleanup] Reject task results
|
|
461
|
+
coder export <task-id> [--workspace=delta|full] Export task contents and logs
|
|
462
|
+
coder import <directory-or-zip> [options] Restore an objective or task
|
|
392
463
|
|
|
393
464
|
Interactive Sessions:
|
|
394
465
|
coder start <environment> [options] Start interactive session with AI agent
|
|
@@ -445,6 +516,8 @@ Task Management (Web UI alternative):
|
|
|
445
516
|
coder status 1759542727986-3uoyf48lr # Check task status
|
|
446
517
|
coder results 1759542727986-3uoyf48lr # Get task results
|
|
447
518
|
coder list --status=completed # List completed tasks
|
|
519
|
+
coder export 1759542727986-3uoyf48lr # Download a portable task package
|
|
520
|
+
coder import ./task-export.zip # Restore it in the state it was exported in
|
|
448
521
|
|
|
449
522
|
Interactive Sessions (Web UI alternative):
|
|
450
523
|
coder start hello # Start AI agent in environment
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profoundlogic/coderflow-cli",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.11-dev.20260809021314.g778dadcc",
|
|
4
4
|
"description": "AI Coder CLI - Command-line interface for managing AI coding tasks",
|
|
5
5
|
"main": "coder.js",
|
|
6
6
|
"type": "module",
|
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
29
29
|
"homepage": "https://coderflow.ai",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"ws": "^8.21.1",
|
|
32
31
|
"@inquirer/prompts": "^8.5.2",
|
|
33
|
-
"
|
|
32
|
+
"archiver": "^8.0.0",
|
|
33
|
+
"open": "^11.0.0",
|
|
34
|
+
"ws": "^8.21.1"
|
|
34
35
|
}
|
|
35
36
|
}
|