@profoundlogic/coderflow-cli 0.13.10 → 0.13.11-dev.20260807233136.g074f26b1

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 CHANGED
@@ -90,6 +90,38 @@ 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 a ready-to-use staged container (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 `objective`, `staged` (the default), and `running`. Use
121
+ `--environment`, `--name`, `--agent`, or `--instructions` to override package
122
+ metadata. In the Web UI, **Import** creates an objective from the Objectives
123
+ view and a staged task from the Tasks or Board view.
124
+
93
125
  ## Configuration and Profiles
94
126
 
95
127
  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,78 @@
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
+ function parseExportArgs(args) {
14
+ let taskId = null;
15
+ let output = null;
16
+
17
+ for (let index = 0; index < args.length; index += 1) {
18
+ const arg = args[index];
19
+ if (arg === '--output' || arg === '-o') {
20
+ output = args[++index];
21
+ } else if (arg.startsWith('--output=')) {
22
+ output = arg.slice('--output='.length);
23
+ } else if (!arg.startsWith('-') && !taskId) {
24
+ taskId = arg;
25
+ } else {
26
+ throw new Error(`Unexpected export argument: ${arg}`);
27
+ }
28
+ }
29
+
30
+ if (!taskId) throw new Error('Task ID required. Usage: coder export <task-id> [--output <file.zip>]');
31
+ return { taskId, output };
32
+ }
33
+
34
+ function getResponseFilename(response, taskId) {
35
+ const disposition = response.headers.get('content-disposition') || '';
36
+ const match = disposition.match(/filename="([^"]+)"/i);
37
+ return match?.[1] || `coderflow-task-${taskId}.zip`;
38
+ }
39
+
40
+ export async function writeExportStream(source, destination) {
41
+ const directory = path.dirname(destination);
42
+ const temporaryPath = path.join(
43
+ directory,
44
+ `.coderflow-export-${process.pid}-${randomUUID()}.tmp`
45
+ );
46
+
47
+ try {
48
+ await pipeline(source, createWriteStream(temporaryPath, { flags: 'wx' }));
49
+ // Publishing with a hard link is atomic and fails with EEXIST instead of
50
+ // replacing a destination another process (or the user) already owns.
51
+ await fs.link(temporaryPath, destination);
52
+ } finally {
53
+ await fs.rm(temporaryPath, { force: true }).catch(() => {});
54
+ }
55
+ }
56
+
57
+ export async function exportTask(args = []) {
58
+ const { taskId, output } = parseExportArgs(args);
59
+ const serverUrl = await getServerUrl();
60
+ const apiKey = await getApiKey();
61
+ const response = await fetch(`${serverUrl}/tasks/${encodeURIComponent(taskId)}/export`, {
62
+ headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
63
+ });
64
+
65
+ if (!response.ok) {
66
+ const contentType = response.headers.get('content-type') || '';
67
+ const body = contentType.includes('application/json')
68
+ ? await response.json()
69
+ : await response.text();
70
+ throw buildHttpError(response.status, body);
71
+ }
72
+
73
+ const destination = path.resolve(output || getResponseFilename(response, taskId));
74
+ await fs.mkdir(path.dirname(destination), { recursive: true });
75
+ await writeExportStream(Readable.fromWeb(response.body), destination);
76
+ console.log(`Task ${taskId} exported to ${destination}`);
77
+ return destination;
78
+ }
@@ -0,0 +1,115 @@
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
+ export function parseImportArgs(args) {
25
+ let source = null;
26
+ const options = { mode: 'staged' };
27
+ const names = ['mode', 'environment', 'name', 'agent', 'instructions'];
28
+
29
+ for (let index = 0; index < args.length;) {
30
+ const arg = args[index];
31
+ let matched = false;
32
+ for (const name of names) {
33
+ const parsed = takeOption(args, index, name);
34
+ if (!parsed) continue;
35
+ options[name] = parsed.value;
36
+ index += parsed.consumed;
37
+ matched = true;
38
+ break;
39
+ }
40
+ if (matched) continue;
41
+ if (!arg.startsWith('-') && !source) {
42
+ source = arg;
43
+ index += 1;
44
+ continue;
45
+ }
46
+ throw new Error(`Unexpected import argument: ${arg}`);
47
+ }
48
+
49
+ if (!source) throw new Error('Path required. Usage: coder import <directory-or-zip> [options]');
50
+ if (!['objective', 'staged', 'running'].includes(options.mode)) {
51
+ throw new Error('--mode must be objective, staged, or running');
52
+ }
53
+ return { source, options };
54
+ }
55
+
56
+ async function zipDirectory(sourceDir) {
57
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'coderflow-cli-import-'));
58
+ const zipPath = path.join(tempDir, 'workspace.zip');
59
+ await new Promise((resolve, reject) => {
60
+ const output = createWriteStream(zipPath, { flags: 'wx' });
61
+ const archive = new ZipArchive({ zlib: { level: 6 } });
62
+ output.on('close', resolve);
63
+ output.on('error', reject);
64
+ archive.on('error', reject);
65
+ archive.pipe(output);
66
+ archive.directory(sourceDir, false);
67
+ archive.finalize().catch(reject);
68
+ });
69
+ return { zipPath, tempDir };
70
+ }
71
+
72
+ export async function importTask(args = []) {
73
+ const { source, options } = parseImportArgs(args);
74
+ const sourcePath = path.resolve(source);
75
+ const sourceStats = await fs.stat(sourcePath).catch(() => null);
76
+ if (!sourceStats) throw new Error(`Import path does not exist: ${sourcePath}`);
77
+
78
+ let archivePath = sourcePath;
79
+ let tempDir = null;
80
+ if (sourceStats.isDirectory()) {
81
+ console.log(`Packaging workspace directory ${sourcePath}...`);
82
+ ({ zipPath: archivePath, tempDir } = await zipDirectory(sourcePath));
83
+ } else if (path.extname(sourcePath).toLowerCase() !== '.zip') {
84
+ throw new Error('Import path must be a directory or .zip file');
85
+ }
86
+
87
+ try {
88
+ const form = new FormData();
89
+ form.append('archive', await openAsBlob(archivePath, { type: 'application/zip' }), path.basename(archivePath));
90
+ for (const [key, value] of Object.entries(options)) {
91
+ if (value != null && value !== '') form.append(key, value);
92
+ }
93
+
94
+ const serverUrl = await getServerUrl();
95
+ const apiKey = await getApiKey();
96
+ const response = await fetch(`${serverUrl}/tasks/import`, {
97
+ method: 'POST',
98
+ headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
99
+ body: form
100
+ });
101
+ const contentType = response.headers.get('content-type') || '';
102
+ const data = contentType.includes('application/json')
103
+ ? await response.json()
104
+ : await response.text();
105
+ if (!response.ok) throw buildHttpError(response.status, data);
106
+
107
+ console.log(`Task import created ${data.status} ${data.taskId}`);
108
+ if (data.containerId) console.log(`Container: ${data.containerId}`);
109
+ if (data.queuePosition) console.log(`Queue position: ${data.queuePosition}`);
110
+ console.log(`Environment: ${data.environment}`);
111
+ return data;
112
+ } finally {
113
+ if (tempDir) await fs.rm(tempDir, { recursive: true, force: true });
114
+ }
115
+ }
package/lib/help.js CHANGED
@@ -182,6 +182,46 @@ 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 retained container workspace when it is available.
190
+
191
+ Arguments:
192
+ task-id Task ID to export (required)
193
+
194
+ Options:
195
+ --output, -o <file> Destination zip path (defaults to the server filename)
196
+
197
+ Examples:
198
+ coder export 1759542727986-3uoyf48lr
199
+ coder export 1759542727986-3uoyf48lr --output ./task-backup.zip
200
+ `,
201
+
202
+ import: `
203
+ Usage: coder import <directory-or-zip> [options]
204
+
205
+ Create a new objective or task from a CoderFlow export package or a plain
206
+ workspace directory. Plain directories are packaged before upload.
207
+
208
+ Arguments:
209
+ directory-or-zip Export zip or local workspace directory (required)
210
+
211
+ Options:
212
+ --mode=<mode> objective, staged, or running (default: staged)
213
+ --environment=<name> Override/select the destination environment
214
+ --name=<name> Override the imported task name
215
+ --agent=<agent> Override the imported/default agent
216
+ --instructions=<text> Override instructions used when the task runs
217
+
218
+ Examples:
219
+ coder import ./task-backup.zip
220
+ coder import ./workspace --environment=hello --mode=staged
221
+ coder import ./task-backup.zip --mode=objective
222
+ coder import ./task-backup.zip --mode=running --agent=codex
223
+ `,
224
+
185
225
  list: `
