@dreamlake/dreamlake-cli 0.1.0
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/LICENSE +21 -0
- package/README.md +191 -0
- package/bin/dreamlake.js +41 -0
- package/dist/cli/auth/commands.js +240 -0
- package/dist/cli/auth/constants.js +16 -0
- package/dist/cli/auth/credentials.js +157 -0
- package/dist/cli/auth/device-flow.js +134 -0
- package/dist/cli/auth/device-secret.js +34 -0
- package/dist/cli/client.js +99 -0
- package/dist/cli/config.js +81 -0
- package/dist/cli/create/index.js +204 -0
- package/dist/cli/delete/index.js +228 -0
- package/dist/cli/download/index.js +128 -0
- package/dist/cli/glob.js +45 -0
- package/dist/cli/graphql-helpers.js +42 -0
- package/dist/cli/graphql.js +47 -0
- package/dist/cli/helpers.js +106 -0
- package/dist/cli/index.js +97 -0
- package/dist/cli/list/index.js +254 -0
- package/dist/cli/org/index.js +348 -0
- package/dist/cli/pipeline/index.js +481 -0
- package/dist/cli/progress.js +65 -0
- package/dist/cli/prompt.js +17 -0
- package/dist/cli/resources.js +134 -0
- package/dist/cli/target.js +85 -0
- package/dist/cli/team/index.js +411 -0
- package/dist/cli/update/index.js +256 -0
- package/dist/cli/upload/index.js +263 -0
- package/dist/cli/upload/kinds.js +85 -0
- package/dist/cli/upload/multipart.js +211 -0
- package/package.json +58 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// BSS S3 multipart upload with pause/resume, then registration in BSS and
|
|
2
|
+
// dreamlake-server. Faithful port of dreamlake-py's cli/commands/upload.py
|
|
3
|
+
// `_upload_file` (the 7-phase flow).
|
|
4
|
+
//
|
|
5
|
+
// Resume state lives at ~/.dreamlake/uploads/<hash>.json; on re-run we ask
|
|
6
|
+
// BSS which parts are already done (`.../parts-done`) and only upload the
|
|
7
|
+
// rest.
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { requestJson } from "../client.js";
|
|
13
|
+
import { humanSize } from "../helpers.js";
|
|
14
|
+
import { PartsProgress } from "../progress.js";
|
|
15
|
+
import { MIME_MAP, extOf } from "./kinds.js";
|
|
16
|
+
const CHUNK_SIZE = 10 * 1024 * 1024; // 10 MB per part (S3 min is 5 MB except last)
|
|
17
|
+
const MAX_WORKERS = 4; // parallel part uploads
|
|
18
|
+
// ─── resume state on disk ────────────────────────────────────────────
|
|
19
|
+
function statePath(rawHash) {
|
|
20
|
+
const dir = path.join(homedir(), ".dreamlake", "uploads");
|
|
21
|
+
mkdirSync(dir, { recursive: true });
|
|
22
|
+
return path.join(dir, `${rawHash}.json`);
|
|
23
|
+
}
|
|
24
|
+
function saveState(rawHash, state) {
|
|
25
|
+
writeFileSync(statePath(rawHash), JSON.stringify(state));
|
|
26
|
+
}
|
|
27
|
+
function loadState(rawHash) {
|
|
28
|
+
const p = statePath(rawHash);
|
|
29
|
+
if (!existsSync(p))
|
|
30
|
+
return null;
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function clearState(rawHash) {
|
|
39
|
+
const p = statePath(rawHash);
|
|
40
|
+
if (existsSync(p))
|
|
41
|
+
rmSync(p);
|
|
42
|
+
}
|
|
43
|
+
// ─── simple bounded-concurrency pool ─────────────────────────────────
|
|
44
|
+
async function pool(items, limit, worker) {
|
|
45
|
+
let idx = 0;
|
|
46
|
+
const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
47
|
+
while (idx < items.length) {
|
|
48
|
+
const current = items[idx++];
|
|
49
|
+
await worker(current);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
await Promise.all(runners);
|
|
53
|
+
}
|
|
54
|
+
// ─── the upload ──────────────────────────────────────────────────────
|
|
55
|
+
export async function uploadFile(filePath, t, destPath, kind, token, bss, remote) {
|
|
56
|
+
// Unified BSS route — all kinds go through /files (the old per-category
|
|
57
|
+
// routes /videos, /audio, ... no longer exist server-side).
|
|
58
|
+
const route = "files";
|
|
59
|
+
const filename = path.basename(filePath);
|
|
60
|
+
const contentType = MIME_MAP[extOf(filename)] ?? "application/octet-stream";
|
|
61
|
+
// Phase 1: hash + compute parts.
|
|
62
|
+
const content = readFileSync(filePath);
|
|
63
|
+
const rawHash = createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
64
|
+
const fileSize = content.length;
|
|
65
|
+
const totalParts = Math.max(1, Math.ceil(fileSize / CHUNK_SIZE));
|
|
66
|
+
process.stdout.write(` size: ${humanSize(fileSize)}\n`);
|
|
67
|
+
process.stdout.write(` parts: ${totalParts} x ${CHUNK_SIZE / 1024 / 1024} MB\n`);
|
|
68
|
+
let uploadId = null;
|
|
69
|
+
let key = null;
|
|
70
|
+
const completedMap = new Map();
|
|
71
|
+
// Resume from prior state if BSS still has the parts.
|
|
72
|
+
const prior = loadState(rawHash);
|
|
73
|
+
if (prior) {
|
|
74
|
+
try {
|
|
75
|
+
const done = await requestJson(bss, `/${route}/upload/multipart/parts-done`, { query: { uploadId: prior.uploadId, key: prior.key }, token });
|
|
76
|
+
if (!done.expired) {
|
|
77
|
+
uploadId = prior.uploadId;
|
|
78
|
+
key = prior.key;
|
|
79
|
+
for (const p of done.parts ?? [])
|
|
80
|
+
completedMap.set(p.partNumber, p);
|
|
81
|
+
const remaining = totalParts - completedMap.size;
|
|
82
|
+
process.stdout.write(` resuming: ${completedMap.size}/${totalParts} done, ${remaining} remaining\n`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// ignore — start fresh below
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Phase 2: init (unless resuming).
|
|
90
|
+
if (!uploadId) {
|
|
91
|
+
const init = await requestJson(bss, `/${route}/upload/multipart/init`, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
token,
|
|
94
|
+
json: {
|
|
95
|
+
owner: t.namespace,
|
|
96
|
+
project: t.project,
|
|
97
|
+
hash: rawHash,
|
|
98
|
+
contentType,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
uploadId = init.uploadId;
|
|
102
|
+
key = init.key;
|
|
103
|
+
saveState(rawHash, { uploadId, key, totalParts, completedParts: [] });
|
|
104
|
+
}
|
|
105
|
+
// Phase 3: presigned URLs for remaining parts.
|
|
106
|
+
const remainingParts = [];
|
|
107
|
+
for (let n = 1; n <= totalParts; n++)
|
|
108
|
+
if (!completedMap.has(n))
|
|
109
|
+
remainingParts.push(n);
|
|
110
|
+
let partUrls = {};
|
|
111
|
+
if (remainingParts.length > 0) {
|
|
112
|
+
const resp = await requestJson(bss, `/${route}/upload/multipart/parts`, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
token,
|
|
115
|
+
json: { uploadId, key, partNumbers: remainingParts },
|
|
116
|
+
});
|
|
117
|
+
partUrls = resp.parts;
|
|
118
|
+
}
|
|
119
|
+
// Phase 4: parallel chunk upload to S3 (presigned PUT, no auth header).
|
|
120
|
+
if (remainingParts.length > 0) {
|
|
121
|
+
const progress = new PartsProgress(totalParts, "uploading");
|
|
122
|
+
progress.advance(completedMap.size);
|
|
123
|
+
let failure = null;
|
|
124
|
+
await pool(remainingParts, MAX_WORKERS, async (partNumber) => {
|
|
125
|
+
if (failure)
|
|
126
|
+
return;
|
|
127
|
+
const start = (partNumber - 1) * CHUNK_SIZE;
|
|
128
|
+
const chunk = content.subarray(start, start + CHUNK_SIZE);
|
|
129
|
+
const url = partUrls[String(partNumber)];
|
|
130
|
+
try {
|
|
131
|
+
const res = await fetch(url, {
|
|
132
|
+
method: "PUT",
|
|
133
|
+
headers: { "Content-Type": contentType },
|
|
134
|
+
body: chunk,
|
|
135
|
+
signal: AbortSignal.timeout(300000),
|
|
136
|
+
});
|
|
137
|
+
if (!res.ok) {
|
|
138
|
+
throw new Error(`part ${partNumber} PUT failed (${res.status})`);
|
|
139
|
+
}
|
|
140
|
+
const etag = (res.headers.get("etag") ?? "").replace(/"/g, "");
|
|
141
|
+
completedMap.set(partNumber, { partNumber, etag });
|
|
142
|
+
saveState(rawHash, {
|
|
143
|
+
uploadId: uploadId,
|
|
144
|
+
key: key,
|
|
145
|
+
totalParts,
|
|
146
|
+
completedParts: [...completedMap.values()],
|
|
147
|
+
});
|
|
148
|
+
progress.advance();
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
failure = err;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
progress.done_();
|
|
155
|
+
if (failure) {
|
|
156
|
+
throw new Error(`${failure.message} — upload paused, re-run to resume`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Phase 5: complete.
|
|
160
|
+
const completedParts = [...completedMap.values()].sort((a, b) => a.partNumber - b.partNumber);
|
|
161
|
+
await requestJson(bss, `/${route}/upload/multipart/complete`, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
token,
|
|
164
|
+
timeoutMs: 60000,
|
|
165
|
+
json: { uploadId, key, parts: completedParts },
|
|
166
|
+
});
|
|
167
|
+
clearState(rawHash);
|
|
168
|
+
// Phase 6: register in BSS (unified POST /files — kind passes through).
|
|
169
|
+
const fullName = `/${destPath}/${filename}`.replace(/\/+/g, "/");
|
|
170
|
+
const bssBody = {
|
|
171
|
+
name: fullName,
|
|
172
|
+
owner: t.namespace,
|
|
173
|
+
project: t.project,
|
|
174
|
+
stagingHash: rawHash,
|
|
175
|
+
kind,
|
|
176
|
+
contentType,
|
|
177
|
+
size: fileSize,
|
|
178
|
+
originalName: filename,
|
|
179
|
+
};
|
|
180
|
+
if (kind === "text-track") {
|
|
181
|
+
const fmt = { ".vtt": "vtt", ".srt": "srt", ".jsonl": "jsonl" };
|
|
182
|
+
bssBody.metadata = { format: fmt[extOf(filename)] ?? "jsonl" };
|
|
183
|
+
}
|
|
184
|
+
const bssResult = await requestJson(bss, `/${route}`, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
token,
|
|
187
|
+
json: bssBody,
|
|
188
|
+
});
|
|
189
|
+
const bssId = bssResult.id ?? null;
|
|
190
|
+
// Phase 7: register in dreamlake-server (POST /nodes).
|
|
191
|
+
const nodeBody = {
|
|
192
|
+
namespace: t.namespace,
|
|
193
|
+
kind,
|
|
194
|
+
name: fullName,
|
|
195
|
+
project: t.project,
|
|
196
|
+
metadata: {
|
|
197
|
+
bssId,
|
|
198
|
+
hash: rawHash,
|
|
199
|
+
size: fileSize,
|
|
200
|
+
contentType,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
if (t.episode)
|
|
204
|
+
nodeBody.episode = t.episode;
|
|
205
|
+
const dlResult = await requestJson(remote, "/nodes", {
|
|
206
|
+
method: "POST",
|
|
207
|
+
token,
|
|
208
|
+
json: nodeBody,
|
|
209
|
+
});
|
|
210
|
+
return { nodeId: dlResult.id ?? null, bssId };
|
|
211
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dreamlake/dreamlake-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "dreamlake — data-warehouse CLI. The `dreamlake` command uploads/downloads assets and manages episodes, bindrs, and datasets against a DreamLake server + BSS.",
|
|
5
|
+
"private": false,
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"dreamlake": "bin/dreamlake.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"dist",
|
|
14
|
+
"!dist/**/*.map"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/dreamlake-ai/dreamlake-cli.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/dreamlake-ai/dreamlake-cli#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/dreamlake-ai/dreamlake-cli/issues"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"dreamlake",
|
|
29
|
+
"cli",
|
|
30
|
+
"data-warehouse",
|
|
31
|
+
"dataset",
|
|
32
|
+
"robotics"
|
|
33
|
+
],
|
|
34
|
+
"author": "DreamLake AI",
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc",
|
|
40
|
+
"cli": "tsx src/cli/index.ts",
|
|
41
|
+
"test": "tsc -p tsconfig.json --noEmit && node --import tsx --test src/cli/__tests__/*.test.ts",
|
|
42
|
+
"prepublishOnly": "npm test",
|
|
43
|
+
"prepack": "npm run build",
|
|
44
|
+
"docs:dev": "pnpm -C docs dev",
|
|
45
|
+
"docs:build": "pnpm -C docs build",
|
|
46
|
+
"docs:preview": "pnpm -C docs preview"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^22.10.0",
|
|
50
|
+
"tsx": "^4.19.0",
|
|
51
|
+
"typescript": "^5.7.0"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@inquirer/prompts": "^8.4.3",
|
|
55
|
+
"commander": "^12.1.0",
|
|
56
|
+
"yaml": "^2.6.1"
|
|
57
|
+
}
|
|
58
|
+
}
|