@dreamlake/dreamlake-cli 0.2.0 → 0.9.1
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/bin/dreamlake.js +30 -26
- package/package.json +19 -27
- package/README.md +0 -171
- package/dist/cli/auth/commands.js +0 -240
- package/dist/cli/auth/constants.js +0 -16
- package/dist/cli/auth/credentials.js +0 -157
- package/dist/cli/auth/device-flow.js +0 -134
- package/dist/cli/auth/device-secret.js +0 -34
- package/dist/cli/client.js +0 -99
- package/dist/cli/config.js +0 -81
- package/dist/cli/create/index.js +0 -204
- package/dist/cli/delete/index.js +0 -228
- package/dist/cli/download/index.js +0 -128
- package/dist/cli/glob.js +0 -45
- package/dist/cli/graphql-helpers.js +0 -42
- package/dist/cli/graphql.js +0 -47
- package/dist/cli/helpers.js +0 -106
- package/dist/cli/index.js +0 -95
- package/dist/cli/list/index.js +0 -254
- package/dist/cli/org/index.js +0 -348
- package/dist/cli/pipeline/index.js +0 -606
- package/dist/cli/progress.js +0 -65
- package/dist/cli/prompt.js +0 -17
- package/dist/cli/resources.js +0 -134
- package/dist/cli/target.js +0 -85
- package/dist/cli/team/index.js +0 -411
- package/dist/cli/update/index.js +0 -256
- package/dist/cli/upload/index.js +0 -263
- package/dist/cli/upload/kinds.js +0 -85
- package/dist/cli/upload/multipart.js +0 -211
- package/dist/cli/workflow/index.js +0 -627
package/dist/cli/create/index.js
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
// `dreamlake create bindr|dataset <name> --project <target> ...`.
|
|
2
|
-
// Port of dreamlake-py's cli/commands/create.py.
|
|
3
|
-
//
|
|
4
|
-
// bindr — members are EPISODES matched by `--episode <glob>` (node path)
|
|
5
|
-
// dataset — created empty; add bindrs later via `update dataset`
|
|
6
|
-
import { HttpError, requestJson } from "../client.js";
|
|
7
|
-
import { resolveNamespace, resolveRemote, resolveToken } from "../config.js";
|
|
8
|
-
import { fail, ok, splitCsv } from "../helpers.js";
|
|
9
|
-
import { confirm } from "../prompt.js";
|
|
10
|
-
import { fetchAllEpisodes, matchEpisodes, resolveProjectCtx, } from "../resources.js";
|
|
11
|
-
import { formatProject } from "../target.js";
|
|
12
|
-
export async function runCreateBindr(name, opts) {
|
|
13
|
-
if (!opts.project) {
|
|
14
|
-
fail("--project is required");
|
|
15
|
-
return 1;
|
|
16
|
-
}
|
|
17
|
-
const ctx = await resolveProjectCtx(opts.project);
|
|
18
|
-
if (!ctx)
|
|
19
|
-
return 1;
|
|
20
|
-
const { s, token, remote } = ctx;
|
|
21
|
-
const base = `/namespaces/${s.namespace}/projects/${s.project}/bindrs`;
|
|
22
|
-
let members = [];
|
|
23
|
-
if (opts.episode) {
|
|
24
|
-
const episodes = await fetchAllEpisodes(remote, s.namespace, s.project, token);
|
|
25
|
-
const matched = matchEpisodes(episodes, opts.episode);
|
|
26
|
-
if (matched.length === 0) {
|
|
27
|
-
fail(`no episodes match '${opts.episode}'`);
|
|
28
|
-
return 1;
|
|
29
|
-
}
|
|
30
|
-
process.stdout.write(`Matched ${matched.length} episode(s):\n`);
|
|
31
|
-
for (const ep of matched)
|
|
32
|
-
process.stdout.write(` ${ep.nodePath ?? ep.name ?? ""}\n`);
|
|
33
|
-
const proceed = await confirm(`Create bindr '${name}' with ${matched.length} episodes?`, false);
|
|
34
|
-
if (!proceed) {
|
|
35
|
-
process.stdout.write("Cancelled.\n");
|
|
36
|
-
return 0;
|
|
37
|
-
}
|
|
38
|
-
members = matched.map((ep) => ep.id);
|
|
39
|
-
}
|
|
40
|
-
const body = { name, members };
|
|
41
|
-
if (opts.description)
|
|
42
|
-
body.description = opts.description;
|
|
43
|
-
const tags = splitCsv(opts.tags);
|
|
44
|
-
if (tags.length)
|
|
45
|
-
body.tags = tags;
|
|
46
|
-
try {
|
|
47
|
-
await requestJson(remote, base, { method: "POST", token, json: body });
|
|
48
|
-
}
|
|
49
|
-
catch (err) {
|
|
50
|
-
if (err instanceof HttpError && err.status === 409) {
|
|
51
|
-
fail(`bindr '${name}' already exists in ${formatProject(s)}`);
|
|
52
|
-
return 1;
|
|
53
|
-
}
|
|
54
|
-
if (err instanceof HttpError && err.status === 404) {
|
|
55
|
-
fail(`project '${formatProject(s)}' not found`);
|
|
56
|
-
return 1;
|
|
57
|
-
}
|
|
58
|
-
fail(err.message);
|
|
59
|
-
return 1;
|
|
60
|
-
}
|
|
61
|
-
ok(`Created bindr: ${name} in ${formatProject(s)}`);
|
|
62
|
-
if (members.length)
|
|
63
|
-
process.stdout.write(` episodes: ${members.length}\n`);
|
|
64
|
-
if (opts.description)
|
|
65
|
-
process.stdout.write(` description: ${opts.description}\n`);
|
|
66
|
-
if (tags.length)
|
|
67
|
-
process.stdout.write(` tags: ${tags.join(", ")}\n`);
|
|
68
|
-
return 0;
|
|
69
|
-
}
|
|
70
|
-
export async function runCreateDataset(name, opts) {
|
|
71
|
-
if (!opts.project) {
|
|
72
|
-
fail("--project is required");
|
|
73
|
-
return 1;
|
|
74
|
-
}
|
|
75
|
-
const ctx = await resolveProjectCtx(opts.project);
|
|
76
|
-
if (!ctx)
|
|
77
|
-
return 1;
|
|
78
|
-
const { s, token, remote } = ctx;
|
|
79
|
-
const body = { name };
|
|
80
|
-
if (opts.description)
|
|
81
|
-
body.description = opts.description;
|
|
82
|
-
const tags = splitCsv(opts.tags);
|
|
83
|
-
if (tags.length)
|
|
84
|
-
body.tags = tags;
|
|
85
|
-
try {
|
|
86
|
-
await requestJson(remote, `/namespaces/${s.namespace}/projects/${s.project}/datasets`, {
|
|
87
|
-
method: "POST",
|
|
88
|
-
token,
|
|
89
|
-
json: body,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
catch (err) {
|
|
93
|
-
if (err instanceof HttpError && err.status === 409) {
|
|
94
|
-
fail(`dataset '${name}' already exists in ${formatProject(s)}`);
|
|
95
|
-
return 1;
|
|
96
|
-
}
|
|
97
|
-
if (err instanceof HttpError && err.status === 404) {
|
|
98
|
-
fail(`project '${formatProject(s)}' not found`);
|
|
99
|
-
return 1;
|
|
100
|
-
}
|
|
101
|
-
fail(err.message);
|
|
102
|
-
return 1;
|
|
103
|
-
}
|
|
104
|
-
ok(`Created dataset: ${name} in ${formatProject(s)}`);
|
|
105
|
-
if (opts.description)
|
|
106
|
-
process.stdout.write(` description: ${opts.description}\n`);
|
|
107
|
-
if (tags.length)
|
|
108
|
-
process.stdout.write(` tags: ${tags.join(", ")}\n`);
|
|
109
|
-
return 0;
|
|
110
|
-
}
|
|
111
|
-
export async function runCreateProject(name, opts) {
|
|
112
|
-
if (opts.visibility && opts.visibility !== "public" && opts.visibility !== "private") {
|
|
113
|
-
fail(`--visibility must be 'public' or 'private', got '${opts.visibility}'`);
|
|
114
|
-
return 1;
|
|
115
|
-
}
|
|
116
|
-
const remote = resolveRemote();
|
|
117
|
-
const token = resolveToken();
|
|
118
|
-
if (!token) {
|
|
119
|
-
fail("not authenticated. run 'dreamlake login' first");
|
|
120
|
-
return 1;
|
|
121
|
-
}
|
|
122
|
-
const namespace = await resolveNamespace(opts.namespace, { token, remote });
|
|
123
|
-
if (!namespace) {
|
|
124
|
-
fail("namespace not specified and no authenticated user found. run 'dreamlake login'");
|
|
125
|
-
return 1;
|
|
126
|
-
}
|
|
127
|
-
const body = { name };
|
|
128
|
-
if (opts.slug)
|
|
129
|
-
body.slug = opts.slug;
|
|
130
|
-
if (opts.description)
|
|
131
|
-
body.description = opts.description;
|
|
132
|
-
if (opts.visibility)
|
|
133
|
-
body.visibility = opts.visibility;
|
|
134
|
-
const tags = splitCsv(opts.tags);
|
|
135
|
-
if (tags.length)
|
|
136
|
-
body.tags = tags;
|
|
137
|
-
let created;
|
|
138
|
-
try {
|
|
139
|
-
created = await requestJson(remote, `/namespaces/${namespace}/projects`, {
|
|
140
|
-
method: "POST",
|
|
141
|
-
token,
|
|
142
|
-
json: body,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
catch (err) {
|
|
146
|
-
if (err instanceof HttpError && err.status === 409) {
|
|
147
|
-
fail(`project '${opts.slug ?? name}' already exists in namespace ${namespace}`);
|
|
148
|
-
return 1;
|
|
149
|
-
}
|
|
150
|
-
if (err instanceof HttpError && err.status === 404) {
|
|
151
|
-
fail(`namespace '${namespace}' not found`);
|
|
152
|
-
return 1;
|
|
153
|
-
}
|
|
154
|
-
fail(err.message);
|
|
155
|
-
return 1;
|
|
156
|
-
}
|
|
157
|
-
ok(`Created project: ${name} in namespace ${namespace}`);
|
|
158
|
-
process.stdout.write(` slug: ${created.slug ?? "?"} (use this in --project/--episode targets)\n`);
|
|
159
|
-
process.stdout.write(` id: ${created.id ?? "?"}\n`);
|
|
160
|
-
process.stdout.write(` visibility: ${created.visibility ?? "private"}\n`);
|
|
161
|
-
if (opts.description)
|
|
162
|
-
process.stdout.write(` description: ${opts.description}\n`);
|
|
163
|
-
if (tags.length)
|
|
164
|
-
process.stdout.write(` tags: ${tags.join(", ")}\n`);
|
|
165
|
-
return 0;
|
|
166
|
-
}
|
|
167
|
-
export function registerCreateCommand(program) {
|
|
168
|
-
const create = program.command("create").description("create a project, bindr, or dataset");
|
|
169
|
-
create
|
|
170
|
-
.command("project")
|
|
171
|
-
.description("create a project in a namespace (private by default)")
|
|
172
|
-
.argument("<name>", "project display name")
|
|
173
|
-
.option("--namespace <slug>", "namespace (default: your own)")
|
|
174
|
-
.option("--slug <slug>", "URL-safe slug (auto-derived from name if omitted)")
|
|
175
|
-
.option("--visibility <vis>", "'public' or 'private' (default: private)")
|
|
176
|
-
.option("--public", "shortcut for --visibility public")
|
|
177
|
-
.option("--description <text>", "description")
|
|
178
|
-
.option("--tags <list>", "comma-separated tags")
|
|
179
|
-
.action(async (name, opts) => {
|
|
180
|
-
const visibility = opts.public ? "public" : opts.visibility;
|
|
181
|
-
process.exit(await runCreateProject(name, { ...opts, visibility }));
|
|
182
|
-
});
|
|
183
|
-
create
|
|
184
|
-
.command("bindr")
|
|
185
|
-
.description("create a bindr (optionally seeded with episodes by glob)")
|
|
186
|
-
.argument("<name>", "bindr name")
|
|
187
|
-
.requiredOption("--project <target>", "project scope: space[@namespace]")
|
|
188
|
-
.option("--episode <glob>", "glob to match episodes by node path")
|
|
189
|
-
.option("--description <text>", "description")
|
|
190
|
-
.option("--tags <list>", "comma-separated tags")
|
|
191
|
-
.action(async (name, opts) => {
|
|
192
|
-
process.exit(await runCreateBindr(name, opts));
|
|
193
|
-
});
|
|
194
|
-
create
|
|
195
|
-
.command("dataset")
|
|
196
|
-
.description("create a dataset")
|
|
197
|
-
.argument("<name>", "dataset name")
|
|
198
|
-
.requiredOption("--project <target>", "project scope: space[@namespace]")
|
|
199
|
-
.option("--description <text>", "description")
|
|
200
|
-
.option("--tags <list>", "comma-separated tags")
|
|
201
|
-
.action(async (name, opts) => {
|
|
202
|
-
process.exit(await runCreateDataset(name, opts));
|
|
203
|
-
});
|
|
204
|
-
}
|
package/dist/cli/delete/index.js
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
// `dreamlake delete bindr|dataset <name> --project <target> [--yes]`.
|
|
2
|
-
// Port of dreamlake-py's cli/commands/delete.py.
|
|
3
|
-
import { HttpError, requestJson } from "../client.js";
|
|
4
|
-
import { resolveNamespace, resolveRemote, resolveToken } from "../config.js";
|
|
5
|
-
import { fail, ok } from "../helpers.js";
|
|
6
|
-
import { confirm } from "../prompt.js";
|
|
7
|
-
import { fetchAllEpisodes, fetchProjects, lookupNode, resolveProjectCtx, } from "../resources.js";
|
|
8
|
-
import { formatProject, parseTarget } from "../target.js";
|
|
9
|
-
async function runDelete(kind, name, opts) {
|
|
10
|
-
if (!opts.project) {
|
|
11
|
-
fail("--project is required");
|
|
12
|
-
return 1;
|
|
13
|
-
}
|
|
14
|
-
const ctx = await resolveProjectCtx(opts.project);
|
|
15
|
-
if (!ctx)
|
|
16
|
-
return 1;
|
|
17
|
-
const { s, token, remote } = ctx;
|
|
18
|
-
const proj = formatProject(s);
|
|
19
|
-
if (!opts.yes) {
|
|
20
|
-
const proceed = await confirm(`Delete ${kind} '${name}' from ${proj}?`, false);
|
|
21
|
-
if (!proceed) {
|
|
22
|
-
process.stdout.write("Cancelled.\n");
|
|
23
|
-
return 0;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
const collection = kind === "bindr" ? "bindrs" : "datasets";
|
|
27
|
-
try {
|
|
28
|
-
await requestJson(remote, `/namespaces/${s.namespace}/projects/${s.project}/${collection}/${name}`, { method: "DELETE", token });
|
|
29
|
-
}
|
|
30
|
-
catch (err) {
|
|
31
|
-
if (err instanceof HttpError && err.status === 404) {
|
|
32
|
-
fail(`${kind} '${name}' not found in ${proj}`);
|
|
33
|
-
return 1;
|
|
34
|
-
}
|
|
35
|
-
fail(err.message);
|
|
36
|
-
return 1;
|
|
37
|
-
}
|
|
38
|
-
ok(`Deleted ${kind}: ${name} from ${proj}`);
|
|
39
|
-
return 0;
|
|
40
|
-
}
|
|
41
|
-
export const runDeleteBindr = (name, opts) => runDelete("bindr", name, opts);
|
|
42
|
-
export const runDeleteDataset = (name, opts) => runDelete("dataset", name, opts);
|
|
43
|
-
// ─── node deletions (DELETE /nodes/:id — hard-deletes the subtree) ───
|
|
44
|
-
async function deleteNodeById(remote, token, nodeId) {
|
|
45
|
-
const res = await requestJson(remote, `/nodes/${nodeId}`, {
|
|
46
|
-
method: "DELETE",
|
|
47
|
-
token,
|
|
48
|
-
});
|
|
49
|
-
return res.deleted ?? 1;
|
|
50
|
-
}
|
|
51
|
-
export async function runDeleteProject(slug, opts) {
|
|
52
|
-
const remote = resolveRemote();
|
|
53
|
-
const token = resolveToken();
|
|
54
|
-
if (!token) {
|
|
55
|
-
fail("not authenticated. run 'dreamlake login' first");
|
|
56
|
-
return 1;
|
|
57
|
-
}
|
|
58
|
-
const namespace = await resolveNamespace(opts.namespace, { token, remote });
|
|
59
|
-
if (!namespace) {
|
|
60
|
-
fail("namespace not specified and no authenticated user found. run 'dreamlake login'");
|
|
61
|
-
return 1;
|
|
62
|
-
}
|
|
63
|
-
const projects = await fetchProjects(remote, token, namespace);
|
|
64
|
-
const proj = projects.find((p) => p.slug === slug);
|
|
65
|
-
if (!proj) {
|
|
66
|
-
fail(`project '${slug}' not found in namespace ${namespace}`);
|
|
67
|
-
return 1;
|
|
68
|
-
}
|
|
69
|
-
if (!opts.yes) {
|
|
70
|
-
const proceed = await confirm(`HARD-DELETE project '${slug}' (${namespace}) and EVERYTHING inside it (episodes, files, S3 objects)?`, false);
|
|
71
|
-
if (!proceed) {
|
|
72
|
-
process.stdout.write("Cancelled.\n");
|
|
73
|
-
return 0;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
try {
|
|
77
|
-
const deleted = await deleteNodeById(remote, token, proj.id);
|
|
78
|
-
ok(`Deleted project: ${slug} (${deleted} node(s) removed)`);
|
|
79
|
-
return 0;
|
|
80
|
-
}
|
|
81
|
-
catch (err) {
|
|
82
|
-
fail(err.message);
|
|
83
|
-
return 1;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
export async function runDeleteEpisode(name, opts) {
|
|
87
|
-
if (!opts.project) {
|
|
88
|
-
fail("--project is required");
|
|
89
|
-
return 1;
|
|
90
|
-
}
|
|
91
|
-
const ctx = await resolveProjectCtx(opts.project);
|
|
92
|
-
if (!ctx)
|
|
93
|
-
return 1;
|
|
94
|
-
const { s, token, remote } = ctx;
|
|
95
|
-
const proj = formatProject(s);
|
|
96
|
-
const episodes = await fetchAllEpisodes(remote, s.namespace, s.project, token);
|
|
97
|
-
const ep = episodes.find((e) => e.name === name);
|
|
98
|
-
if (!ep) {
|
|
99
|
-
fail(`episode '${name}' not found in ${proj}`);
|
|
100
|
-
return 1;
|
|
101
|
-
}
|
|
102
|
-
if (!opts.yes) {
|
|
103
|
-
const proceed = await confirm(`HARD-DELETE episode '${name}' from ${proj} and all files inside it?`, false);
|
|
104
|
-
if (!proceed) {
|
|
105
|
-
process.stdout.write("Cancelled.\n");
|
|
106
|
-
return 0;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
try {
|
|
110
|
-
const deleted = await deleteNodeById(remote, token, ep.id);
|
|
111
|
-
ok(`Deleted episode: ${name} from ${proj} (${deleted} node(s) removed)`);
|
|
112
|
-
return 0;
|
|
113
|
-
}
|
|
114
|
-
catch (err) {
|
|
115
|
-
fail(err.message);
|
|
116
|
-
return 1;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
export async function runDeleteFile(filePath, opts) {
|
|
120
|
-
if (!opts.episode) {
|
|
121
|
-
fail("--episode is required");
|
|
122
|
-
return 1;
|
|
123
|
-
}
|
|
124
|
-
let t;
|
|
125
|
-
try {
|
|
126
|
-
t = parseTarget(opts.episode);
|
|
127
|
-
}
|
|
128
|
-
catch (err) {
|
|
129
|
-
fail(err.message);
|
|
130
|
-
return 1;
|
|
131
|
-
}
|
|
132
|
-
const remote = resolveRemote();
|
|
133
|
-
const token = resolveToken();
|
|
134
|
-
if (!token) {
|
|
135
|
-
fail("not authenticated. run 'dreamlake login' first");
|
|
136
|
-
return 1;
|
|
137
|
-
}
|
|
138
|
-
const namespace = await resolveNamespace(t.namespace, { token, remote });
|
|
139
|
-
if (!namespace) {
|
|
140
|
-
fail("namespace not specified and no authenticated user found. run 'dreamlake login'");
|
|
141
|
-
return 1;
|
|
142
|
-
}
|
|
143
|
-
const assetPath = "/" + filePath.replace(/^\/+/, "");
|
|
144
|
-
let node;
|
|
145
|
-
try {
|
|
146
|
-
node = await lookupNode(remote, token, {
|
|
147
|
-
namespace,
|
|
148
|
-
project: t.project,
|
|
149
|
-
path: assetPath,
|
|
150
|
-
episode: t.episode,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
catch (err) {
|
|
154
|
-
if (err instanceof HttpError && err.status === 404) {
|
|
155
|
-
fail(`nothing found at ${assetPath}`);
|
|
156
|
-
return 1;
|
|
157
|
-
}
|
|
158
|
-
fail(err.message);
|
|
159
|
-
return 1;
|
|
160
|
-
}
|
|
161
|
-
if (!opts.yes) {
|
|
162
|
-
const what = node.kind === "folder" ? `folder '${assetPath}' and everything inside it` : `${node.kind} '${assetPath}'`;
|
|
163
|
-
const proceed = await confirm(`HARD-DELETE ${what}?`, false);
|
|
164
|
-
if (!proceed) {
|
|
165
|
-
process.stdout.write("Cancelled.\n");
|
|
166
|
-
return 0;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
try {
|
|
170
|
-
const deleted = await deleteNodeById(remote, token, node.id);
|
|
171
|
-
ok(`Deleted: ${assetPath} (${deleted} node(s) removed)`);
|
|
172
|
-
return 0;
|
|
173
|
-
}
|
|
174
|
-
catch (err) {
|
|
175
|
-
fail(err.message);
|
|
176
|
-
return 1;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
export function registerDeleteCommand(program) {
|
|
180
|
-
const del = program
|
|
181
|
-
.command("delete")
|
|
182
|
-
.description("delete a project, episode, file, bindr, or dataset");
|
|
183
|
-
del
|
|
184
|
-
.command("project")
|
|
185
|
-
.description("HARD-delete a project and its entire subtree (episodes, files, S3)")
|
|
186
|
-
.argument("<slug>", "project slug")
|
|
187
|
-
.option("--namespace <slug>", "namespace (default: your own)")
|
|
188
|
-
.option("--yes", "skip the confirmation prompt")
|
|
189
|
-
.action(async (slug, opts) => {
|
|
190
|
-
process.exit(await runDeleteProject(slug, opts));
|
|
191
|
-
});
|
|
192
|
-
del
|
|
193
|
-
.command("episode")
|
|
194
|
-
.description("HARD-delete an episode and all files inside it")
|
|
195
|
-
.argument("<name>", "episode name")
|
|
196
|
-
.requiredOption("--project <target>", "project scope: space[@namespace]")
|
|
197
|
-
.option("--yes", "skip the confirmation prompt")
|
|
198
|
-
.action(async (name, opts) => {
|
|
199
|
-
process.exit(await runDeleteEpisode(name, opts));
|
|
200
|
-
});
|
|
201
|
-
del
|
|
202
|
-
.command("file")
|
|
203
|
-
.description("HARD-delete a file (or folder, recursively) inside an episode")
|
|
204
|
-
.argument("<path>", "path within the episode, e.g. /camera/front/clip.mp4")
|
|
205
|
-
.requiredOption("--episode <target>", "episode scope: space[@namespace][:episode]")
|
|
206
|
-
.option("--yes", "skip the confirmation prompt")
|
|
207
|
-
.action(async (p, opts) => {
|
|
208
|
-
process.exit(await runDeleteFile(p, opts));
|
|
209
|
-
});
|
|
210
|
-
del
|
|
211
|
-
.command("bindr")
|
|
212
|
-
.description("delete a bindr")
|
|
213
|
-
.argument("<name>", "bindr name")
|
|
214
|
-
.requiredOption("--project <target>", "project scope: space[@namespace]")
|
|
215
|
-
.option("--yes", "skip the confirmation prompt")
|
|
216
|
-
.action(async (name, opts) => {
|
|
217
|
-
process.exit(await runDeleteBindr(name, opts));
|
|
218
|
-
});
|
|
219
|
-
del
|
|
220
|
-
.command("dataset")
|
|
221
|
-
.description("delete a dataset")
|
|
222
|
-
.argument("<name>", "dataset name")
|
|
223
|
-
.requiredOption("--project <target>", "project scope: space[@namespace]")
|
|
224
|
-
.option("--yes", "skip the confirmation prompt")
|
|
225
|
-
.action(async (name, opts) => {
|
|
226
|
-
process.exit(await runDeleteDataset(name, opts));
|
|
227
|
-
});
|
|
228
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
// `dreamlake download --episode <target> --from <path> [-o <out>]`.
|
|
2
|
-
//
|
|
3
|
-
// Node-based download (the current server protocol):
|
|
4
|
-
// GET /nodes/lookup?namespace&project&path&episode → node record
|
|
5
|
-
// leaf → GET /nodes/:id/download → presigned URL → stream to disk
|
|
6
|
-
// container → GET /nodes/:id/contents → download every file, preserving
|
|
7
|
-
// the relative tree under the output directory
|
|
8
|
-
//
|
|
9
|
-
// (The old /assets/video/download route this command originally ported from
|
|
10
|
-
// dreamlake-py's CLI no longer exists server-side; any kind downloads now.)
|
|
11
|
-
import { createWriteStream, mkdirSync } from "node:fs";
|
|
12
|
-
import path from "node:path";
|
|
13
|
-
import { Readable } from "node:stream";
|
|
14
|
-
import { pipeline } from "node:stream/promises";
|
|
15
|
-
import { HttpError, requestJson, requestRaw } from "../client.js";
|
|
16
|
-
import { resolveNamespace, resolveRemote, resolveToken } from "../config.js";
|
|
17
|
-
import { fail, ok } from "../helpers.js";
|
|
18
|
-
import { BytesProgress } from "../progress.js";
|
|
19
|
-
import { fetchAllContents, lookupNode, relPathUnder, } from "../resources.js";
|
|
20
|
-
import { formatTarget, parseTarget } from "../target.js";
|
|
21
|
-
const CONTAINER_KINDS = new Set(["folder", "episode", "project"]);
|
|
22
|
-
async function downloadNodeToFile(remote, token, nodeId, dest) {
|
|
23
|
-
const presigned = await requestJson(remote, `/nodes/${nodeId}/download`, { token });
|
|
24
|
-
const res = await requestRaw(presigned.url, {});
|
|
25
|
-
const total = Number(res.headers.get("content-length") ?? 0);
|
|
26
|
-
const progress = new BytesProgress(total);
|
|
27
|
-
const body = res.body;
|
|
28
|
-
if (!body)
|
|
29
|
-
throw new Error("empty response body");
|
|
30
|
-
mkdirSync(path.dirname(path.resolve(dest)), { recursive: true });
|
|
31
|
-
const nodeStream = Readable.fromWeb(body);
|
|
32
|
-
nodeStream.on("data", (chunk) => progress.advance(chunk.length));
|
|
33
|
-
await pipeline(nodeStream, createWriteStream(dest));
|
|
34
|
-
progress.finish();
|
|
35
|
-
}
|
|
36
|
-
export async function runDownload(opts) {
|
|
37
|
-
if (!opts.episode) {
|
|
38
|
-
fail("--episode is required");
|
|
39
|
-
return 1;
|
|
40
|
-
}
|
|
41
|
-
if (!opts.from) {
|
|
42
|
-
fail("--from is required");
|
|
43
|
-
return 1;
|
|
44
|
-
}
|
|
45
|
-
let t;
|
|
46
|
-
try {
|
|
47
|
-
t = parseTarget(opts.episode);
|
|
48
|
-
}
|
|
49
|
-
catch (err) {
|
|
50
|
-
fail(err.message);
|
|
51
|
-
return 1;
|
|
52
|
-
}
|
|
53
|
-
const remote = resolveRemote();
|
|
54
|
-
const token = resolveToken();
|
|
55
|
-
if (!token) {
|
|
56
|
-
fail("not authenticated. run 'dreamlake login' first");
|
|
57
|
-
return 1;
|
|
58
|
-
}
|
|
59
|
-
const namespace = await resolveNamespace(t.namespace, { token, remote });
|
|
60
|
-
if (!namespace) {
|
|
61
|
-
fail("namespace not specified and no authenticated user found. run 'dreamlake login'");
|
|
62
|
-
return 1;
|
|
63
|
-
}
|
|
64
|
-
t.namespace = namespace;
|
|
65
|
-
const assetPath = "/" + opts.from.replace(/^\/+/, "");
|
|
66
|
-
process.stdout.write(`Downloading ${assetPath}\n`);
|
|
67
|
-
process.stdout.write(` episode: ${formatTarget(t)}\n`);
|
|
68
|
-
// Resolve the node.
|
|
69
|
-
let node;
|
|
70
|
-
try {
|
|
71
|
-
node = await lookupNode(remote, token, {
|
|
72
|
-
namespace: t.namespace,
|
|
73
|
-
project: t.project,
|
|
74
|
-
path: assetPath,
|
|
75
|
-
episode: t.episode,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
catch (err) {
|
|
79
|
-
if (err instanceof HttpError && err.status === 404) {
|
|
80
|
-
fail(`asset not found at ${assetPath}`);
|
|
81
|
-
return 1;
|
|
82
|
-
}
|
|
83
|
-
fail(err.message);
|
|
84
|
-
return 1;
|
|
85
|
-
}
|
|
86
|
-
try {
|
|
87
|
-
// ── Single file ─────────────────────────────────────────────────
|
|
88
|
-
if (!CONTAINER_KINDS.has(node.kind)) {
|
|
89
|
-
const output = opts.output ?? path.basename(assetPath);
|
|
90
|
-
process.stdout.write(` output: ${output}\n`);
|
|
91
|
-
await downloadNodeToFile(remote, token, node.id, output);
|
|
92
|
-
ok(`Downloaded: ${output}`);
|
|
93
|
-
return 0;
|
|
94
|
-
}
|
|
95
|
-
// ── Container (folder / episode / project) — recursive ──────────
|
|
96
|
-
const outDir = opts.output ?? node.name;
|
|
97
|
-
const prefix = `${node.path ?? ""}${node.name},`;
|
|
98
|
-
const files = await fetchAllContents(remote, token, node.id);
|
|
99
|
-
if (files.length === 0) {
|
|
100
|
-
process.stdout.write(` (no files under ${node.kind} '${node.name}')\n`);
|
|
101
|
-
return 0;
|
|
102
|
-
}
|
|
103
|
-
process.stdout.write(` output: ${outDir}/ (${files.length} file(s) from ${node.kind} '${node.name}')\n`);
|
|
104
|
-
for (const f of files) {
|
|
105
|
-
const rel = relPathUnder(f, prefix);
|
|
106
|
-
const dest = path.join(outDir, rel);
|
|
107
|
-
process.stdout.write(` → ${rel}\n`);
|
|
108
|
-
await downloadNodeToFile(remote, token, f.id, dest);
|
|
109
|
-
}
|
|
110
|
-
ok(`Downloaded: ${outDir} (${files.length} files)`);
|
|
111
|
-
return 0;
|
|
112
|
-
}
|
|
113
|
-
catch (err) {
|
|
114
|
-
fail(err.message);
|
|
115
|
-
return 1;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
export function registerDownloadCommand(program) {
|
|
119
|
-
program
|
|
120
|
-
.command("download")
|
|
121
|
-
.description("download a file (or a whole folder/episode, recursively) from DreamLake")
|
|
122
|
-
.requiredOption("--episode <target>", "episode scope: space[@namespace][:episode]")
|
|
123
|
-
.requiredOption("--from <path>", "source path within the episode (file or folder)")
|
|
124
|
-
.option("-o, --output <path>", "output file/directory (default: derived from --from)")
|
|
125
|
-
.action(async (opts) => {
|
|
126
|
-
process.exit(await runDownload(opts));
|
|
127
|
-
});
|
|
128
|
-
}
|
package/dist/cli/glob.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
// Minimal fnmatch-style glob matching — zero-dependency replacement for
|
|
2
|
-
// Python's `fnmatch.fnmatch`, which dreamlake-py uses to match episodes
|
|
3
|
-
// by node path and bindrs by name.
|
|
4
|
-
//
|
|
5
|
-
// Supports `*` (any run), `?` (single char), and `[seq]` / `[!seq]`
|
|
6
|
-
// character classes. Case-sensitive (POSIX fnmatch semantics).
|
|
7
|
-
function translate(pattern) {
|
|
8
|
-
let re = "";
|
|
9
|
-
let i = 0;
|
|
10
|
-
while (i < pattern.length) {
|
|
11
|
-
const c = pattern[i++];
|
|
12
|
-
if (c === "*") {
|
|
13
|
-
re += ".*";
|
|
14
|
-
}
|
|
15
|
-
else if (c === "?") {
|
|
16
|
-
re += ".";
|
|
17
|
-
}
|
|
18
|
-
else if (c === "[") {
|
|
19
|
-
let j = i;
|
|
20
|
-
if (pattern[j] === "!")
|
|
21
|
-
j++;
|
|
22
|
-
if (pattern[j] === "]")
|
|
23
|
-
j++;
|
|
24
|
-
while (j < pattern.length && pattern[j] !== "]")
|
|
25
|
-
j++;
|
|
26
|
-
if (j >= pattern.length) {
|
|
27
|
-
re += "\\[";
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
let stuff = pattern.slice(i, j).replace(/\\/g, "\\\\");
|
|
31
|
-
i = j + 1;
|
|
32
|
-
if (stuff.startsWith("!"))
|
|
33
|
-
stuff = "^" + stuff.slice(1);
|
|
34
|
-
re += `[${stuff}]`;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
re += c.replace(/[.+^${}()|\\]/g, "\\$&");
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return new RegExp(`^${re}$`);
|
|
42
|
-
}
|
|
43
|
-
export function matchGlob(name, pattern) {
|
|
44
|
-
return translate(pattern).test(name);
|
|
45
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
// Shared GraphQL resolution helpers for org/team commands: turn the
|
|
2
|
-
// slugs / emails users type into the ids the GraphQL API wants.
|
|
3
|
-
import { gql } from "./graphql.js";
|
|
4
|
-
/** Resolve an org slug to {id,name}, or null if not found. */
|
|
5
|
-
export async function resolveOrg(slug) {
|
|
6
|
-
const data = await gql(`query($slug:String!){ organizationBySlug(slug:$slug){ id name } }`, { slug });
|
|
7
|
-
return data.organizationBySlug;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Resolve a user reference (email or name) to a userId via the global
|
|
11
|
-
* searchUsers query. Exact email match wins; multiple matches → error
|
|
12
|
-
* listing them; zero → error.
|
|
13
|
-
*/
|
|
14
|
-
export async function resolveUserId(ref) {
|
|
15
|
-
const data = await gql(`query($kw:String!){ searchUsers(keyword:$kw, limit:10){ id name email } }`, { kw: ref });
|
|
16
|
-
const users = data.searchUsers ?? [];
|
|
17
|
-
if (users.length === 0)
|
|
18
|
-
throw new Error(`no user matches '${ref}'`);
|
|
19
|
-
const exact = users.filter((u) => u.email?.toLowerCase() === ref.toLowerCase());
|
|
20
|
-
if (exact.length === 1)
|
|
21
|
-
return exact[0].id;
|
|
22
|
-
if (users.length > 1) {
|
|
23
|
-
const list = users.map((u) => ` ${u.email ?? "?"} (${u.name ?? "?"})`).join("\n");
|
|
24
|
-
throw new Error(`'${ref}' is ambiguous — ${users.length} matches. Use an exact email:\n${list}`);
|
|
25
|
-
}
|
|
26
|
-
return users[0].id;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Resolve (orgSlug, teamSlug) → team. Team slugs are unique only within an
|
|
30
|
-
* org, and the API has no teamBySlug query, so we list the org's teams and
|
|
31
|
-
* match by slug. Returns null if either the org or the team is not found.
|
|
32
|
-
*/
|
|
33
|
-
export async function resolveTeam(orgSlug, teamSlug) {
|
|
34
|
-
const org = await resolveOrg(orgSlug);
|
|
35
|
-
if (!org)
|
|
36
|
-
return null;
|
|
37
|
-
const data = await gql(`query($orgId:ID!){ organizationTeams(orgId:$orgId){ id slug name } }`, { orgId: org.id });
|
|
38
|
-
const team = (data.organizationTeams ?? []).find((t) => t.slug === teamSlug);
|
|
39
|
-
if (!team)
|
|
40
|
-
return null;
|
|
41
|
-
return { id: team.id, slug: team.slug, name: team.name, orgId: org.id };
|
|
42
|
-
}
|