Skip to content

Commit dcf5193

Browse files
johnxieclaude
andcommitted
feat: Add workspace export script (round-trip complete)
- npm run export: Pull any workspace into local JSON files - Splits SpaceBundleData into agents/, automations/, projects/, apps/ - Updates manifest.json with workspace name - Completes the import/export round-trip Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 810ba2f commit dcf5193

File tree

3 files changed

+110
-0
lines changed

3 files changed

+110
-0
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ npm run build
3232

3333
# 5. Import into Taskade
3434
TASKADE_API_TOKEN=tskdp_... TASKADE_WORKSPACE_ID=... npm run import
35+
36+
# Or export an existing workspace into this repo
37+
TASKADE_API_TOKEN=tskdp_... TASKADE_WORKSPACE_ID=... npm run export
3538
```
3639

3740
## Repo Structure
@@ -82,6 +85,7 @@ Each JSON file is the same data shape that Taskade's internal `SpaceBundleData`
8285

8386
| Command | What it does |
8487
|---------|-------------|
88+
| `npm run export` | Pull a workspace into local JSON files |
8589
| `npm run validate` | Validates all JSON files against Taskade schemas |
8690
| `npm run assemble` | Combines all items into `dist/workspace.json` |
8791
| `npm run build` | Assemble + validate (both steps) |

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"validate": "npx tsx scripts/validate.ts",
99
"assemble": "npx tsx scripts/assemble.ts",
1010
"import": "npx tsx scripts/import.ts",
11+
"export": "npx tsx scripts/export.ts",
1112
"build": "npm run assemble && npm run validate"
1213
},
1314
"devDependencies": {

scripts/export.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* Exports a Taskade workspace as individual DNA files (agents/, automations/, projects/, apps/).
3+
*
4+
* Prerequisites:
5+
* 1. Set TASKADE_API_TOKEN (Personal Access Token: tskdp_...)
6+
* 2. Set TASKADE_WORKSPACE_ID (source workspace ID)
7+
*
8+
* Usage: TASKADE_API_TOKEN=tskdp_... TASKADE_WORKSPACE_ID=... npx tsx scripts/export.ts
9+
*
10+
* API endpoint: GET /api/v1/bundles/:workspaceId/export
11+
*/
12+
13+
import * as fs from 'node:fs';
14+
import * as path from 'node:path';
15+
16+
const ROOT = path.resolve(import.meta.dirname, '..');
17+
const API_BASE = process.env.TASKADE_API_URL ?? 'https://taskade.com';
18+
const API_TOKEN = process.env.TASKADE_API_TOKEN;
19+
const WORKSPACE_ID = process.env.TASKADE_WORKSPACE_ID;
20+
21+
if (!API_TOKEN) {
22+
console.error('Error: TASKADE_API_TOKEN environment variable is required.');
23+
console.error('Create a Personal Access Token at https://taskade.com/settings/api');
24+
process.exit(1);
25+
}
26+
27+
if (!WORKSPACE_ID) {
28+
console.error('Error: TASKADE_WORKSPACE_ID environment variable is required.');
29+
console.error('Find your workspace ID in the workspace URL: https://taskade.com/ws/<workspace-id>');
30+
process.exit(1);
31+
}
32+
33+
console.log(`Exporting workspace ${WORKSPACE_ID}...`);
34+
35+
const url = `${API_BASE}/api/v1/bundles/${WORKSPACE_ID}/export`;
36+
37+
const response = await fetch(url, {
38+
headers: {
39+
Authorization: `Bearer ${API_TOKEN}`,
40+
},
41+
});
42+
43+
if (!response.ok) {
44+
const body = await response.text();
45+
console.error(`Export failed (${response.status}): ${body}`);
46+
process.exit(1);
47+
}
48+
49+
const result = (await response.json()) as { ok: boolean; item: { version: string; items: Record<string, any>; name?: string; description?: string } };
50+
const bundleData = result.item;
51+
52+
// Write raw bundle
53+
fs.mkdirSync(path.join(ROOT, 'dist'), { recursive: true });
54+
fs.writeFileSync(path.join(ROOT, 'dist', 'workspace.json'), JSON.stringify(bundleData, null, 2));
55+
console.log(`Saved dist/workspace.json`);
56+
57+
// Split into individual files by type
58+
const typeMap: Record<string, { dir: string; idField: string; dataField: string }> = {
59+
'space-bundle-agent-item': { dir: 'agents', idField: 'agentId', dataField: 'template' },
60+
'space-bundle-flow-item': { dir: 'automations', idField: 'flowId', dataField: 'template' },
61+
'space-bundle-project-item': { dir: 'projects', idField: 'projectId', dataField: 'root' },
62+
'space-bundle-app-item': { dir: 'apps', idField: 'appId', dataField: 'files' },
63+
};
64+
65+
let totalFiles = 0;
66+
67+
for (const [itemId, item] of Object.entries(bundleData.items)) {
68+
const mapping = typeMap[item.type];
69+
if (mapping == null) continue;
70+
71+
const dir = path.join(ROOT, mapping.dir);
72+
fs.mkdirSync(dir, { recursive: true });
73+
74+
const data = item[mapping.dataField];
75+
const fileName = `${itemId}.json`;
76+
fs.writeFileSync(path.join(dir, fileName), JSON.stringify(data, null, 2));
77+
totalFiles++;
78+
}
79+
80+
console.log(`\nExported ${totalFiles} items from workspace "${bundleData.name ?? WORKSPACE_ID}":`);
81+
82+
const counts: Record<string, number> = {};
83+
for (const item of Object.values(bundleData.items) as any[]) {
84+
const mapping = typeMap[item.type];
85+
if (mapping != null) {
86+
counts[mapping.dir] = (counts[mapping.dir] ?? 0) + 1;
87+
}
88+
}
89+
for (const [dir, count] of Object.entries(counts)) {
90+
console.log(` ${dir}/: ${count} files`);
91+
}
92+
93+
// Update manifest if workspace has a name
94+
if (bundleData.name != null) {
95+
const manifestPath = path.join(ROOT, 'manifest.json');
96+
if (fs.existsSync(manifestPath)) {
97+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
98+
manifest.name = bundleData.name;
99+
if (bundleData.description != null) {
100+
manifest.description = bundleData.description;
101+
}
102+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
103+
console.log(`\nUpdated manifest.json with workspace name: "${bundleData.name}"`);
104+
}
105+
}

0 commit comments

Comments
 (0)