186
226
  Usage: coder list [options]
187
227
 
@@ -327,6 +367,8 @@ Commands:
327
367
  coder apply [task-id] Apply patches from completed task to local repos
328
368
  coder discard [--env=<environment>] [--yes] Discard applied changes from repos
329
369
  coder attach [container-or-task-id] [options] Connect to a running container
370
+ coder export <task-id> [--output=<file.zip>] Export task contents and logs
371
+ coder import <directory-or-zip> [options] Import an objective or task container
330
372
 
331
373
  Setup:
332
374
  coder login [--sso] Authenticate with server
@@ -389,6 +431,8 @@ Task Management:
389
431
  coder logs <task-id> [--tail=N] Show task logs
390
432
  coder list [--status=...] [--environment=...] List tasks with optional filters
391
433
  coder reject <task-id> [--cleanup] Reject task results
434
+ coder export <task-id> [--output=file.zip] Export task contents and logs
435
+ coder import <directory-or-zip> [options] Restore an objective or task
392
436
 
393
437
  Interactive Sessions:
394
438
  coder start <environment> [options] Start interactive session with AI agent
@@ -445,6 +489,8 @@ Task Management (Web UI alternative):
445
489
  coder status 1759542727986-3uoyf48lr # Check task status
446
490
  coder results 1759542727986-3uoyf48lr # Get task results
447
491
  coder list --status=completed # List completed tasks
492
+ coder export 1759542727986-3uoyf48lr # Download a portable task package
493
+ coder import ./task-export.zip --mode=staged # Restore into a staged container
448
494
 
449
495
  Interactive Sessions (Web UI alternative):
450
496
  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.10",
3
+ "version": "0.13.11-dev.20260807233136.g074f26b1",
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
- "open": "^11.0.0"
32
+ "archiver": "^8.0.0",
33
+ "open": "^11.0.0",
34
+ "ws": "^8.21.1"
34
35
  }
35
36
  }