@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 +32 -0
- package/coder.js +10 -0
- package/lib/commands/export.js +78 -0
- package/lib/commands/import.js +115 -0
- package/lib/help.js +46 -0
- package/package.json +4 -3
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.
|
|
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
|
-
"
|
|
32
|
+
"archiver": "^8.0.0",
|
|
33
|
+
"open": "^11.0.0",
|
|
34
|
+
"ws": "^8.21.1"
|
|
34
35
|
}
|
|
35
36
|
}
|