@platforma-sdk/pl-cli 0.9.41 → 0.10.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/dist/cli.js +213 -88
- package/dist/cli.js.map +1 -1
- package/dist/cmd/maintenance/list-resources.d.ts +2 -0
- package/dist/cmd/maintenance/set-resource-error.d.ts +2 -0
- package/dist/cmd-opts.d.ts +2 -0
- package/dist/resource_id.d.ts +6 -0
- package/package.json +10 -5
- package/src/cli.ts +9 -0
- package/src/cmd/maintenance/list-resources.ts +148 -0
- package/src/cmd/maintenance/set-resource-error.ts +78 -0
- package/src/cmd-opts.ts +8 -3
- package/src/resource_id.ts +23 -0
package/dist/cli.js
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
|
-
import { _ as e, a as t, b as n, c as r, d as i, f as a, g as o, h as s, i as c, l, m as u, n as d, o as f, p, r as
|
|
2
|
-
import { Command as
|
|
3
|
-
import { field as C,
|
|
4
|
-
import { randomUUID as
|
|
5
|
-
import { ProjectMetaKey as
|
|
6
|
-
import { createInterface as
|
|
1
|
+
import { _ as e, a as t, b as n, c as r, d as i, f as a, g as o, h as s, i as c, l, m as u, n as d, o as f, p, r as ee, s as te, t as m, u as h, v as g, x as ne, y as _ } from "./output-BAuWMUzX.js";
|
|
2
|
+
import { Command as v, InvalidArgumentError as y, Option as b } from "commander";
|
|
3
|
+
import { asSignedResourceId as x, createSignedResourceId as S, field as C, isLocalResourceId as re, isNotNullSignedResourceId as w, resourceIdFromString as T, resourceIdToString as E, resourceType as D, resourceTypeToString as O } from "@milaboratories/pl-client";
|
|
4
|
+
import { randomUUID as k } from "node:crypto";
|
|
5
|
+
import { ProjectMetaKey as A } from "@milaboratories/pl-middle-layer";
|
|
6
|
+
import { createInterface as j } from "node:readline";
|
|
7
|
+
import { once as M } from "node:events";
|
|
7
8
|
//#region src/base_command.ts
|
|
8
|
-
async function
|
|
9
|
+
async function N(e) {
|
|
9
10
|
return e.adminUser && e.adminPassword ? n({
|
|
10
11
|
address: e.address,
|
|
11
12
|
adminUser: e.adminUser,
|
|
12
13
|
adminPassword: e.adminPassword
|
|
13
|
-
}) :
|
|
14
|
+
}) : ne({
|
|
14
15
|
address: e.address,
|
|
15
16
|
user: e.user,
|
|
16
17
|
password: e.password
|
|
17
18
|
});
|
|
18
19
|
}
|
|
19
|
-
async function
|
|
20
|
+
async function P(e) {
|
|
20
21
|
let t = !!e.adminUser, n = !!e.adminPassword, r = !!e.targetUser;
|
|
21
22
|
if (r && !(t && n)) throw Error("--target-user requires --admin-user and --admin-password");
|
|
22
23
|
if ((t || n) && !r) throw Error("--admin-user/--admin-password require --target-user for project commands");
|
|
23
|
-
let i = await
|
|
24
|
+
let i = await N(e), s;
|
|
24
25
|
return s = r ? (await o(i, e.targetUser)).projectListRid : await a(i), {
|
|
25
26
|
pl: i,
|
|
26
27
|
projectListRid: s
|
|
@@ -28,26 +29,26 @@ async function k(e) {
|
|
|
28
29
|
}
|
|
29
30
|
//#endregion
|
|
30
31
|
//#region src/cmd-opts.ts
|
|
31
|
-
function
|
|
32
|
+
function F(e, ...t) {
|
|
32
33
|
for (let n of t) for (let t of n) e.addOption(t);
|
|
33
34
|
return e;
|
|
34
35
|
}
|
|
35
|
-
var
|
|
36
|
-
new
|
|
37
|
-
new
|
|
38
|
-
new
|
|
36
|
+
var I = () => [new b("-a, --address <url>", "Platforma server address").env("PL_ADDRESS").makeOptionMandatory()], L = (e = "text", t = ["text", "json"]) => [...I(), new b("-f, --format <format>", "Output format").choices(t).default(e)], R = () => [new b("-u, --user <user>", "Username for authentication").env("PL_USER"), new b("-p, --password <password>", "Password for authentication").env("PL_PASSWORD")], z = () => [new b("--admin-user <user>", "Admin/controller username").env("PL_ADMIN_USER").makeOptionMandatory(), new b("--admin-password <password>", "Admin/controller password").env("PL_ADMIN_PASSWORD").makeOptionMandatory()], B = () => [
|
|
37
|
+
new b("--admin-user <user>", "Admin/controller username (enables admin mode)").env("PL_ADMIN_USER"),
|
|
38
|
+
new b("--admin-password <password>", "Admin/controller password").env("PL_ADMIN_PASSWORD"),
|
|
39
|
+
new b("--target-user <user>", "Operate on this user's data (requires admin credentials)").env("PL_TARGET_USER")
|
|
39
40
|
];
|
|
40
41
|
//#endregion
|
|
41
42
|
//#region src/cmd/project/list.ts
|
|
42
|
-
function
|
|
43
|
-
let e = new
|
|
44
|
-
return
|
|
45
|
-
let { pl: n, projectListRid: r } = await
|
|
43
|
+
function V() {
|
|
44
|
+
let e = new v("list").description("List all projects for the authenticated user.");
|
|
45
|
+
return F(e, L(), R(), B()), e.action(async (e) => {
|
|
46
|
+
let { pl: n, projectListRid: r } = await P(e);
|
|
46
47
|
try {
|
|
47
48
|
let i = await u(n, r);
|
|
48
49
|
if (e.format === "json") c(i.map((e) => ({
|
|
49
50
|
id: e.id,
|
|
50
|
-
rid:
|
|
51
|
+
rid: E(e.rid),
|
|
51
52
|
label: e.label,
|
|
52
53
|
created: e.created.toISOString(),
|
|
53
54
|
lastModified: e.lastModified.toISOString()
|
|
@@ -65,10 +66,10 @@ function F() {
|
|
|
65
66
|
"LAST MODIFIED"
|
|
66
67
|
], i.map((e) => [
|
|
67
68
|
e.id,
|
|
68
|
-
|
|
69
|
+
E(e.rid),
|
|
69
70
|
e.label,
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
m(e.created),
|
|
72
|
+
m(e.lastModified)
|
|
72
73
|
]))), t(`\n${i.length} project(s)`);
|
|
73
74
|
}
|
|
74
75
|
} finally {
|
|
@@ -78,15 +79,15 @@ function F() {
|
|
|
78
79
|
}
|
|
79
80
|
//#endregion
|
|
80
81
|
//#region src/cmd/project/info.ts
|
|
81
|
-
function
|
|
82
|
-
let e = new
|
|
83
|
-
return e.argument("<project>", "Project ID or label"),
|
|
84
|
-
let { pl: r, projectListRid: a } = await
|
|
82
|
+
function H() {
|
|
83
|
+
let e = new v("info").description("Show detailed information about a project.");
|
|
84
|
+
return e.argument("<project>", "Project ID or label"), F(e, L(), R(), B()), e.action(async (e, n) => {
|
|
85
|
+
let { pl: r, projectListRid: a } = await P(n);
|
|
85
86
|
try {
|
|
86
|
-
let { id: o, rid: s } = await
|
|
87
|
+
let { id: o, rid: s } = await _(r, a, e), l = await i(r, o, s);
|
|
87
88
|
if (n.format === "json") c({
|
|
88
89
|
id: l.id,
|
|
89
|
-
rid:
|
|
90
|
+
rid: E(l.rid),
|
|
90
91
|
label: l.label,
|
|
91
92
|
schemaVersion: l.schemaVersion,
|
|
92
93
|
blockCount: l.blockCount,
|
|
@@ -95,8 +96,8 @@ function I() {
|
|
|
95
96
|
lastModified: l.lastModified.toISOString()
|
|
96
97
|
});
|
|
97
98
|
else {
|
|
98
|
-
if (t(`Project: ${l.label}`), t(`ID: ${l.id}`), t(`RID: ${
|
|
99
|
-
t(`Created: ${
|
|
99
|
+
if (t(`Project: ${l.label}`), t(`ID: ${l.id}`), t(`RID: ${E(l.rid)}`), t(`Schema: ${l.schemaVersion ?? "(unknown)"}`), t(`Blocks: ${l.blockCount}`), l.blockIds.length > 0) for (let e of l.blockIds) t(` - ${e}`);
|
|
100
|
+
t(`Created: ${m(l.created)}`), t(`Last Modified: ${m(l.lastModified)}`);
|
|
100
101
|
}
|
|
101
102
|
} finally {
|
|
102
103
|
await r.close();
|
|
@@ -105,13 +106,13 @@ function I() {
|
|
|
105
106
|
}
|
|
106
107
|
//#endregion
|
|
107
108
|
//#region src/cmd/project/duplicate.ts
|
|
108
|
-
function
|
|
109
|
-
let e = new
|
|
110
|
-
return e.argument("<project>", "Project ID or label"),
|
|
111
|
-
let { pl: i, projectListRid: a } = await
|
|
109
|
+
function U() {
|
|
110
|
+
let e = new v("duplicate").description("Duplicate a project within the same user. Auto-renames on collision by default.");
|
|
111
|
+
return e.argument("<project>", "Project ID or label"), F(e, L(), R(), B()), e.option("-n, --name <name>", "Name for the duplicate"), e.addOption(new b("--auto-rename", "Auto-rename on collision (default: true)").default(!0)), e.option("--no-auto-rename", "Fail on name collision instead of auto-renaming"), e.action(async (e, n) => {
|
|
112
|
+
let { pl: i, projectListRid: a } = await P(n);
|
|
112
113
|
try {
|
|
113
|
-
let { rid: o } = await
|
|
114
|
-
let t = await e.getKValueString(o,
|
|
114
|
+
let { rid: o } = await _(i, a, e), s = k(), l = await i.withWriteTx("duplicateProject", async (e) => {
|
|
115
|
+
let t = await e.getKValueString(o, A), i = JSON.parse(t).label, c = await h(e, a), l;
|
|
115
116
|
if (n.name) {
|
|
116
117
|
if (!n.autoRename && c.includes(n.name)) throw Error(`Project name "${n.name}" already exists.`);
|
|
117
118
|
l = c.includes(n.name) ? f(n.name, c) : n.name;
|
|
@@ -120,14 +121,14 @@ function L() {
|
|
|
120
121
|
e.createField(C(a, s), "Dynamic", u), await e.commit();
|
|
121
122
|
let d = await u.globalId;
|
|
122
123
|
return {
|
|
123
|
-
id:
|
|
124
|
+
id: E(d),
|
|
124
125
|
rid: d,
|
|
125
126
|
label: l
|
|
126
127
|
};
|
|
127
128
|
});
|
|
128
129
|
n.format === "json" ? c({
|
|
129
130
|
id: l.id,
|
|
130
|
-
rid:
|
|
131
|
+
rid: E(l.rid),
|
|
131
132
|
label: l.label
|
|
132
133
|
}) : t(`Duplicated project as "${l.label}" (id: ${l.id})`);
|
|
133
134
|
} finally {
|
|
@@ -137,15 +138,15 @@ function L() {
|
|
|
137
138
|
}
|
|
138
139
|
//#endregion
|
|
139
140
|
//#region src/cmd/project/rename.ts
|
|
140
|
-
function
|
|
141
|
-
let e = new
|
|
142
|
-
return e.argument("<project>", "Project ID or label"),
|
|
143
|
-
let { pl: r, projectListRid: i } = await
|
|
141
|
+
function W() {
|
|
142
|
+
let e = new v("rename").description("Rename a project.");
|
|
143
|
+
return e.argument("<project>", "Project ID or label"), F(e, L(), R(), B()), e.addOption(new b("-n, --name <name>", "New name for the project").makeOptionMandatory()), e.action(async (e, n) => {
|
|
144
|
+
let { pl: r, projectListRid: i } = await P(n);
|
|
144
145
|
try {
|
|
145
|
-
let { id: a, rid: o } = await
|
|
146
|
-
await
|
|
146
|
+
let { id: a, rid: o } = await _(r, i, e);
|
|
147
|
+
await g(r, o, n.name), n.format === "json" ? c({
|
|
147
148
|
id: a,
|
|
148
|
-
rid:
|
|
149
|
+
rid: E(o),
|
|
149
150
|
label: n.name
|
|
150
151
|
}) : t(`Renamed project to "${n.name}"`);
|
|
151
152
|
} finally {
|
|
@@ -155,14 +156,14 @@ function R() {
|
|
|
155
156
|
}
|
|
156
157
|
//#endregion
|
|
157
158
|
//#region src/cmd/project/delete.ts
|
|
158
|
-
function
|
|
159
|
-
let e = new
|
|
160
|
-
return e.argument("<project>", "Project ID or label"),
|
|
161
|
-
let { pl: r, projectListRid: a } = await
|
|
159
|
+
function G() {
|
|
160
|
+
let e = new v("delete").description("Delete a project. This permanently destroys all project data.");
|
|
161
|
+
return e.argument("<project>", "Project ID or label"), F(e, L(), R(), B()), e.option("--force", "Skip confirmation", !1), e.action(async (e, n) => {
|
|
162
|
+
let { pl: r, projectListRid: a } = await P(n);
|
|
162
163
|
try {
|
|
163
|
-
let { id: o, rid: s, fieldName: l } = await
|
|
164
|
+
let { id: o, rid: s, fieldName: l } = await _(r, a, e), u = await i(r, o, s);
|
|
164
165
|
if (!n.force) {
|
|
165
|
-
let e =
|
|
166
|
+
let e = j({
|
|
166
167
|
input: process.stdin,
|
|
167
168
|
output: process.stderr
|
|
168
169
|
});
|
|
@@ -177,7 +178,7 @@ function z() {
|
|
|
177
178
|
e.close();
|
|
178
179
|
}
|
|
179
180
|
}
|
|
180
|
-
await
|
|
181
|
+
await te(r, a, l), n.format === "json" ? c({
|
|
181
182
|
deleted: !0,
|
|
182
183
|
id: o,
|
|
183
184
|
label: u.label
|
|
@@ -189,13 +190,13 @@ function z() {
|
|
|
189
190
|
}
|
|
190
191
|
//#endregion
|
|
191
192
|
//#region src/cmd/admin/copy-project.ts
|
|
192
|
-
function
|
|
193
|
-
let e = new
|
|
194
|
-
return
|
|
195
|
-
let n = await
|
|
193
|
+
function K() {
|
|
194
|
+
let e = new v("copy-project").description("Copy a project from one user to another. Requires admin/controller credentials.");
|
|
195
|
+
return F(e, L(), z()), e.addOption(new b("--source-user <user>", "Username of the source project owner").makeOptionMandatory()), e.addOption(new b("--source-project <project>", "Source project ID or label").makeOptionMandatory()), e.option("--target-user <user>", "Username of the target user (defaults to source-user for same-user copy)"), e.option("-n, --name <name>", "Name for the copied project"), e.addOption(new b("--auto-rename", "Auto-rename on collision (default: true)").default(!0)), e.option("--no-auto-rename", "Fail on name collision instead of auto-renaming"), e.action(async (e) => {
|
|
196
|
+
let n = await N(e);
|
|
196
197
|
try {
|
|
197
|
-
let i = e.targetUser ?? e.sourceUser, a = await o(n, e.sourceUser), { rid: s } = await
|
|
198
|
-
let n = await t.getKValueString(s,
|
|
198
|
+
let i = e.targetUser ?? e.sourceUser, a = await o(n, e.sourceUser), { rid: s } = await _(n, a.projectListRid, e.sourceProject), l = i === e.sourceUser ? a : await o(n, i), u = k(), d = await n.withWriteTx("adminCopyProject", async (t) => {
|
|
199
|
+
let n = await t.getKValueString(s, A), i = JSON.parse(n).label, a = await h(t, l.projectListRid), o = e.name ?? i;
|
|
199
200
|
if (a.includes(o)) {
|
|
200
201
|
if (!e.autoRename) throw Error(`Project name "${o}" already exists for target user.`);
|
|
201
202
|
o = f(e.name ?? i, a);
|
|
@@ -204,18 +205,18 @@ function B() {
|
|
|
204
205
|
t.createField(C(l.projectListRid, u), "Dynamic", c), await t.commit();
|
|
205
206
|
let d = await c.globalId;
|
|
206
207
|
return {
|
|
207
|
-
id:
|
|
208
|
+
id: E(d),
|
|
208
209
|
rid: d,
|
|
209
210
|
label: o
|
|
210
211
|
};
|
|
211
212
|
});
|
|
212
213
|
e.format === "json" ? c({
|
|
213
214
|
id: d.id,
|
|
214
|
-
rid:
|
|
215
|
+
rid: E(d.rid),
|
|
215
216
|
label: d.label,
|
|
216
217
|
sourceUser: e.sourceUser,
|
|
217
218
|
targetUser: i
|
|
218
|
-
}) : t(`Copied project from ${e.sourceUser} to ${i} as "${d.label}" (id: ${d.id}, rid: ${
|
|
219
|
+
}) : t(`Copied project from ${e.sourceUser} to ${i} as "${d.label}" (id: ${d.id}, rid: ${E(d.rid)})`);
|
|
219
220
|
} finally {
|
|
220
221
|
await n.close();
|
|
221
222
|
}
|
|
@@ -223,17 +224,17 @@ function B() {
|
|
|
223
224
|
}
|
|
224
225
|
//#endregion
|
|
225
226
|
//#region src/cmd/admin/user-list.ts
|
|
226
|
-
function
|
|
227
|
-
let e = new
|
|
228
|
-
return
|
|
227
|
+
function q() {
|
|
228
|
+
let e = new v("user-list").description("List all known users' logins from a live server. Requires admin/controller credentials.");
|
|
229
|
+
return F(e, L("csv", [
|
|
229
230
|
"text",
|
|
230
231
|
"json",
|
|
231
232
|
"csv"
|
|
232
|
-
]),
|
|
233
|
-
let n = await
|
|
233
|
+
]), z()), e.action(async (e) => {
|
|
234
|
+
let n = await N(e);
|
|
234
235
|
try {
|
|
235
236
|
let r = (await n.listUsers()).map((e) => e.login).sort();
|
|
236
|
-
e.format === "json" ? c(r) : e.format === "text" ? t(r.join("\n")) :
|
|
237
|
+
e.format === "json" ? c(r) : e.format === "text" ? t(r.join("\n")) : ee(["login"], r.map((e) => [e]));
|
|
237
238
|
} finally {
|
|
238
239
|
await n.close();
|
|
239
240
|
}
|
|
@@ -241,17 +242,17 @@ function V() {
|
|
|
241
242
|
}
|
|
242
243
|
//#endregion
|
|
243
244
|
//#region src/cmd/admin/delete-user.ts
|
|
244
|
-
function
|
|
245
|
-
let n = new
|
|
246
|
-
return n.argument("<user>", "Username of the account to delete"),
|
|
245
|
+
function J() {
|
|
246
|
+
let n = new v("delete-user").description("Delete a user account. Optionally re-homes the user's projects to another user first. Requires admin/controller credentials.");
|
|
247
|
+
return n.argument("<user>", "Username of the account to delete"), F(n, L(), z()), n.addOption(new b("--move-projects-to <user>", "Re-home the deleted user's projects to this user before deleting the account")), n.addOption(new b("--delete-projects", "Delete the user's projects along with the account, losing their data")), n.option("--force", "Skip confirmation", !1), n.action(async (n, r) => {
|
|
247
248
|
if (r.moveProjectsTo && r.deleteProjects) throw Error("--move-projects-to and --delete-projects are mutually exclusive");
|
|
248
249
|
if (r.moveProjectsTo === n) throw Error("--move-projects-to must name a different user than the one being deleted");
|
|
249
250
|
if (r.adminUser === n) throw Error(`Refusing to delete "${n}" — it is the account these credentials authenticate as.`);
|
|
250
|
-
let i = await
|
|
251
|
+
let i = await N(r);
|
|
251
252
|
try {
|
|
252
253
|
let a = await e(i, n), o = a.projectListRid === void 0 ? [] : await p(i, a.projectListRid);
|
|
253
254
|
if (o.length > 0 && !r.moveProjectsTo && !r.deleteProjects) throw Error(`User "${n}" owns ${o.length} project(s). Pass --move-projects-to <user> to re-home them, or --delete-projects to delete them with the account.`);
|
|
254
|
-
if (!await
|
|
255
|
+
if (!await ie(n, o, r)) {
|
|
255
256
|
t("Aborted.");
|
|
256
257
|
return;
|
|
257
258
|
}
|
|
@@ -261,7 +262,7 @@ function H() {
|
|
|
261
262
|
u = await s(i, a.projectListRid, t);
|
|
262
263
|
}
|
|
263
264
|
let d = a.projectListRid === void 0 ? [] : await p(i, a.projectListRid);
|
|
264
|
-
|
|
265
|
+
Y(n, r, o, d);
|
|
265
266
|
let f = await i.deleteUser(n);
|
|
266
267
|
r.format === "json" ? c({
|
|
267
268
|
deleted: !0,
|
|
@@ -273,13 +274,13 @@ function H() {
|
|
|
273
274
|
userRootDeleted: f.userRootDeleted,
|
|
274
275
|
revokedGrants: f.revokedGrants,
|
|
275
276
|
removedIdentityIndexEntries: f.removedIdentityIndexEntries
|
|
276
|
-
}) : t(
|
|
277
|
+
}) : t(ae(n, r.moveProjectsTo, u, d));
|
|
277
278
|
} finally {
|
|
278
279
|
await i.close();
|
|
279
280
|
}
|
|
280
281
|
}), n;
|
|
281
282
|
}
|
|
282
|
-
function
|
|
283
|
+
function Y(e, t, n, r) {
|
|
283
284
|
if (t.moveProjectsTo) {
|
|
284
285
|
if (r.length === 0) return;
|
|
285
286
|
throw Error(`User "${e}" still owns ${r.length} project(s) after the move: ${r.map((e) => e.label).join(", ")}. The account was NOT deleted — the projects were attached while the move ran. Re-run the command to move them too.`);
|
|
@@ -287,11 +288,11 @@ function U(e, t, n, r) {
|
|
|
287
288
|
let i = new Set(n.map((e) => e.id)), a = r.filter((e) => !i.has(e.id));
|
|
288
289
|
if (a.length !== 0) throw Error(`User "${e}" gained ${a.length} project(s) since the list you confirmed: ${a.map((e) => e.label).join(", ")}. The account was NOT deleted — deleting it now would destroy them unreviewed. Re-run the command to see the current list.`);
|
|
289
290
|
}
|
|
290
|
-
async function
|
|
291
|
+
async function ie(e, t, n) {
|
|
291
292
|
if (n.force) return !0;
|
|
292
293
|
let r = n.moveProjectsTo ? `${t.length} project(s) will move to "${n.moveProjectsTo}"` : t.length > 0 ? `${t.length} project(s) will be PERMANENTLY DELETED` : "the account owns no projects";
|
|
293
294
|
process.stderr.write(`Delete user "${e}"? ${r}.\n`), t.length > 0 && process.stderr.write(t.map((e) => ` - ${e.label}`).join("\n") + "\n");
|
|
294
|
-
let i =
|
|
295
|
+
let i = j({
|
|
295
296
|
input: process.stdin,
|
|
296
297
|
output: process.stderr
|
|
297
298
|
});
|
|
@@ -303,7 +304,7 @@ async function W(e, t, n) {
|
|
|
303
304
|
i.close();
|
|
304
305
|
}
|
|
305
306
|
}
|
|
306
|
-
function
|
|
307
|
+
function ae(e, t, n, r) {
|
|
307
308
|
let i = [];
|
|
308
309
|
return t ? n.length > 0 ? (i.push(`Moved ${n.length} project(s) from ${e} to ${t}:`), i.push(d([
|
|
309
310
|
"id",
|
|
@@ -316,19 +317,143 @@ function G(e, t, n, r) {
|
|
|
316
317
|
])))) : i.push(`User ${e} had no projects to move.`) : r.length > 0 && (i.push(`Deleted ${r.length} project(s) belonging to ${e}:`), i.push(r.map((e) => ` - ${e.label}`).join("\n"))), i.push(`Deleted user "${e}".`), i.join("\n");
|
|
317
318
|
}
|
|
318
319
|
//#endregion
|
|
320
|
+
//#region src/resource_id.ts
|
|
321
|
+
function X(e) {
|
|
322
|
+
let t = e.trim();
|
|
323
|
+
if (t.includes("|")) return x(t);
|
|
324
|
+
let n = T(t);
|
|
325
|
+
if (n === void 0 && /^\d+$/.test(t) && (n = BigInt(t)), n === void 0 || n === 0n || re(n)) throw Error(`not a global resource ID: ${e}`);
|
|
326
|
+
return S(n);
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/cmd/maintenance/list-resources.ts
|
|
330
|
+
function oe() {
|
|
331
|
+
let e = new v("list-resources").description("List resources of one type as JSON lines, in ascending order of resource ID. Resource data and key-value values are printed as JSON when they are valid JSON, as text when they are valid UTF-8, and as {\"base64\": ...} in other cases. Requires admin/controller credentials.");
|
|
332
|
+
return F(e, I(), z()), e.addOption(new b("--type <name:version>", "Resource type to list (e.g. 'Blob/work:1')").argParser(le).makeOptionMandatory()), e.addOption(new b("--limit <n>", "Maximum number of resources to list. 0 lists all resources").argParser(ue).default(0)), e.addOption(new b("--after <resource-id>", "List only resources with IDs greater than this ID. To get the next page, give the ID of the last resource of the previous page").argParser(X)), e.option("--load-fields", "Load the fields and the error of each resource ('fields' and 'error' keys)", !1), e.option("--load-kv", "Load the key-value entries of each resource ('kv' key)", !1), e.action(async (e) => {
|
|
333
|
+
let t = await N(e);
|
|
334
|
+
try {
|
|
335
|
+
let n = await t.withReadTx("listResources", async (t) => {
|
|
336
|
+
let n = await t.listResourcesByType(e.type, {
|
|
337
|
+
limit: e.limit,
|
|
338
|
+
after: e.after,
|
|
339
|
+
loadFields: e.loadFields
|
|
340
|
+
});
|
|
341
|
+
return await Promise.all(n.map(async (n) => ({
|
|
342
|
+
resource: n,
|
|
343
|
+
kv: e.loadKv ? await t.listKeyValues(n.id) : void 0
|
|
344
|
+
})));
|
|
345
|
+
});
|
|
346
|
+
for (let { resource: t, kv: r } of n) {
|
|
347
|
+
let n = JSON.stringify(se(t, e.loadFields, r)) + "\n";
|
|
348
|
+
process.stdout.write(n) || await M(process.stdout, "drain");
|
|
349
|
+
}
|
|
350
|
+
} finally {
|
|
351
|
+
await t.close();
|
|
352
|
+
}
|
|
353
|
+
}), e;
|
|
354
|
+
}
|
|
355
|
+
function se(e, t, n) {
|
|
356
|
+
return {
|
|
357
|
+
id: E(e.id),
|
|
358
|
+
type: O(e.type),
|
|
359
|
+
kind: e.kind,
|
|
360
|
+
originalId: Z(e.originalResourceId),
|
|
361
|
+
inputsLocked: e.inputsLocked,
|
|
362
|
+
outputsLocked: e.outputsLocked,
|
|
363
|
+
ready: e.resourceReady,
|
|
364
|
+
final: e.final,
|
|
365
|
+
data: e.data === void 0 || e.data.length === 0 ? null : Q(e.data),
|
|
366
|
+
...t ? {
|
|
367
|
+
error: Z(e.error),
|
|
368
|
+
fields: e.fields.map(ce)
|
|
369
|
+
} : {},
|
|
370
|
+
...n === void 0 ? {} : { kv: Object.fromEntries(n.map((e) => [e.key, Q(e.value)])) }
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function ce(e) {
|
|
374
|
+
return {
|
|
375
|
+
name: e.name,
|
|
376
|
+
type: e.type,
|
|
377
|
+
status: e.status,
|
|
378
|
+
value: Z(e.value),
|
|
379
|
+
error: Z(e.error),
|
|
380
|
+
valueIsFinal: e.valueIsFinal
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function Z(e) {
|
|
384
|
+
return w(e) ? E(e) : null;
|
|
385
|
+
}
|
|
386
|
+
function Q(e) {
|
|
387
|
+
let t;
|
|
388
|
+
try {
|
|
389
|
+
t = new TextDecoder("utf-8", { fatal: !0 }).decode(e);
|
|
390
|
+
} catch {
|
|
391
|
+
return { base64: Buffer.from(e).toString("base64") };
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
return JSON.parse(t);
|
|
395
|
+
} catch {
|
|
396
|
+
return t;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function le(e) {
|
|
400
|
+
let t = e.lastIndexOf(":");
|
|
401
|
+
if (t <= 0 || t === e.length - 1) throw new y(`expected <name>:<version>, e.g. 'Blob/work:1': ${e}`);
|
|
402
|
+
return D(e.slice(0, t), e.slice(t + 1));
|
|
403
|
+
}
|
|
404
|
+
function ue(e) {
|
|
405
|
+
let t = Number(e);
|
|
406
|
+
if (!Number.isInteger(t) || t < 0) throw new y(`not a non-negative integer: ${e}`);
|
|
407
|
+
return t;
|
|
408
|
+
}
|
|
409
|
+
//#endregion
|
|
410
|
+
//#region src/cmd/maintenance/set-resource-error.ts
|
|
411
|
+
var de = "Manual error set by administrator to trigger blocks re-run. Run block once again to get rid of it.";
|
|
412
|
+
function fe() {
|
|
413
|
+
let e = new v("set-resource-error").description("Put an error on one or more resources in a single transaction. Resources that already have an error are skipped. Requires admin/controller credentials.");
|
|
414
|
+
return e.argument("<resource-id...>", "Resource IDs: as printed by the server (NG:0x1A2B), as decimal numbers, or as signed IDs (<decimal ID>|<signature hex>)"), F(e, I(), z()), e.addOption(new b("-m, --error-message <message>", "Error message to put on the resources").default(de)), e.option("--skip-missing", "Skip resources that do not exist instead of failing the whole transaction", !1), e.action(async (e, n) => {
|
|
415
|
+
let r = e.map(X), i = await N(n);
|
|
416
|
+
try {
|
|
417
|
+
let e = await i.withWriteTx("setResourceError", async (e) => {
|
|
418
|
+
let t = await Promise.all(r.map(async (t) => ({
|
|
419
|
+
id: t,
|
|
420
|
+
data: n.skipMissing ? await e.getResourceDataIfExists(t, !0) : await e.getResourceData(t, !0, !0)
|
|
421
|
+
}))), i = t.filter((e) => e.data === void 0).map((e) => e.id), a = t.flatMap((e) => e.data === void 0 ? [] : [e.data]), o = a.filter((e) => !w(e.error)), s = a.filter((e) => w(e.error));
|
|
422
|
+
if (o.length > 0) {
|
|
423
|
+
let t = e.createError(n.errorMessage);
|
|
424
|
+
for (let n of o) e.setResourceError(n.id, t);
|
|
425
|
+
await e.commit();
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
targets: o,
|
|
429
|
+
skipped: s,
|
|
430
|
+
missing: i
|
|
431
|
+
};
|
|
432
|
+
});
|
|
433
|
+
for (let n of e.missing) t(`skipped ${E(n)}: resource does not exist`);
|
|
434
|
+
for (let n of e.skipped) t(`skipped ${E(n.id)}: resource already has an error`);
|
|
435
|
+
for (let n of e.targets) t(`error set ${E(n.id)}`);
|
|
436
|
+
} finally {
|
|
437
|
+
await i.close();
|
|
438
|
+
}
|
|
439
|
+
}), e;
|
|
440
|
+
}
|
|
441
|
+
//#endregion
|
|
319
442
|
//#region src/cli.ts
|
|
320
|
-
function
|
|
321
|
-
let e = new
|
|
443
|
+
function $() {
|
|
444
|
+
let e = new v();
|
|
322
445
|
e.name("pl-cli").description("Platforma command-line client");
|
|
323
|
-
let t = new
|
|
324
|
-
t.addCommand(
|
|
325
|
-
let n = new
|
|
326
|
-
|
|
446
|
+
let t = new v("project").description("Manage projects");
|
|
447
|
+
t.addCommand(V()), t.addCommand(H()), t.addCommand(U()), t.addCommand(W()), t.addCommand(G()), e.addCommand(t);
|
|
448
|
+
let n = new v("admin").description("Admin operations (requires controller credentials)");
|
|
449
|
+
n.addCommand(K()), n.addCommand(q()), n.addCommand(J()), e.addCommand(n);
|
|
450
|
+
let r = new v("maintenance").description("Low-level server state repair (requires admin/controller credentials)");
|
|
451
|
+
return r.addCommand(oe()), r.addCommand(fe()), e.addCommand(r), e;
|
|
327
452
|
}
|
|
328
|
-
async function
|
|
329
|
-
await
|
|
453
|
+
async function pe(e = process.argv) {
|
|
454
|
+
await $().parseAsync(e);
|
|
330
455
|
}
|
|
331
456
|
//#endregion
|
|
332
|
-
export {
|
|
457
|
+
export { $ as buildProgram, pe as run };
|
|
333
458
|
|
|
334
459
|
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../src/base_command.ts","../src/cmd-opts.ts","../src/cmd/project/list.ts","../src/cmd/project/info.ts","../src/cmd/project/duplicate.ts","../src/cmd/project/rename.ts","../src/cmd/project/delete.ts","../src/cmd/admin/copy-project.ts","../src/cmd/admin/user-list.ts","../src/cmd/admin/delete-user.ts","../src/cli.ts"],"sourcesContent":["import type { PlClient, SignedResourceId } from \"@milaboratories/pl-client\";\nimport { createPlConnection, createAdminPlConnection } from \"./connection\";\nimport { getProjectListRid, navigateToUserRoot } from \"./project_ops\";\n\n/**\n * Low-level: get an authenticated PlClient without resolving a project list.\n * Use this for commands that navigate to multiple users (e.g. admin copy-project).\n *\n * Callers own the returned client and must `await pl.close()` when done.\n */\nexport async function connectClient(flags: {\n address: string;\n user?: string;\n password?: string;\n adminUser?: string;\n adminPassword?: string;\n}): Promise<PlClient> {\n if (flags.adminUser && flags.adminPassword) {\n return createAdminPlConnection({\n address: flags.address,\n adminUser: flags.adminUser,\n adminPassword: flags.adminPassword,\n });\n }\n return createPlConnection({\n address: flags.address,\n user: flags.user,\n password: flags.password,\n });\n}\n\n/**\n * Connect and resolve the project list for a single user.\n * In admin mode (--admin-user + --admin-password + --target-user), operates on the target user's data.\n * In user mode, operates on the authenticated user's own data.\n *\n * Callers own the returned client and must `await pl.close()` when done.\n */\nexport async function connect(flags: {\n address: string;\n user?: string;\n password?: string;\n adminUser?: string;\n adminPassword?: string;\n targetUser?: string;\n}): Promise<{ pl: PlClient; projectListRid: SignedResourceId }> {\n const hasAdminUser = !!flags.adminUser;\n const hasAdminPassword = !!flags.adminPassword;\n const hasTarget = !!flags.targetUser;\n\n // Validate flag combinations\n if (hasTarget && !(hasAdminUser && hasAdminPassword)) {\n throw new Error(\"--target-user requires --admin-user and --admin-password\");\n }\n if ((hasAdminUser || hasAdminPassword) && !hasTarget) {\n throw new Error(\"--admin-user/--admin-password require --target-user for project commands\");\n }\n\n const pl = await connectClient(flags);\n\n let projectListRid: SignedResourceId;\n if (hasTarget) {\n const nav = await navigateToUserRoot(pl, flags.targetUser!);\n projectListRid = nav.projectListRid;\n } else {\n projectListRid = await getProjectListRid(pl);\n }\n\n return { pl, projectListRid };\n}\n","import { Command, Option } from \"commander\";\nimport type { OutputFormat } from \"./output\";\n\n/** Add one or more groups of options to a command, preserving group order. */\nexport function addOptions(cmd: Command, ...optionGroups: Option[][]): Command {\n for (const group of optionGroups) {\n for (const opt of group) cmd.addOption(opt);\n }\n return cmd;\n}\n\nexport const GlobalOptions = (\n formatDefault: OutputFormat = \"text\",\n allowedFormats: OutputFormat[] = [\"text\", \"json\"],\n): Option[] => [\n new Option(\"-a, --address <url>\", \"Platforma server address\")\n .env(\"PL_ADDRESS\")\n .makeOptionMandatory(),\n new Option(\"-f, --format <format>\", \"Output format\")\n .choices(allowedFormats)\n .default(formatDefault),\n];\n\nexport const UserAuthOptions = (): Option[] => [\n new Option(\"-u, --user <user>\", \"Username for authentication\").env(\"PL_USER\"),\n new Option(\"-p, --password <password>\", \"Password for authentication\").env(\"PL_PASSWORD\"),\n];\n\n/** Admin credentials only (for purely admin commands like copy-project). */\nexport const AdminAuthOptions = (): Option[] => [\n new Option(\"--admin-user <user>\", \"Admin/controller username\")\n .env(\"PL_ADMIN_USER\")\n .makeOptionMandatory(),\n new Option(\"--admin-password <password>\", \"Admin/controller password\")\n .env(\"PL_ADMIN_PASSWORD\")\n .makeOptionMandatory(),\n];\n\n/** Admin credentials + target user (for regular commands that can optionally operate on another user). */\nexport const AdminTargetOptions = (): Option[] => [\n new Option(\"--admin-user <user>\", \"Admin/controller username (enables admin mode)\").env(\n \"PL_ADMIN_USER\",\n ),\n new Option(\"--admin-password <password>\", \"Admin/controller password\").env(\"PL_ADMIN_PASSWORD\"),\n new Option(\n \"--target-user <user>\",\n \"Operate on this user's data (requires admin credentials)\",\n ).env(\"PL_TARGET_USER\"),\n];\n","import { Command } from \"commander\";\nimport { connect } from \"../../base_command\";\nimport { addOptions, GlobalOptions, UserAuthOptions, AdminTargetOptions } from \"../../cmd-opts\";\nimport { listProjects } from \"../../project_ops\";\nimport { formatTable, formatDate, outputJson, outputText } from \"../../output\";\nimport { resourceIdToString } from \"@milaboratories/pl-client\";\n\nexport default function projectListCommand(): Command {\n const cmd = new Command(\"list\").description(\"List all projects for the authenticated user.\");\n\n addOptions(cmd, GlobalOptions(), UserAuthOptions(), AdminTargetOptions());\n\n cmd.action(async (flags) => {\n const { pl, projectListRid } = await connect(flags);\n try {\n const projects = await listProjects(pl, projectListRid);\n\n if (flags.format === \"json\") {\n outputJson(\n projects.map((p) => ({\n id: p.id,\n rid: resourceIdToString(p.rid),\n label: p.label,\n created: p.created.toISOString(),\n lastModified: p.lastModified.toISOString(),\n })),\n );\n } else {\n if (projects.length === 0) {\n outputText(\"No projects found.\");\n return;\n }\n\n outputText(\n formatTable(\n [\"ID\", \"RID\", \"LABEL\", \"CREATED\", \"LAST MODIFIED\"],\n projects.map((p) => [\n p.id,\n resourceIdToString(p.rid),\n p.label,\n formatDate(p.created),\n formatDate(p.lastModified),\n ]),\n ),\n );\n outputText(`\\n${projects.length} project(s)`);\n }\n } finally {\n await pl.close();\n }\n });\n\n return cmd;\n}\n","import { Command } from \"commander\";\nimport { connect } from \"../../base_command\";\nimport { addOptions, GlobalOptions, UserAuthOptions, AdminTargetOptions } from \"../../cmd-opts\";\nimport { resolveProject, getProjectInfo } from \"../../project_ops\";\nimport { formatDate, outputJson, outputText } from \"../../output\";\nimport { resourceIdToString } from \"@milaboratories/pl-client\";\n\nexport default function projectInfoCommand(): Command {\n const cmd = new Command(\"info\").description(\"Show detailed information about a project.\");\n\n cmd.argument(\"<project>\", \"Project ID or label\");\n addOptions(cmd, GlobalOptions(), UserAuthOptions(), AdminTargetOptions());\n\n cmd.action(async (project: string, flags) => {\n const { pl, projectListRid } = await connect(flags);\n try {\n const { id, rid } = await resolveProject(pl, projectListRid, project);\n const info = await getProjectInfo(pl, id, rid);\n\n if (flags.format === \"json\") {\n outputJson({\n id: info.id,\n rid: resourceIdToString(info.rid),\n label: info.label,\n schemaVersion: info.schemaVersion,\n blockCount: info.blockCount,\n blockIds: info.blockIds,\n created: info.created.toISOString(),\n lastModified: info.lastModified.toISOString(),\n });\n } else {\n outputText(`Project: ${info.label}`);\n outputText(`ID: ${info.id}`);\n outputText(`RID: ${resourceIdToString(info.rid)}`);\n outputText(`Schema: ${info.schemaVersion ?? \"(unknown)\"}`);\n outputText(`Blocks: ${info.blockCount}`);\n if (info.blockIds.length > 0) {\n for (const bid of info.blockIds) {\n outputText(` - ${bid}`);\n }\n }\n outputText(`Created: ${formatDate(info.created)}`);\n outputText(`Last Modified: ${formatDate(info.lastModified)}`);\n }\n } finally {\n await pl.close();\n }\n });\n\n return cmd;\n}\n","import { Command, Option } from \"commander\";\nimport { field, resourceIdToString } from \"@milaboratories/pl-client\";\nimport { ProjectMetaKey } from \"@milaboratories/pl-middle-layer\";\nimport { randomUUID } from \"node:crypto\";\nimport { connect } from \"../../base_command\";\nimport { addOptions, GlobalOptions, UserAuthOptions, AdminTargetOptions } from \"../../cmd-opts\";\nimport {\n resolveProject,\n deduplicateName,\n duplicateProject,\n getExistingLabelsInTx,\n} from \"../../project_ops\";\nimport { outputJson, outputText } from \"../../output\";\n\nexport default function projectDuplicateCommand(): Command {\n const cmd = new Command(\"duplicate\").description(\n \"Duplicate a project within the same user. Auto-renames on collision by default.\",\n );\n\n cmd.argument(\"<project>\", \"Project ID or label\");\n addOptions(cmd, GlobalOptions(), UserAuthOptions(), AdminTargetOptions());\n cmd.option(\"-n, --name <name>\", \"Name for the duplicate\");\n cmd.addOption(\n new Option(\"--auto-rename\", \"Auto-rename on collision (default: true)\").default(true),\n );\n cmd.option(\"--no-auto-rename\", \"Fail on name collision instead of auto-renaming\");\n\n cmd.action(async (project: string, flags) => {\n const { pl, projectListRid } = await connect(flags);\n try {\n const { rid: sourceRid } = await resolveProject(pl, projectListRid, project);\n\n const newId = randomUUID();\n\n const result = await pl.withWriteTx(\"duplicateProject\", async (tx) => {\n const sourceMetaStr = await tx.getKValueString(sourceRid, ProjectMetaKey);\n const sourceMeta = JSON.parse(sourceMetaStr);\n const sourceLabel: string = sourceMeta.label;\n\n const existingLabels = await getExistingLabelsInTx(tx, projectListRid);\n\n // Compute new label\n let newLabel: string;\n if (flags.name) {\n if (!flags.autoRename && existingLabels.includes(flags.name)) {\n throw new Error(`Project name \"${flags.name}\" already exists.`);\n }\n newLabel = existingLabels.includes(flags.name)\n ? deduplicateName(flags.name, existingLabels)\n : flags.name;\n } else {\n newLabel = deduplicateName(sourceLabel, existingLabels);\n }\n\n const newPrj = await duplicateProject(tx, sourceRid, { label: newLabel });\n tx.createField(field(projectListRid, newId), \"Dynamic\", newPrj);\n await tx.commit();\n\n const projectResourceId = await newPrj.globalId;\n const projectId = resourceIdToString(projectResourceId);\n return { id: projectId, rid: projectResourceId, label: newLabel };\n });\n\n if (flags.format === \"json\") {\n outputJson({ id: result.id, rid: resourceIdToString(result.rid), label: result.label });\n } else {\n outputText(`Duplicated project as \"${result.label}\" (id: ${result.id})`);\n }\n } finally {\n await pl.close();\n }\n });\n\n return cmd;\n}\n","import { Command, Option } from \"commander\";\nimport { connect } from \"../../base_command\";\nimport { addOptions, GlobalOptions, UserAuthOptions, AdminTargetOptions } from \"../../cmd-opts\";\nimport { resolveProject, renameProject } from \"../../project_ops\";\nimport { outputJson, outputText } from \"../../output\";\nimport { resourceIdToString } from \"@milaboratories/pl-client\";\n\nexport default function projectRenameCommand(): Command {\n const cmd = new Command(\"rename\").description(\"Rename a project.\");\n\n cmd.argument(\"<project>\", \"Project ID or label\");\n addOptions(cmd, GlobalOptions(), UserAuthOptions(), AdminTargetOptions());\n cmd.addOption(new Option(\"-n, --name <name>\", \"New name for the project\").makeOptionMandatory());\n\n cmd.action(async (project: string, flags) => {\n const { pl, projectListRid } = await connect(flags);\n try {\n const { id, rid } = await resolveProject(pl, projectListRid, project);\n\n await renameProject(pl, rid, flags.name);\n\n if (flags.format === \"json\") {\n outputJson({ id, rid: resourceIdToString(rid), label: flags.name });\n } else {\n outputText(`Renamed project to \"${flags.name}\"`);\n }\n } finally {\n await pl.close();\n }\n });\n\n return cmd;\n}\n","import { Command } from \"commander\";\nimport { createInterface } from \"node:readline\";\nimport { connect } from \"../../base_command\";\nimport { addOptions, GlobalOptions, UserAuthOptions, AdminTargetOptions } from \"../../cmd-opts\";\nimport { resolveProject, deleteProject, getProjectInfo } from \"../../project_ops\";\nimport { outputJson, outputText } from \"../../output\";\n\nexport default function projectDeleteCommand(): Command {\n const cmd = new Command(\"delete\").description(\n \"Delete a project. This permanently destroys all project data.\",\n );\n\n cmd.argument(\"<project>\", \"Project ID or label\");\n addOptions(cmd, GlobalOptions(), UserAuthOptions(), AdminTargetOptions());\n cmd.option(\"--force\", \"Skip confirmation\", false);\n\n cmd.action(async (project: string, flags) => {\n const { pl, projectListRid } = await connect(flags);\n try {\n const { id, rid, fieldName } = await resolveProject(pl, projectListRid, project);\n\n const info = await getProjectInfo(pl, id, rid);\n\n if (!flags.force) {\n const rl = createInterface({ input: process.stdin, output: process.stderr });\n try {\n const answer = await new Promise<string>((resolve) => {\n rl.question(\n `Delete project \"${info.label}\" (${info.blockCount} blocks)? [y/N] `,\n resolve,\n );\n });\n if (answer.toLowerCase() !== \"y\") {\n outputText(\"Aborted.\");\n return;\n }\n } finally {\n rl.close();\n }\n }\n\n await deleteProject(pl, projectListRid, fieldName);\n\n if (flags.format === \"json\") {\n outputJson({ deleted: true, id, label: info.label });\n } else {\n outputText(`Deleted project \"${info.label}\"`);\n }\n } finally {\n await pl.close();\n }\n });\n\n return cmd;\n}\n","import { Command, Option } from \"commander\";\nimport { field, resourceIdToString } from \"@milaboratories/pl-client\";\nimport { ProjectMetaKey } from \"@milaboratories/pl-middle-layer\";\nimport { randomUUID } from \"node:crypto\";\nimport { connectClient } from \"../../base_command\";\nimport { addOptions, GlobalOptions, AdminAuthOptions } from \"../../cmd-opts\";\nimport {\n resolveProject,\n deduplicateName,\n duplicateProject,\n getExistingLabelsInTx,\n navigateToUserRoot,\n} from \"../../project_ops\";\nimport { outputJson, outputText } from \"../../output\";\n\nexport default function adminCopyProjectCommand(): Command {\n const cmd = new Command(\"copy-project\").description(\n \"Copy a project from one user to another. Requires admin/controller credentials.\",\n );\n\n addOptions(cmd, GlobalOptions(), AdminAuthOptions());\n cmd.addOption(\n new Option(\n \"--source-user <user>\",\n \"Username of the source project owner\",\n ).makeOptionMandatory(),\n );\n cmd.addOption(\n new Option(\"--source-project <project>\", \"Source project ID or label\").makeOptionMandatory(),\n );\n cmd.option(\n \"--target-user <user>\",\n \"Username of the target user (defaults to source-user for same-user copy)\",\n );\n cmd.option(\"-n, --name <name>\", \"Name for the copied project\");\n cmd.addOption(\n new Option(\"--auto-rename\", \"Auto-rename on collision (default: true)\").default(true),\n );\n cmd.option(\"--no-auto-rename\", \"Fail on name collision instead of auto-renaming\");\n\n cmd.action(async (flags) => {\n const pl = await connectClient(flags);\n try {\n const targetUser = flags.targetUser ?? flags.sourceUser;\n\n const source = await navigateToUserRoot(pl, flags.sourceUser);\n const { rid: sourceRid } = await resolveProject(\n pl,\n source.projectListRid,\n flags.sourceProject,\n );\n\n const target =\n targetUser === flags.sourceUser ? source : await navigateToUserRoot(pl, targetUser);\n\n const newId = randomUUID();\n\n const result = await pl.withWriteTx(\"adminCopyProject\", async (tx) => {\n const sourceMetaStr = await tx.getKValueString(sourceRid, ProjectMetaKey);\n const sourceMeta = JSON.parse(sourceMetaStr);\n const sourceLabel: string = sourceMeta.label;\n\n const existingLabels = await getExistingLabelsInTx(tx, target.projectListRid);\n\n let newLabel: string = flags.name ?? sourceLabel;\n\n if (existingLabels.includes(newLabel)) {\n if (!flags.autoRename) {\n throw new Error(`Project name \"${newLabel}\" already exists for target user.`);\n }\n newLabel = deduplicateName(flags.name ?? sourceLabel, existingLabels);\n }\n\n const newPrj = await duplicateProject(tx, sourceRid, { label: newLabel });\n tx.createField(field(target.projectListRid, newId), \"Dynamic\", newPrj);\n await tx.commit();\n\n const prjResourceId = await newPrj.globalId;\n const prjId = resourceIdToString(prjResourceId);\n return { id: prjId, rid: prjResourceId, label: newLabel };\n });\n\n if (flags.format === \"json\") {\n outputJson({\n id: result.id,\n rid: resourceIdToString(result.rid),\n label: result.label,\n sourceUser: flags.sourceUser,\n targetUser,\n });\n } else {\n outputText(\n `Copied project from ${flags.sourceUser} to ${targetUser} as \"${result.label}\" (id: ${result.id}, rid: ${resourceIdToString(result.rid)})`,\n );\n }\n } finally {\n await pl.close();\n }\n });\n\n return cmd;\n}\n","import { Command } from \"commander\";\nimport { connectClient } from \"../../base_command\";\nimport { addOptions, GlobalOptions, AdminAuthOptions } from \"../../cmd-opts\";\nimport { outputJson, outputText, outputCsv } from \"../../output\";\n\nexport default function adminUserListCommand(): Command {\n const cmd = new Command(\"user-list\").description(\n \"List all known users' logins from a live server. Requires admin/controller credentials.\",\n );\n\n addOptions(cmd, GlobalOptions(\"csv\", [\"text\", \"json\", \"csv\"]), AdminAuthOptions());\n\n cmd.action(async (flags) => {\n const pl = await connectClient(flags);\n try {\n const logins = (await pl.listUsers()).map((u) => u.login).sort();\n if (flags.format === \"json\") outputJson(logins);\n else if (flags.format === \"text\") outputText(logins.join(\"\\n\"));\n else\n outputCsv(\n [\"login\"],\n logins.map((l) => [l]),\n );\n } finally {\n await pl.close();\n }\n });\n\n return cmd;\n}\n","import { Command, Option } from \"commander\";\nimport { createInterface } from \"node:readline\";\nimport { connectClient } from \"../../base_command\";\nimport { addOptions, GlobalOptions, AdminAuthOptions } from \"../../cmd-opts\";\nimport {\n ensureUserProjectList,\n listProjectIdentities,\n moveProjects,\n openUserRoot,\n} from \"../../project_ops\";\nimport type { MovedProject, ProjectIdentityWithLabel } from \"../../project_ops\";\nimport { formatTable, outputJson, outputText } from \"../../output\";\n\nexport default function adminDeleteUserCommand(): Command {\n const cmd = new Command(\"delete-user\").description(\n \"Delete a user account. Optionally re-homes the user's projects to another user first. \" +\n \"Requires admin/controller credentials.\",\n );\n\n cmd.argument(\"<user>\", \"Username of the account to delete\");\n addOptions(cmd, GlobalOptions(), AdminAuthOptions());\n cmd.addOption(\n new Option(\n \"--move-projects-to <user>\",\n \"Re-home the deleted user's projects to this user before deleting the account\",\n ),\n );\n cmd.addOption(\n new Option(\n \"--delete-projects\",\n \"Delete the user's projects along with the account, losing their data\",\n ),\n );\n cmd.option(\"--force\", \"Skip confirmation\", false);\n\n cmd.action(async (user: string, flags) => {\n // The two modes are alternatives, not a combination: one keeps the projects, the other\n // destroys them, and a request for both says nothing about which was meant.\n if (flags.moveProjectsTo && flags.deleteProjects) {\n throw new Error(\"--move-projects-to and --delete-projects are mutually exclusive\");\n }\n if (flags.moveProjectsTo === user) {\n throw new Error(\"--move-projects-to must name a different user than the one being deleted\");\n }\n // Caught here as well as server-side, where it is authoritative: the operator gets the reason\n // instead of a failure from whichever lookup happens to run first.\n if (flags.adminUser === user) {\n throw new Error(\n `Refusing to delete \"${user}\" — it is the account these credentials authenticate as.`,\n );\n }\n\n const pl = await connectClient(flags);\n try {\n const source = await openUserRoot(pl, user);\n // No project list means the account never opened the app, so it owns nothing — a state to\n // delete straight through, not to fail on.\n const projects =\n source.projectListRid === undefined\n ? []\n : await listProjectIdentities(pl, source.projectListRid);\n\n // Neither mode given while the account still owns projects: refuse rather than pick. Both\n // defaults are wrong to assume — one silently destroys data, the other silently hands it to\n // someone. An account with no projects has nothing to decide about, so it just proceeds.\n if (projects.length > 0 && !flags.moveProjectsTo && !flags.deleteProjects) {\n throw new Error(\n `User \"${user}\" owns ${projects.length} project(s). Pass --move-projects-to <user> to ` +\n \"re-home them, or --delete-projects to delete them with the account.\",\n );\n }\n\n const confirmed = await confirmDeletion(user, projects, flags);\n if (!confirmed) {\n outputText(\"Aborted.\");\n return;\n }\n\n let moved: MovedProject[] = [];\n if (flags.moveProjectsTo && projects.length > 0) {\n const target = await openUserRoot(pl, flags.moveProjectsTo);\n // Created if the target has none, so re-homing to a user who has never opened the app\n // works rather than failing halfway with the account still present.\n const targetList = await ensureUserProjectList(pl, target.userRoot);\n moved = await moveProjects(pl, source.projectListRid!, targetList);\n }\n\n // deleteUser takes the root and everything still attached to it. The listing, the prompt and\n // the move each ran in an earlier transaction, so re-read: a project attached since then would\n // be destroyed without ever being reviewed.\n const remaining =\n source.projectListRid === undefined\n ? []\n : await listProjectIdentities(pl, source.projectListRid);\n assertProjectsAccountedFor(user, flags, projects, remaining);\n\n const report = await pl.deleteUser(user);\n\n if (flags.format === \"json\") {\n outputJson({\n deleted: true,\n user,\n movedTo: flags.moveProjectsTo ?? null,\n movedProjects: moved,\n deletedProjects: flags.moveProjectsTo ? [] : remaining.map((p) => p.label),\n userRootId: report.userRootId === undefined ? null : report.userRootId.toString(),\n userRootDeleted: report.userRootDeleted,\n revokedGrants: report.revokedGrants,\n removedIdentityIndexEntries: report.removedIdentityIndexEntries,\n });\n } else {\n outputText(renderResult(user, flags.moveProjectsTo, moved, remaining));\n }\n } finally {\n await pl.close();\n }\n });\n\n return cmd;\n}\n\n/**\n * Refuses while the root holds a project the operator never agreed to lose — attached after the\n * listing, or left behind by the move.\n *\n * A last read, not a lock: it closes the prompt-sized window, not the round trip that follows.\n */\nfunction assertProjectsAccountedFor(\n user: string,\n flags: { moveProjectsTo?: string },\n approved: ProjectIdentityWithLabel[],\n remaining: ProjectIdentityWithLabel[],\n): void {\n if (flags.moveProjectsTo) {\n if (remaining.length === 0) return;\n throw new Error(\n `User \"${user}\" still owns ${remaining.length} project(s) after the move: ` +\n `${remaining.map((p) => p.label).join(\", \")}. The account was NOT deleted — ` +\n \"the projects were attached while the move ran. Re-run the command to move them too.\",\n );\n }\n\n const approvedIds = new Set(approved.map((p) => p.id));\n const unapproved = remaining.filter((p) => !approvedIds.has(p.id));\n if (unapproved.length === 0) return;\n throw new Error(\n `User \"${user}\" gained ${unapproved.length} project(s) since the list you confirmed: ` +\n `${unapproved.map((p) => p.label).join(\", \")}. The account was NOT deleted — ` +\n \"deleting it now would destroy them unreviewed. Re-run the command to see the current list.\",\n );\n}\n\n/** Asks the operator to confirm, naming the affected projects and their fate. */\nasync function confirmDeletion(\n user: string,\n projects: ProjectIdentityWithLabel[],\n flags: { moveProjectsTo?: string; force?: boolean },\n): Promise<boolean> {\n if (flags.force) return true;\n\n const fate = flags.moveProjectsTo\n ? `${projects.length} project(s) will move to \"${flags.moveProjectsTo}\"`\n : projects.length > 0\n ? `${projects.length} project(s) will be PERMANENTLY DELETED`\n : \"the account owns no projects\";\n\n process.stderr.write(`Delete user \"${user}\"? ${fate}.\\n`);\n if (projects.length > 0) {\n process.stderr.write(projects.map((p) => ` - ${p.label}`).join(\"\\n\") + \"\\n\");\n }\n\n const rl = createInterface({ input: process.stdin, output: process.stderr });\n try {\n const answer = await new Promise<string>((resolve) => {\n rl.question(\"Proceed? [y/N] \", resolve);\n });\n return answer.toLowerCase() === \"y\";\n } finally {\n rl.close();\n }\n}\n\nfunction renderResult(\n user: string,\n movedTo: string | undefined,\n moved: MovedProject[],\n projects: ProjectIdentityWithLabel[],\n): string {\n const lines: string[] = [];\n\n if (movedTo) {\n if (moved.length > 0) {\n lines.push(`Moved ${moved.length} project(s) from ${user} to ${movedTo}:`);\n lines.push(\n formatTable(\n [\"id\", \"name\", \"name in target\"],\n moved.map((p) => [p.id, p.sourceLabel, p.targetLabel]),\n ),\n );\n } else {\n lines.push(`User ${user} had no projects to move.`);\n }\n } else if (projects.length > 0) {\n lines.push(`Deleted ${projects.length} project(s) belonging to ${user}:`);\n lines.push(projects.map((p) => ` - ${p.label}`).join(\"\\n\"));\n }\n\n lines.push(`Deleted user \"${user}\".`);\n return lines.join(\"\\n\");\n}\n","import { Command } from \"commander\";\nimport projectListCommand from \"./cmd/project/list\";\nimport projectInfoCommand from \"./cmd/project/info\";\nimport projectDuplicateCommand from \"./cmd/project/duplicate\";\nimport projectRenameCommand from \"./cmd/project/rename\";\nimport projectDeleteCommand from \"./cmd/project/delete\";\nimport adminCopyProjectCommand from \"./cmd/admin/copy-project\";\nimport adminUserListCommand from \"./cmd/admin/user-list\";\nimport adminDeleteUserCommand from \"./cmd/admin/delete-user\";\n\nexport function buildProgram(): Command {\n const program = new Command();\n program.name(\"pl-cli\").description(\"Platforma command-line client\");\n\n const project = new Command(\"project\").description(\"Manage projects\");\n project.addCommand(projectListCommand());\n project.addCommand(projectInfoCommand());\n project.addCommand(projectDuplicateCommand());\n project.addCommand(projectRenameCommand());\n project.addCommand(projectDeleteCommand());\n program.addCommand(project);\n\n const admin = new Command(\"admin\").description(\n \"Admin operations (requires controller credentials)\",\n );\n admin.addCommand(adminCopyProjectCommand());\n admin.addCommand(adminUserListCommand());\n admin.addCommand(adminDeleteUserCommand());\n program.addCommand(admin);\n\n return program;\n}\n\nexport async function run(argv: string[] = process.argv): Promise<void> {\n await buildProgram().parseAsync(argv);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/base_command.ts","../src/cmd-opts.ts","../src/cmd/project/list.ts","../src/cmd/project/info.ts","../src/cmd/project/duplicate.ts","../src/cmd/project/rename.ts","../src/cmd/project/delete.ts","../src/cmd/admin/copy-project.ts","../src/cmd/admin/user-list.ts","../src/cmd/admin/delete-user.ts","../src/resource_id.ts","../src/cmd/maintenance/list-resources.ts","../src/cmd/maintenance/set-resource-error.ts","../src/cli.ts"],"sourcesContent":["import type { PlClient, SignedResourceId } from \"@milaboratories/pl-client\";\nimport { createPlConnection, createAdminPlConnection } from \"./connection\";\nimport { getProjectListRid, navigateToUserRoot } from \"./project_ops\";\n\n/**\n * Low-level: get an authenticated PlClient without resolving a project list.\n * Use this for commands that navigate to multiple users (e.g. admin copy-project).\n *\n * Callers own the returned client and must `await pl.close()` when done.\n */\nexport async function connectClient(flags: {\n address: string;\n user?: string;\n password?: string;\n adminUser?: string;\n adminPassword?: string;\n}): Promise<PlClient> {\n if (flags.adminUser && flags.adminPassword) {\n return createAdminPlConnection({\n address: flags.address,\n adminUser: flags.adminUser,\n adminPassword: flags.adminPassword,\n });\n }\n return createPlConnection({\n address: flags.address,\n user: flags.user,\n password: flags.password,\n });\n}\n\n/**\n * Connect and resolve the project list for a single user.\n * In admin mode (--admin-user + --admin-password + --target-user), operates on the target user's data.\n * In user mode, operates on the authenticated user's own data.\n *\n * Callers own the returned client and must `await pl.close()` when done.\n */\nexport async function connect(flags: {\n address: string;\n user?: string;\n password?: string;\n adminUser?: string;\n adminPassword?: string;\n targetUser?: string;\n}): Promise<{ pl: PlClient; projectListRid: SignedResourceId }> {\n const hasAdminUser = !!flags.adminUser;\n const hasAdminPassword = !!flags.adminPassword;\n const hasTarget = !!flags.targetUser;\n\n // Validate flag combinations\n if (hasTarget && !(hasAdminUser && hasAdminPassword)) {\n throw new Error(\"--target-user requires --admin-user and --admin-password\");\n }\n if ((hasAdminUser || hasAdminPassword) && !hasTarget) {\n throw new Error(\"--admin-user/--admin-password require --target-user for project commands\");\n }\n\n const pl = await connectClient(flags);\n\n let projectListRid: SignedResourceId;\n if (hasTarget) {\n const nav = await navigateToUserRoot(pl, flags.targetUser!);\n projectListRid = nav.projectListRid;\n } else {\n projectListRid = await getProjectListRid(pl);\n }\n\n return { pl, projectListRid };\n}\n","import { Command, Option } from \"commander\";\nimport type { OutputFormat } from \"./output\";\n\n/** Add one or more groups of options to a command, preserving group order. */\nexport function addOptions(cmd: Command, ...optionGroups: Option[][]): Command {\n for (const group of optionGroups) {\n for (const opt of group) cmd.addOption(opt);\n }\n return cmd;\n}\n\n/** Server address only (for commands with a fixed output format). */\nexport const AddressOptions = (): Option[] => [\n new Option(\"-a, --address <url>\", \"Platforma server address\")\n .env(\"PL_ADDRESS\")\n .makeOptionMandatory(),\n];\n\nexport const GlobalOptions = (\n formatDefault: OutputFormat = \"text\",\n allowedFormats: OutputFormat[] = [\"text\", \"json\"],\n): Option[] => [\n ...AddressOptions(),\n new Option(\"-f, --format <format>\", \"Output format\")\n .choices(allowedFormats)\n .default(formatDefault),\n];\n\nexport const UserAuthOptions = (): Option[] => [\n new Option(\"-u, --user <user>\", \"Username for authentication\").env(\"PL_USER\"),\n new Option(\"-p, --password <password>\", \"Password for authentication\").env(\"PL_PASSWORD\"),\n];\n\n/** Admin credentials only (for purely admin commands like copy-project). */\nexport const AdminAuthOptions = (): Option[] => [\n new Option(\"--admin-user <user>\", \"Admin/controller username\")\n .env(\"PL_ADMIN_USER\")\n .makeOptionMandatory(),\n new Option(\"--admin-password <password>\", \"Admin/controller password\")\n .env(\"PL_ADMIN_PASSWORD\")\n .makeOptionMandatory(),\n];\n\n/** Admin credentials + target user (for regular commands that can optionally operate on another user). */\nexport const AdminTargetOptions = (): Option[] => [\n new Option(\"--admin-user <user>\", \"Admin/controller username (enables admin mode)\").env(\n \"PL_ADMIN_USER\",\n ),\n new Option(\"--admin-password <password>\", \"Admin/controller password\").env(\"PL_ADMIN_PASSWORD\"),\n new Option(\n \"--target-user <user>\",\n \"Operate on this user's data (requires admin credentials)\",\n ).env(\"PL_TARGET_USER\"),\n];\n","import { Command } from \"commander\";\nimport { connect } from \"../../base_command\";\nimport { addOptions, GlobalOptions, UserAuthOptions, AdminTargetOptions } from \"../../cmd-opts\";\nimport { listProjects } from \"../../project_ops\";\nimport { formatTable, formatDate, outputJson, outputText } from \"../../output\";\nimport { resourceIdToString } from \"@milaboratories/pl-client\";\n\nexport default function projectListCommand(): Command {\n const cmd = new Command(\"list\").description(\"List all projects for the authenticated user.\");\n\n addOptions(cmd, GlobalOptions(), UserAuthOptions(), AdminTargetOptions());\n\n cmd.action(async (flags) => {\n const { pl, projectListRid } = await connect(flags);\n try {\n const projects = await listProjects(pl, projectListRid);\n\n if (flags.format === \"json\") {\n outputJson(\n projects.map((p) => ({\n id: p.id,\n rid: resourceIdToString(p.rid),\n label: p.label,\n created: p.created.toISOString(),\n lastModified: p.lastModified.toISOString(),\n })),\n );\n } else {\n if (projects.length === 0) {\n outputText(\"No projects found.\");\n return;\n }\n\n outputText(\n formatTable(\n [\"ID\", \"RID\", \"LABEL\", \"CREATED\", \"LAST MODIFIED\"],\n projects.map((p) => [\n p.id,\n resourceIdToString(p.rid),\n p.label,\n formatDate(p.created),\n formatDate(p.lastModified),\n ]),\n ),\n );\n outputText(`\\n${projects.length} project(s)`);\n }\n } finally {\n await pl.close();\n }\n });\n\n return cmd;\n}\n","import { Command } from \"commander\";\nimport { connect } from \"../../base_command\";\nimport { addOptions, GlobalOptions, UserAuthOptions, AdminTargetOptions } from \"../../cmd-opts\";\nimport { resolveProject, getProjectInfo } from \"../../project_ops\";\nimport { formatDate, outputJson, outputText } from \"../../output\";\nimport { resourceIdToString } from \"@milaboratories/pl-client\";\n\nexport default function projectInfoCommand(): Command {\n const cmd = new Command(\"info\").description(\"Show detailed information about a project.\");\n\n cmd.argument(\"<project>\", \"Project ID or label\");\n addOptions(cmd, GlobalOptions(), UserAuthOptions(), AdminTargetOptions());\n\n cmd.action(async (project: string, flags) => {\n const { pl, projectListRid } = await connect(flags);\n try {\n const { id, rid } = await resolveProject(pl, projectListRid, project);\n const info = await getProjectInfo(pl, id, rid);\n\n if (flags.format === \"json\") {\n outputJson({\n id: info.id,\n rid: resourceIdToString(info.rid),\n label: info.label,\n schemaVersion: info.schemaVersion,\n blockCount: info.blockCount,\n blockIds: info.blockIds,\n created: info.created.toISOString(),\n lastModified: info.lastModified.toISOString(),\n });\n } else {\n outputText(`Project: ${info.label}`);\n outputText(`ID: ${info.id}`);\n outputText(`RID: ${resourceIdToString(info.rid)}`);\n outputText(`Schema: ${info.schemaVersion ?? \"(unknown)\"}`);\n outputText(`Blocks: ${info.blockCount}`);\n if (info.blockIds.length > 0) {\n for (const bid of info.blockIds) {\n outputText(` - ${bid}`);\n }\n }\n outputText(`Created: ${formatDate(info.created)}`);\n outputText(`Last Modified: ${formatDate(info.lastModified)}`);\n }\n } finally {\n await pl.close();\n }\n });\n\n return cmd;\n}\n","import { Command, Option } from \"commander\";\nimport { field, resourceIdToString } from \"@milaboratories/pl-client\";\nimport { ProjectMetaKey } from \"@milaboratories/pl-middle-layer\";\nimport { randomUUID } from \"node:crypto\";\nimport { connect } from \"../../base_command\";\nimport { addOptions, GlobalOptions, UserAuthOptions, AdminTargetOptions } from \"../../cmd-opts\";\nimport {\n resolveProject,\n deduplicateName,\n duplicateProject,\n getExistingLabelsInTx,\n} from \"../../project_ops\";\nimport { outputJson, outputText } from \"../../output\";\n\nexport default function projectDuplicateCommand(): Command {\n const cmd = new Command(\"duplicate\").description(\n \"Duplicate a project within the same user. Auto-renames on collision by default.\",\n );\n\n cmd.argument(\"<project>\", \"Project ID or label\");\n addOptions(cmd, GlobalOptions(), UserAuthOptions(), AdminTargetOptions());\n cmd.option(\"-n, --name <name>\", \"Name for the duplicate\");\n cmd.addOption(\n new Option(\"--auto-rename\", \"Auto-rename on collision (default: true)\").default(true),\n );\n cmd.option(\"--no-auto-rename\", \"Fail on name collision instead of auto-renaming\");\n\n cmd.action(async (project: string, flags) => {\n const { pl, projectListRid } = await connect(flags);\n try {\n const { rid: sourceRid } = await resolveProject(pl, projectListRid, project);\n\n const newId = randomUUID();\n\n const result = await pl.withWriteTx(\"duplicateProject\", async (tx) => {\n const sourceMetaStr = await tx.getKValueString(sourceRid, ProjectMetaKey);\n const sourceMeta = JSON.parse(sourceMetaStr);\n const sourceLabel: string = sourceMeta.label;\n\n const existingLabels = await getExistingLabelsInTx(tx, projectListRid);\n\n // Compute new label\n let newLabel: string;\n if (flags.name) {\n if (!flags.autoRename && existingLabels.includes(flags.name)) {\n throw new Error(`Project name \"${flags.name}\" already exists.`);\n }\n newLabel = existingLabels.includes(flags.name)\n ? deduplicateName(flags.name, existingLabels)\n : flags.name;\n } else {\n newLabel = deduplicateName(sourceLabel, existingLabels);\n }\n\n const newPrj = await duplicateProject(tx, sourceRid, { label: newLabel });\n tx.createField(field(projectListRid, newId), \"Dynamic\", newPrj);\n await tx.commit();\n\n const projectResourceId = await newPrj.globalId;\n const projectId = resourceIdToString(projectResourceId);\n return { id: projectId, rid: projectResourceId, label: newLabel };\n });\n\n if (flags.format === \"json\") {\n outputJson({ id: result.id, rid: resourceIdToString(result.rid), label: result.label });\n } else {\n outputText(`Duplicated project as \"${result.label}\" (id: ${result.id})`);\n }\n } finally {\n await pl.close();\n }\n });\n\n return cmd;\n}\n","import { Command, Option } from \"commander\";\nimport { connect } from \"../../base_command\";\nimport { addOptions, GlobalOptions, UserAuthOptions, AdminTargetOptions } from \"../../cmd-opts\";\nimport { resolveProject, renameProject } from \"../../project_ops\";\nimport { outputJson, outputText } from \"../../output\";\nimport { resourceIdToString } from \"@milaboratories/pl-client\";\n\nexport default function projectRenameCommand(): Command {\n const cmd = new Command(\"rename\").description(\"Rename a project.\");\n\n cmd.argument(\"<project>\", \"Project ID or label\");\n addOptions(cmd, GlobalOptions(), UserAuthOptions(), AdminTargetOptions());\n cmd.addOption(new Option(\"-n, --name <name>\", \"New name for the project\").makeOptionMandatory());\n\n cmd.action(async (project: string, flags) => {\n const { pl, projectListRid } = await connect(flags);\n try {\n const { id, rid } = await resolveProject(pl, projectListRid, project);\n\n await renameProject(pl, rid, flags.name);\n\n if (flags.format === \"json\") {\n outputJson({ id, rid: resourceIdToString(rid), label: flags.name });\n } else {\n outputText(`Renamed project to \"${flags.name}\"`);\n }\n } finally {\n await pl.close();\n }\n });\n\n return cmd;\n}\n","import { Command } from \"commander\";\nimport { createInterface } from \"node:readline\";\nimport { connect } from \"../../base_command\";\nimport { addOptions, GlobalOptions, UserAuthOptions, AdminTargetOptions } from \"../../cmd-opts\";\nimport { resolveProject, deleteProject, getProjectInfo } from \"../../project_ops\";\nimport { outputJson, outputText } from \"../../output\";\n\nexport default function projectDeleteCommand(): Command {\n const cmd = new Command(\"delete\").description(\n \"Delete a project. This permanently destroys all project data.\",\n );\n\n cmd.argument(\"<project>\", \"Project ID or label\");\n addOptions(cmd, GlobalOptions(), UserAuthOptions(), AdminTargetOptions());\n cmd.option(\"--force\", \"Skip confirmation\", false);\n\n cmd.action(async (project: string, flags) => {\n const { pl, projectListRid } = await connect(flags);\n try {\n const { id, rid, fieldName } = await resolveProject(pl, projectListRid, project);\n\n const info = await getProjectInfo(pl, id, rid);\n\n if (!flags.force) {\n const rl = createInterface({ input: process.stdin, output: process.stderr });\n try {\n const answer = await new Promise<string>((resolve) => {\n rl.question(\n `Delete project \"${info.label}\" (${info.blockCount} blocks)? [y/N] `,\n resolve,\n );\n });\n if (answer.toLowerCase() !== \"y\") {\n outputText(\"Aborted.\");\n return;\n }\n } finally {\n rl.close();\n }\n }\n\n await deleteProject(pl, projectListRid, fieldName);\n\n if (flags.format === \"json\") {\n outputJson({ deleted: true, id, label: info.label });\n } else {\n outputText(`Deleted project \"${info.label}\"`);\n }\n } finally {\n await pl.close();\n }\n });\n\n return cmd;\n}\n","import { Command, Option } from \"commander\";\nimport { field, resourceIdToString } from \"@milaboratories/pl-client\";\nimport { ProjectMetaKey } from \"@milaboratories/pl-middle-layer\";\nimport { randomUUID } from \"node:crypto\";\nimport { connectClient } from \"../../base_command\";\nimport { addOptions, GlobalOptions, AdminAuthOptions } from \"../../cmd-opts\";\nimport {\n resolveProject,\n deduplicateName,\n duplicateProject,\n getExistingLabelsInTx,\n navigateToUserRoot,\n} from \"../../project_ops\";\nimport { outputJson, outputText } from \"../../output\";\n\nexport default function adminCopyProjectCommand(): Command {\n const cmd = new Command(\"copy-project\").description(\n \"Copy a project from one user to another. Requires admin/controller credentials.\",\n );\n\n addOptions(cmd, GlobalOptions(), AdminAuthOptions());\n cmd.addOption(\n new Option(\n \"--source-user <user>\",\n \"Username of the source project owner\",\n ).makeOptionMandatory(),\n );\n cmd.addOption(\n new Option(\"--source-project <project>\", \"Source project ID or label\").makeOptionMandatory(),\n );\n cmd.option(\n \"--target-user <user>\",\n \"Username of the target user (defaults to source-user for same-user copy)\",\n );\n cmd.option(\"-n, --name <name>\", \"Name for the copied project\");\n cmd.addOption(\n new Option(\"--auto-rename\", \"Auto-rename on collision (default: true)\").default(true),\n );\n cmd.option(\"--no-auto-rename\", \"Fail on name collision instead of auto-renaming\");\n\n cmd.action(async (flags) => {\n const pl = await connectClient(flags);\n try {\n const targetUser = flags.targetUser ?? flags.sourceUser;\n\n const source = await navigateToUserRoot(pl, flags.sourceUser);\n const { rid: sourceRid } = await resolveProject(\n pl,\n source.projectListRid,\n flags.sourceProject,\n );\n\n const target =\n targetUser === flags.sourceUser ? source : await navigateToUserRoot(pl, targetUser);\n\n const newId = randomUUID();\n\n const result = await pl.withWriteTx(\"adminCopyProject\", async (tx) => {\n const sourceMetaStr = await tx.getKValueString(sourceRid, ProjectMetaKey);\n const sourceMeta = JSON.parse(sourceMetaStr);\n const sourceLabel: string = sourceMeta.label;\n\n const existingLabels = await getExistingLabelsInTx(tx, target.projectListRid);\n\n let newLabel: string = flags.name ?? sourceLabel;\n\n if (existingLabels.includes(newLabel)) {\n if (!flags.autoRename) {\n throw new Error(`Project name \"${newLabel}\" already exists for target user.`);\n }\n newLabel = deduplicateName(flags.name ?? sourceLabel, existingLabels);\n }\n\n const newPrj = await duplicateProject(tx, sourceRid, { label: newLabel });\n tx.createField(field(target.projectListRid, newId), \"Dynamic\", newPrj);\n await tx.commit();\n\n const prjResourceId = await newPrj.globalId;\n const prjId = resourceIdToString(prjResourceId);\n return { id: prjId, rid: prjResourceId, label: newLabel };\n });\n\n if (flags.format === \"json\") {\n outputJson({\n id: result.id,\n rid: resourceIdToString(result.rid),\n label: result.label,\n sourceUser: flags.sourceUser,\n targetUser,\n });\n } else {\n outputText(\n `Copied project from ${flags.sourceUser} to ${targetUser} as \"${result.label}\" (id: ${result.id}, rid: ${resourceIdToString(result.rid)})`,\n );\n }\n } finally {\n await pl.close();\n }\n });\n\n return cmd;\n}\n","import { Command } from \"commander\";\nimport { connectClient } from \"../../base_command\";\nimport { addOptions, GlobalOptions, AdminAuthOptions } from \"../../cmd-opts\";\nimport { outputJson, outputText, outputCsv } from \"../../output\";\n\nexport default function adminUserListCommand(): Command {\n const cmd = new Command(\"user-list\").description(\n \"List all known users' logins from a live server. Requires admin/controller credentials.\",\n );\n\n addOptions(cmd, GlobalOptions(\"csv\", [\"text\", \"json\", \"csv\"]), AdminAuthOptions());\n\n cmd.action(async (flags) => {\n const pl = await connectClient(flags);\n try {\n const logins = (await pl.listUsers()).map((u) => u.login).sort();\n if (flags.format === \"json\") outputJson(logins);\n else if (flags.format === \"text\") outputText(logins.join(\"\\n\"));\n else\n outputCsv(\n [\"login\"],\n logins.map((l) => [l]),\n );\n } finally {\n await pl.close();\n }\n });\n\n return cmd;\n}\n","import { Command, Option } from \"commander\";\nimport { createInterface } from \"node:readline\";\nimport { connectClient } from \"../../base_command\";\nimport { addOptions, GlobalOptions, AdminAuthOptions } from \"../../cmd-opts\";\nimport {\n ensureUserProjectList,\n listProjectIdentities,\n moveProjects,\n openUserRoot,\n} from \"../../project_ops\";\nimport type { MovedProject, ProjectIdentityWithLabel } from \"../../project_ops\";\nimport { formatTable, outputJson, outputText } from \"../../output\";\n\nexport default function adminDeleteUserCommand(): Command {\n const cmd = new Command(\"delete-user\").description(\n \"Delete a user account. Optionally re-homes the user's projects to another user first. \" +\n \"Requires admin/controller credentials.\",\n );\n\n cmd.argument(\"<user>\", \"Username of the account to delete\");\n addOptions(cmd, GlobalOptions(), AdminAuthOptions());\n cmd.addOption(\n new Option(\n \"--move-projects-to <user>\",\n \"Re-home the deleted user's projects to this user before deleting the account\",\n ),\n );\n cmd.addOption(\n new Option(\n \"--delete-projects\",\n \"Delete the user's projects along with the account, losing their data\",\n ),\n );\n cmd.option(\"--force\", \"Skip confirmation\", false);\n\n cmd.action(async (user: string, flags) => {\n // The two modes are alternatives, not a combination: one keeps the projects, the other\n // destroys them, and a request for both says nothing about which was meant.\n if (flags.moveProjectsTo && flags.deleteProjects) {\n throw new Error(\"--move-projects-to and --delete-projects are mutually exclusive\");\n }\n if (flags.moveProjectsTo === user) {\n throw new Error(\"--move-projects-to must name a different user than the one being deleted\");\n }\n // Caught here as well as server-side, where it is authoritative: the operator gets the reason\n // instead of a failure from whichever lookup happens to run first.\n if (flags.adminUser === user) {\n throw new Error(\n `Refusing to delete \"${user}\" — it is the account these credentials authenticate as.`,\n );\n }\n\n const pl = await connectClient(flags);\n try {\n const source = await openUserRoot(pl, user);\n // No project list means the account never opened the app, so it owns nothing — a state to\n // delete straight through, not to fail on.\n const projects =\n source.projectListRid === undefined\n ? []\n : await listProjectIdentities(pl, source.projectListRid);\n\n // Neither mode given while the account still owns projects: refuse rather than pick. Both\n // defaults are wrong to assume — one silently destroys data, the other silently hands it to\n // someone. An account with no projects has nothing to decide about, so it just proceeds.\n if (projects.length > 0 && !flags.moveProjectsTo && !flags.deleteProjects) {\n throw new Error(\n `User \"${user}\" owns ${projects.length} project(s). Pass --move-projects-to <user> to ` +\n \"re-home them, or --delete-projects to delete them with the account.\",\n );\n }\n\n const confirmed = await confirmDeletion(user, projects, flags);\n if (!confirmed) {\n outputText(\"Aborted.\");\n return;\n }\n\n let moved: MovedProject[] = [];\n if (flags.moveProjectsTo && projects.length > 0) {\n const target = await openUserRoot(pl, flags.moveProjectsTo);\n // Created if the target has none, so re-homing to a user who has never opened the app\n // works rather than failing halfway with the account still present.\n const targetList = await ensureUserProjectList(pl, target.userRoot);\n moved = await moveProjects(pl, source.projectListRid!, targetList);\n }\n\n // deleteUser takes the root and everything still attached to it. The listing, the prompt and\n // the move each ran in an earlier transaction, so re-read: a project attached since then would\n // be destroyed without ever being reviewed.\n const remaining =\n source.projectListRid === undefined\n ? []\n : await listProjectIdentities(pl, source.projectListRid);\n assertProjectsAccountedFor(user, flags, projects, remaining);\n\n const report = await pl.deleteUser(user);\n\n if (flags.format === \"json\") {\n outputJson({\n deleted: true,\n user,\n movedTo: flags.moveProjectsTo ?? null,\n movedProjects: moved,\n deletedProjects: flags.moveProjectsTo ? [] : remaining.map((p) => p.label),\n userRootId: report.userRootId === undefined ? null : report.userRootId.toString(),\n userRootDeleted: report.userRootDeleted,\n revokedGrants: report.revokedGrants,\n removedIdentityIndexEntries: report.removedIdentityIndexEntries,\n });\n } else {\n outputText(renderResult(user, flags.moveProjectsTo, moved, remaining));\n }\n } finally {\n await pl.close();\n }\n });\n\n return cmd;\n}\n\n/**\n * Refuses while the root holds a project the operator never agreed to lose — attached after the\n * listing, or left behind by the move.\n *\n * A last read, not a lock: it closes the prompt-sized window, not the round trip that follows.\n */\nfunction assertProjectsAccountedFor(\n user: string,\n flags: { moveProjectsTo?: string },\n approved: ProjectIdentityWithLabel[],\n remaining: ProjectIdentityWithLabel[],\n): void {\n if (flags.moveProjectsTo) {\n if (remaining.length === 0) return;\n throw new Error(\n `User \"${user}\" still owns ${remaining.length} project(s) after the move: ` +\n `${remaining.map((p) => p.label).join(\", \")}. The account was NOT deleted — ` +\n \"the projects were attached while the move ran. Re-run the command to move them too.\",\n );\n }\n\n const approvedIds = new Set(approved.map((p) => p.id));\n const unapproved = remaining.filter((p) => !approvedIds.has(p.id));\n if (unapproved.length === 0) return;\n throw new Error(\n `User \"${user}\" gained ${unapproved.length} project(s) since the list you confirmed: ` +\n `${unapproved.map((p) => p.label).join(\", \")}. The account was NOT deleted — ` +\n \"deleting it now would destroy them unreviewed. Re-run the command to see the current list.\",\n );\n}\n\n/** Asks the operator to confirm, naming the affected projects and their fate. */\nasync function confirmDeletion(\n user: string,\n projects: ProjectIdentityWithLabel[],\n flags: { moveProjectsTo?: string; force?: boolean },\n): Promise<boolean> {\n if (flags.force) return true;\n\n const fate = flags.moveProjectsTo\n ? `${projects.length} project(s) will move to \"${flags.moveProjectsTo}\"`\n : projects.length > 0\n ? `${projects.length} project(s) will be PERMANENTLY DELETED`\n : \"the account owns no projects\";\n\n process.stderr.write(`Delete user \"${user}\"? ${fate}.\\n`);\n if (projects.length > 0) {\n process.stderr.write(projects.map((p) => ` - ${p.label}`).join(\"\\n\") + \"\\n\");\n }\n\n const rl = createInterface({ input: process.stdin, output: process.stderr });\n try {\n const answer = await new Promise<string>((resolve) => {\n rl.question(\"Proceed? [y/N] \", resolve);\n });\n return answer.toLowerCase() === \"y\";\n } finally {\n rl.close();\n }\n}\n\nfunction renderResult(\n user: string,\n movedTo: string | undefined,\n moved: MovedProject[],\n projects: ProjectIdentityWithLabel[],\n): string {\n const lines: string[] = [];\n\n if (movedTo) {\n if (moved.length > 0) {\n lines.push(`Moved ${moved.length} project(s) from ${user} to ${movedTo}:`);\n lines.push(\n formatTable(\n [\"id\", \"name\", \"name in target\"],\n moved.map((p) => [p.id, p.sourceLabel, p.targetLabel]),\n ),\n );\n } else {\n lines.push(`User ${user} had no projects to move.`);\n }\n } else if (projects.length > 0) {\n lines.push(`Deleted ${projects.length} project(s) belonging to ${user}:`);\n lines.push(projects.map((p) => ` - ${p.label}`).join(\"\\n\"));\n }\n\n lines.push(`Deleted user \"${user}\".`);\n return lines.join(\"\\n\");\n}\n","import {\n asSignedResourceId,\n createSignedResourceId,\n isLocalResourceId,\n resourceIdFromString,\n type SignedResourceId,\n} from \"@milaboratories/pl-client\";\n\n/**\n * Admin and controller roles do not need resource signatures: an ID without a signature\n * is accepted by the backend for these roles.\n */\nexport function parseResourceId(raw: string): SignedResourceId {\n const str = raw.trim();\n if (str.includes(\"|\")) return asSignedResourceId(str);\n\n let id: bigint | undefined = resourceIdFromString(str);\n if (id === undefined && /^\\d+$/.test(str)) id = BigInt(str);\n if (id === undefined || id === 0n || isLocalResourceId(id))\n throw new Error(`not a global resource ID: ${raw}`);\n\n return createSignedResourceId(id);\n}\n","import { once } from \"node:events\";\nimport { Command, InvalidArgumentError, Option } from \"commander\";\nimport {\n isNotNullSignedResourceId,\n resourceIdToString,\n resourceType,\n resourceTypeToString,\n type FieldData,\n type KeyValue,\n type OptionalSignedResourceId,\n type ResourceData,\n type ResourceType,\n} from \"@milaboratories/pl-client\";\nimport { connectClient } from \"../../base_command\";\nimport { addOptions, AddressOptions, AdminAuthOptions } from \"../../cmd-opts\";\nimport { parseResourceId } from \"../../resource_id\";\n\nexport default function maintenanceListResourcesCommand(): Command {\n const cmd = new Command(\"list-resources\").description(\n \"List resources of one type as JSON lines, in ascending order of resource ID. \" +\n \"Resource data and key-value values are printed as JSON when they are valid JSON, \" +\n 'as text when they are valid UTF-8, and as {\"base64\": ...} in other cases. ' +\n \"Requires admin/controller credentials.\",\n );\n\n addOptions(cmd, AddressOptions(), AdminAuthOptions());\n cmd.addOption(\n new Option(\"--type <name:version>\", \"Resource type to list (e.g. 'Blob/work:1')\")\n .argParser(parseResourceTypeArg)\n .makeOptionMandatory(),\n );\n cmd.addOption(\n new Option(\"--limit <n>\", \"Maximum number of resources to list. 0 lists all resources\")\n .argParser(parseNonNegativeInt)\n .default(0),\n );\n cmd.addOption(\n new Option(\n \"--after <resource-id>\",\n \"List only resources with IDs greater than this ID. \" +\n \"To get the next page, give the ID of the last resource of the previous page\",\n ).argParser(parseResourceId),\n );\n cmd.option(\n \"--load-fields\",\n \"Load the fields and the error of each resource ('fields' and 'error' keys)\",\n false,\n );\n cmd.option(\"--load-kv\", \"Load the key-value entries of each resource ('kv' key)\", false);\n\n cmd.action(async (flags) => {\n const pl = await connectClient(flags);\n try {\n const resources = await pl.withReadTx(\"listResources\", async (tx) => {\n const list = await tx.listResourcesByType(flags.type, {\n limit: flags.limit,\n after: flags.after,\n loadFields: flags.loadFields,\n });\n return await Promise.all(\n list.map(async (resource) => ({\n resource,\n kv: flags.loadKv ? await tx.listKeyValues(resource.id) : undefined,\n })),\n );\n });\n\n for (const { resource, kv } of resources) {\n const line = JSON.stringify(toJson(resource, flags.loadFields, kv)) + \"\\n\";\n // Wait for a slow reader: else the output collects in memory a second time.\n if (!process.stdout.write(line)) await once(process.stdout, \"drain\");\n }\n } finally {\n await pl.close();\n }\n });\n\n return cmd;\n}\n\nfunction toJson(\n r: ResourceData,\n withFields: boolean,\n kv: KeyValue[] | undefined,\n): Record<string, unknown> {\n return {\n id: resourceIdToString(r.id),\n type: resourceTypeToString(r.type),\n kind: r.kind,\n originalId: optionalIdToString(r.originalResourceId),\n inputsLocked: r.inputsLocked,\n outputsLocked: r.outputsLocked,\n ready: r.resourceReady,\n final: r.final,\n data: r.data === undefined || r.data.length === 0 ? null : decodeBytes(r.data),\n // The backend sends the resource error as a field: without fields it is not known.\n ...(withFields\n ? { error: optionalIdToString(r.error), fields: r.fields.map(fieldToJson) }\n : {}),\n ...(kv === undefined\n ? {}\n : { kv: Object.fromEntries(kv.map((e) => [e.key, decodeBytes(e.value)])) }),\n };\n}\n\nfunction fieldToJson(f: FieldData): Record<string, unknown> {\n return {\n name: f.name,\n type: f.type,\n status: f.status,\n value: optionalIdToString(f.value),\n error: optionalIdToString(f.error),\n valueIsFinal: f.valueIsFinal,\n };\n}\n\nfunction optionalIdToString(id: OptionalSignedResourceId): string | null {\n return isNotNullSignedResourceId(id) ? resourceIdToString(id) : null;\n}\n\n/** JSON value when the bytes are valid JSON, text when valid UTF-8, {\"base64\": ...} otherwise. */\nfunction decodeBytes(bytes: Uint8Array): unknown {\n let text: string;\n try {\n text = new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes);\n } catch {\n return { base64: Buffer.from(bytes).toString(\"base64\") };\n }\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nfunction parseResourceTypeArg(value: string): ResourceType {\n const sep = value.lastIndexOf(\":\");\n if (sep <= 0 || sep === value.length - 1)\n throw new InvalidArgumentError(`expected <name>:<version>, e.g. 'Blob/work:1': ${value}`);\n return resourceType(value.slice(0, sep), value.slice(sep + 1));\n}\n\nfunction parseNonNegativeInt(value: string): number {\n const n = Number(value);\n if (!Number.isInteger(n) || n < 0)\n throw new InvalidArgumentError(`not a non-negative integer: ${value}`);\n return n;\n}\n","import { Command, Option } from \"commander\";\nimport { isNotNullSignedResourceId, resourceIdToString } from \"@milaboratories/pl-client\";\nimport { connectClient } from \"../../base_command\";\nimport { addOptions, AddressOptions, AdminAuthOptions } from \"../../cmd-opts\";\nimport { outputText } from \"../../output\";\nimport { parseResourceId } from \"../../resource_id\";\n\nconst DefaultErrorMessage =\n \"Manual error set by administrator to trigger blocks re-run. \" +\n \"Run block once again to get rid of it.\";\n\nexport default function maintenanceSetResourceErrorCommand(): Command {\n const cmd = new Command(\"set-resource-error\").description(\n \"Put an error on one or more resources in a single transaction. \" +\n \"Resources that already have an error are skipped. \" +\n \"Requires admin/controller credentials.\",\n );\n\n cmd.argument(\n \"<resource-id...>\",\n \"Resource IDs: as printed by the server (NG:0x1A2B), as decimal numbers, \" +\n \"or as signed IDs (<decimal ID>|<signature hex>)\",\n );\n addOptions(cmd, AddressOptions(), AdminAuthOptions());\n cmd.addOption(\n new Option(\"-m, --error-message <message>\", \"Error message to put on the resources\").default(\n DefaultErrorMessage,\n ),\n );\n cmd.option(\n \"--skip-missing\",\n \"Skip resources that do not exist instead of failing the whole transaction\",\n false,\n );\n\n cmd.action(async (rawIds: string[], flags) => {\n // Parse all IDs before the connection, so that a typo does not leave a half-done job.\n const ids = rawIds.map(parseResourceId);\n\n const pl = await connectClient(flags);\n try {\n const result = await pl.withWriteTx(\"setResourceError\", async (tx) => {\n const found = await Promise.all(\n ids.map(async (id) => ({\n id,\n data: flags.skipMissing\n ? await tx.getResourceDataIfExists(id, true)\n : await tx.getResourceData(id, true, true),\n })),\n );\n\n const missing = found.filter((r) => r.data === undefined).map((r) => r.id);\n const resources = found.flatMap((r) => (r.data === undefined ? [] : [r.data]));\n const targets = resources.filter((r) => !isNotNullSignedResourceId(r.error));\n const skipped = resources.filter((r) => isNotNullSignedResourceId(r.error));\n\n if (targets.length > 0) {\n // One error resource for all targets: the backend links it to each target separately.\n const error = tx.createError(flags.errorMessage);\n for (const r of targets) tx.setResourceError(r.id, error);\n await tx.commit();\n }\n\n return { targets, skipped, missing };\n });\n\n for (const id of result.missing)\n outputText(`skipped ${resourceIdToString(id)}: resource does not exist`);\n for (const r of result.skipped)\n outputText(`skipped ${resourceIdToString(r.id)}: resource already has an error`);\n for (const r of result.targets) outputText(`error set ${resourceIdToString(r.id)}`);\n } finally {\n await pl.close();\n }\n });\n\n return cmd;\n}\n","import { Command } from \"commander\";\nimport projectListCommand from \"./cmd/project/list\";\nimport projectInfoCommand from \"./cmd/project/info\";\nimport projectDuplicateCommand from \"./cmd/project/duplicate\";\nimport projectRenameCommand from \"./cmd/project/rename\";\nimport projectDeleteCommand from \"./cmd/project/delete\";\nimport adminCopyProjectCommand from \"./cmd/admin/copy-project\";\nimport adminUserListCommand from \"./cmd/admin/user-list\";\nimport adminDeleteUserCommand from \"./cmd/admin/delete-user\";\nimport maintenanceListResourcesCommand from \"./cmd/maintenance/list-resources\";\nimport maintenanceSetResourceErrorCommand from \"./cmd/maintenance/set-resource-error\";\n\nexport function buildProgram(): Command {\n const program = new Command();\n program.name(\"pl-cli\").description(\"Platforma command-line client\");\n\n const project = new Command(\"project\").description(\"Manage projects\");\n project.addCommand(projectListCommand());\n project.addCommand(projectInfoCommand());\n project.addCommand(projectDuplicateCommand());\n project.addCommand(projectRenameCommand());\n project.addCommand(projectDeleteCommand());\n program.addCommand(project);\n\n const admin = new Command(\"admin\").description(\n \"Admin operations (requires controller credentials)\",\n );\n admin.addCommand(adminCopyProjectCommand());\n admin.addCommand(adminUserListCommand());\n admin.addCommand(adminDeleteUserCommand());\n program.addCommand(admin);\n\n const maintenance = new Command(\"maintenance\").description(\n \"Low-level server state repair (requires admin/controller credentials)\",\n );\n maintenance.addCommand(maintenanceListResourcesCommand());\n maintenance.addCommand(maintenanceSetResourceErrorCommand());\n program.addCommand(maintenance);\n\n return program;\n}\n\nexport async function run(argv: string[] = process.argv): Promise<void> {\n await buildProgram().parseAsync(argv);\n}\n"],"mappings":";;;;;;;;AAUA,eAAsB,EAAc,GAMd;AAQpB,QAPI,EAAM,aAAa,EAAM,gBACpB,EAAwB;EAC7B,SAAS,EAAM;EACf,WAAW,EAAM;EACjB,eAAe,EAAM;EACtB,CAAC,GAEG,GAAmB;EACxB,SAAS,EAAM;EACf,MAAM,EAAM;EACZ,UAAU,EAAM;EACjB,CAAC;;AAUJ,eAAsB,EAAQ,GAOkC;CAC9D,IAAM,IAAe,CAAC,CAAC,EAAM,WACvB,IAAmB,CAAC,CAAC,EAAM,eAC3B,IAAY,CAAC,CAAC,EAAM;AAG1B,KAAI,KAAa,EAAE,KAAgB,GACjC,OAAU,MAAM,2DAA2D;AAE7E,MAAK,KAAgB,MAAqB,CAAC,EACzC,OAAU,MAAM,2EAA2E;CAG7F,IAAM,IAAK,MAAM,EAAc,EAAM,EAEjC;AAQJ,QAPA,AAIE,IAJE,KACU,MAAM,EAAmB,GAAI,EAAM,WAAY,EACtC,iBAEJ,MAAM,EAAkB,EAAG,EAGvC;EAAE;EAAI;EAAgB;;;;AChE/B,SAAgB,EAAW,GAAc,GAAG,GAAmC;AAC7E,MAAK,IAAM,KAAS,EAClB,MAAK,IAAM,KAAO,EAAO,GAAI,UAAU,EAAI;AAE7C,QAAO;;AAIT,IAAa,UAAiC,CAC5C,IAAI,EAAO,uBAAuB,2BAA2B,CAC1D,IAAI,aAAa,CACjB,qBAAqB,CACzB,EAEY,KACX,IAA8B,QAC9B,IAAiC,CAAC,QAAQ,OAAO,KACpC,CACb,GAAG,GAAgB,EACnB,IAAI,EAAO,yBAAyB,gBAAgB,CACjD,QAAQ,EAAe,CACvB,QAAQ,EAAc,CAC1B,EAEY,UAAkC,CAC7C,IAAI,EAAO,qBAAqB,8BAA8B,CAAC,IAAI,UAAU,EAC7E,IAAI,EAAO,6BAA6B,8BAA8B,CAAC,IAAI,cAAc,CAC1F,EAGY,UAAmC,CAC9C,IAAI,EAAO,uBAAuB,4BAA4B,CAC3D,IAAI,gBAAgB,CACpB,qBAAqB,EACxB,IAAI,EAAO,+BAA+B,4BAA4B,CACnE,IAAI,oBAAoB,CACxB,qBAAqB,CACzB,EAGY,UAAqC;CAChD,IAAI,EAAO,uBAAuB,iDAAiD,CAAC,IAClF,gBACD;CACD,IAAI,EAAO,+BAA+B,4BAA4B,CAAC,IAAI,oBAAoB;CAC/F,IAAI,EACF,wBACA,2DACD,CAAC,IAAI,iBAAiB;CACxB;;;AC9CD,SAAwB,IAA8B;CACpD,IAAM,IAAM,IAAI,EAAQ,OAAO,CAAC,YAAY,gDAAgD;AA4C5F,QA1CA,EAAW,GAAK,GAAe,EAAE,GAAiB,EAAE,GAAoB,CAAC,EAEzE,EAAI,OAAO,OAAO,MAAU;EAC1B,IAAM,EAAE,OAAI,sBAAmB,MAAM,EAAQ,EAAM;AACnD,MAAI;GACF,IAAM,IAAW,MAAM,EAAa,GAAI,EAAe;AAEvD,OAAI,EAAM,WAAW,OACnB,GACE,EAAS,KAAK,OAAO;IACnB,IAAI,EAAE;IACN,KAAK,EAAmB,EAAE,IAAI;IAC9B,OAAO,EAAE;IACT,SAAS,EAAE,QAAQ,aAAa;IAChC,cAAc,EAAE,aAAa,aAAa;IAC3C,EAAE,CACJ;QACI;AACL,QAAI,EAAS,WAAW,GAAG;AACzB,OAAW,qBAAqB;AAChC;;AAeF,IAZA,EACE,EACE;KAAC;KAAM;KAAO;KAAS;KAAW;KAAgB,EAClD,EAAS,KAAK,MAAM;KAClB,EAAE;KACF,EAAmB,EAAE,IAAI;KACzB,EAAE;KACF,EAAW,EAAE,QAAQ;KACrB,EAAW,EAAE,aAAa;KAC3B,CAAC,CACH,CACF,EACD,EAAW,KAAK,EAAS,OAAO,aAAa;;YAEvC;AACR,SAAM,EAAG,OAAO;;GAElB,EAEK;;;;AC7CT,SAAwB,IAA8B;CACpD,IAAM,IAAM,IAAI,EAAQ,OAAO,CAAC,YAAY,6CAA6C;AAyCzF,QAvCA,EAAI,SAAS,aAAa,sBAAsB,EAChD,EAAW,GAAK,GAAe,EAAE,GAAiB,EAAE,GAAoB,CAAC,EAEzE,EAAI,OAAO,OAAO,GAAiB,MAAU;EAC3C,IAAM,EAAE,OAAI,sBAAmB,MAAM,EAAQ,EAAM;AACnD,MAAI;GACF,IAAM,EAAE,OAAI,WAAQ,MAAM,EAAe,GAAI,GAAgB,EAAQ,EAC/D,IAAO,MAAM,EAAe,GAAI,GAAI,EAAI;AAE9C,OAAI,EAAM,WAAW,OACnB,GAAW;IACT,IAAI,EAAK;IACT,KAAK,EAAmB,EAAK,IAAI;IACjC,OAAO,EAAK;IACZ,eAAe,EAAK;IACpB,YAAY,EAAK;IACjB,UAAU,EAAK;IACf,SAAS,EAAK,QAAQ,aAAa;IACnC,cAAc,EAAK,aAAa,aAAa;IAC9C,CAAC;QACG;AAML,QALA,EAAW,YAAY,EAAK,QAAQ,EACpC,EAAW,YAAY,EAAK,KAAK,EACjC,EAAW,YAAY,EAAmB,EAAK,IAAI,GAAG,EACtD,EAAW,YAAY,EAAK,iBAAiB,cAAc,EAC3D,EAAW,YAAY,EAAK,aAAa,EACrC,EAAK,SAAS,SAAS,EACzB,MAAK,IAAM,KAAO,EAAK,SACrB,GAAW,OAAO,IAAM;AAI5B,IADA,EAAW,kBAAkB,EAAW,EAAK,QAAQ,GAAG,EACxD,EAAW,kBAAkB,EAAW,EAAK,aAAa,GAAG;;YAEvD;AACR,SAAM,EAAG,OAAO;;GAElB,EAEK;;;;ACnCT,SAAwB,IAAmC;CACzD,IAAM,IAAM,IAAI,EAAQ,YAAY,CAAC,YACnC,kFACD;AAwDD,QAtDA,EAAI,SAAS,aAAa,sBAAsB,EAChD,EAAW,GAAK,GAAe,EAAE,GAAiB,EAAE,GAAoB,CAAC,EACzE,EAAI,OAAO,qBAAqB,yBAAyB,EACzD,EAAI,UACF,IAAI,EAAO,iBAAiB,2CAA2C,CAAC,QAAQ,GAAK,CACtF,EACD,EAAI,OAAO,oBAAoB,kDAAkD,EAEjF,EAAI,OAAO,OAAO,GAAiB,MAAU;EAC3C,IAAM,EAAE,OAAI,sBAAmB,MAAM,EAAQ,EAAM;AACnD,MAAI;GACF,IAAM,EAAE,KAAK,MAAc,MAAM,EAAe,GAAI,GAAgB,EAAQ,EAEtE,IAAQ,GAAY,EAEpB,IAAS,MAAM,EAAG,YAAY,oBAAoB,OAAO,MAAO;IACpE,IAAM,IAAgB,MAAM,EAAG,gBAAgB,GAAW,EAAe,EAEnE,IADa,KAAK,MAAM,EAAc,CACL,OAEjC,IAAiB,MAAM,EAAsB,GAAI,EAAe,EAGlE;AACJ,QAAI,EAAM,MAAM;AACd,SAAI,CAAC,EAAM,cAAc,EAAe,SAAS,EAAM,KAAK,CAC1D,OAAU,MAAM,iBAAiB,EAAM,KAAK,mBAAmB;AAEjE,SAAW,EAAe,SAAS,EAAM,KAAK,GAC1C,EAAgB,EAAM,MAAM,EAAe,GAC3C,EAAM;UAEV,KAAW,EAAgB,GAAa,EAAe;IAGzD,IAAM,IAAS,MAAM,EAAiB,GAAI,GAAW,EAAE,OAAO,GAAU,CAAC;AAEzE,IADA,EAAG,YAAY,EAAM,GAAgB,EAAM,EAAE,WAAW,EAAO,EAC/D,MAAM,EAAG,QAAQ;IAEjB,IAAM,IAAoB,MAAM,EAAO;AAEvC,WAAO;KAAE,IADS,EAAmB,EAAkB;KAC/B,KAAK;KAAmB,OAAO;KAAU;KACjE;AAEF,GAAI,EAAM,WAAW,SACnB,EAAW;IAAE,IAAI,EAAO;IAAI,KAAK,EAAmB,EAAO,IAAI;IAAE,OAAO,EAAO;IAAO,CAAC,GAEvF,EAAW,0BAA0B,EAAO,MAAM,SAAS,EAAO,GAAG,GAAG;YAElE;AACR,SAAM,EAAG,OAAO;;GAElB,EAEK;;;;AClET,SAAwB,IAAgC;CACtD,IAAM,IAAM,IAAI,EAAQ,SAAS,CAAC,YAAY,oBAAoB;AAuBlE,QArBA,EAAI,SAAS,aAAa,sBAAsB,EAChD,EAAW,GAAK,GAAe,EAAE,GAAiB,EAAE,GAAoB,CAAC,EACzE,EAAI,UAAU,IAAI,EAAO,qBAAqB,2BAA2B,CAAC,qBAAqB,CAAC,EAEhG,EAAI,OAAO,OAAO,GAAiB,MAAU;EAC3C,IAAM,EAAE,OAAI,sBAAmB,MAAM,EAAQ,EAAM;AACnD,MAAI;GACF,IAAM,EAAE,OAAI,WAAQ,MAAM,EAAe,GAAI,GAAgB,EAAQ;AAIrE,GAFA,MAAM,EAAc,GAAI,GAAK,EAAM,KAAK,EAEpC,EAAM,WAAW,SACnB,EAAW;IAAE;IAAI,KAAK,EAAmB,EAAI;IAAE,OAAO,EAAM;IAAM,CAAC,GAEnE,EAAW,uBAAuB,EAAM,KAAK,GAAG;YAE1C;AACR,SAAM,EAAG,OAAO;;GAElB,EAEK;;;;ACxBT,SAAwB,IAAgC;CACtD,IAAM,IAAM,IAAI,EAAQ,SAAS,CAAC,YAChC,gEACD;AA2CD,QAzCA,EAAI,SAAS,aAAa,sBAAsB,EAChD,EAAW,GAAK,GAAe,EAAE,GAAiB,EAAE,GAAoB,CAAC,EACzE,EAAI,OAAO,WAAW,qBAAqB,GAAM,EAEjD,EAAI,OAAO,OAAO,GAAiB,MAAU;EAC3C,IAAM,EAAE,OAAI,sBAAmB,MAAM,EAAQ,EAAM;AACnD,MAAI;GACF,IAAM,EAAE,OAAI,QAAK,iBAAc,MAAM,EAAe,GAAI,GAAgB,EAAQ,EAE1E,IAAO,MAAM,EAAe,GAAI,GAAI,EAAI;AAE9C,OAAI,CAAC,EAAM,OAAO;IAChB,IAAM,IAAK,EAAgB;KAAE,OAAO,QAAQ;KAAO,QAAQ,QAAQ;KAAQ,CAAC;AAC5E,QAAI;AAOF,UANe,MAAM,IAAI,SAAiB,MAAY;AACpD,QAAG,SACD,mBAAmB,EAAK,MAAM,KAAK,EAAK,WAAW,mBACnD,EACD;OACD,EACS,aAAa,KAAK,KAAK;AAChC,QAAW,WAAW;AACtB;;cAEM;AACR,OAAG,OAAO;;;AAMd,GAFA,MAAM,GAAc,GAAI,GAAgB,EAAU,EAE9C,EAAM,WAAW,SACnB,EAAW;IAAE,SAAS;IAAM;IAAI,OAAO,EAAK;IAAO,CAAC,GAEpD,EAAW,oBAAoB,EAAK,MAAM,GAAG;YAEvC;AACR,SAAM,EAAG,OAAO;;GAElB,EAEK;;;;ACtCT,SAAwB,IAAmC;CACzD,IAAM,IAAM,IAAI,EAAQ,eAAe,CAAC,YACtC,kFACD;AAkFD,QAhFA,EAAW,GAAK,GAAe,EAAE,GAAkB,CAAC,EACpD,EAAI,UACF,IAAI,EACF,wBACA,uCACD,CAAC,qBAAqB,CACxB,EACD,EAAI,UACF,IAAI,EAAO,8BAA8B,6BAA6B,CAAC,qBAAqB,CAC7F,EACD,EAAI,OACF,wBACA,2EACD,EACD,EAAI,OAAO,qBAAqB,8BAA8B,EAC9D,EAAI,UACF,IAAI,EAAO,iBAAiB,2CAA2C,CAAC,QAAQ,GAAK,CACtF,EACD,EAAI,OAAO,oBAAoB,kDAAkD,EAEjF,EAAI,OAAO,OAAO,MAAU;EAC1B,IAAM,IAAK,MAAM,EAAc,EAAM;AACrC,MAAI;GACF,IAAM,IAAa,EAAM,cAAc,EAAM,YAEvC,IAAS,MAAM,EAAmB,GAAI,EAAM,WAAW,EACvD,EAAE,KAAK,MAAc,MAAM,EAC/B,GACA,EAAO,gBACP,EAAM,cACP,EAEK,IACJ,MAAe,EAAM,aAAa,IAAS,MAAM,EAAmB,GAAI,EAAW,EAE/E,IAAQ,GAAY,EAEpB,IAAS,MAAM,EAAG,YAAY,oBAAoB,OAAO,MAAO;IACpE,IAAM,IAAgB,MAAM,EAAG,gBAAgB,GAAW,EAAe,EAEnE,IADa,KAAK,MAAM,EAAc,CACL,OAEjC,IAAiB,MAAM,EAAsB,GAAI,EAAO,eAAe,EAEzE,IAAmB,EAAM,QAAQ;AAErC,QAAI,EAAe,SAAS,EAAS,EAAE;AACrC,SAAI,CAAC,EAAM,WACT,OAAU,MAAM,iBAAiB,EAAS,mCAAmC;AAE/E,SAAW,EAAgB,EAAM,QAAQ,GAAa,EAAe;;IAGvE,IAAM,IAAS,MAAM,EAAiB,GAAI,GAAW,EAAE,OAAO,GAAU,CAAC;AAEzE,IADA,EAAG,YAAY,EAAM,EAAO,gBAAgB,EAAM,EAAE,WAAW,EAAO,EACtE,MAAM,EAAG,QAAQ;IAEjB,IAAM,IAAgB,MAAM,EAAO;AAEnC,WAAO;KAAE,IADK,EAAmB,EAAc;KAC3B,KAAK;KAAe,OAAO;KAAU;KACzD;AAEF,GAAI,EAAM,WAAW,SACnB,EAAW;IACT,IAAI,EAAO;IACX,KAAK,EAAmB,EAAO,IAAI;IACnC,OAAO,EAAO;IACd,YAAY,EAAM;IAClB;IACD,CAAC,GAEF,EACE,uBAAuB,EAAM,WAAW,MAAM,EAAW,OAAO,EAAO,MAAM,SAAS,EAAO,GAAG,SAAS,EAAmB,EAAO,IAAI,CAAC,GACzI;YAEK;AACR,SAAM,EAAG,OAAO;;GAElB,EAEK;;;;AC/FT,SAAwB,IAAgC;CACtD,IAAM,IAAM,IAAI,EAAQ,YAAY,CAAC,YACnC,0FACD;AAoBD,QAlBA,EAAW,GAAK,EAAc,OAAO;EAAC;EAAQ;EAAQ;EAAM,CAAC,EAAE,GAAkB,CAAC,EAElF,EAAI,OAAO,OAAO,MAAU;EAC1B,IAAM,IAAK,MAAM,EAAc,EAAM;AACrC,MAAI;GACF,IAAM,KAAU,MAAM,EAAG,WAAW,EAAE,KAAK,MAAM,EAAE,MAAM,CAAC,MAAM;AAChE,GAAI,EAAM,WAAW,SAAQ,EAAW,EAAO,GACtC,EAAM,WAAW,SAAQ,EAAW,EAAO,KAAK,KAAK,CAAC,GAE7D,GACE,CAAC,QAAQ,EACT,EAAO,KAAK,MAAM,CAAC,EAAE,CAAC,CACvB;YACK;AACR,SAAM,EAAG,OAAO;;GAElB,EAEK;;;;ACfT,SAAwB,IAAkC;CACxD,IAAM,IAAM,IAAI,EAAQ,cAAc,CAAC,YACrC,+HAED;AAqGD,QAnGA,EAAI,SAAS,UAAU,oCAAoC,EAC3D,EAAW,GAAK,GAAe,EAAE,GAAkB,CAAC,EACpD,EAAI,UACF,IAAI,EACF,6BACA,+EACD,CACF,EACD,EAAI,UACF,IAAI,EACF,qBACA,uEACD,CACF,EACD,EAAI,OAAO,WAAW,qBAAqB,GAAM,EAEjD,EAAI,OAAO,OAAO,GAAc,MAAU;AAGxC,MAAI,EAAM,kBAAkB,EAAM,eAChC,OAAU,MAAM,kEAAkE;AAEpF,MAAI,EAAM,mBAAmB,EAC3B,OAAU,MAAM,2EAA2E;AAI7F,MAAI,EAAM,cAAc,EACtB,OAAU,MACR,uBAAuB,EAAK,0DAC7B;EAGH,IAAM,IAAK,MAAM,EAAc,EAAM;AACrC,MAAI;GACF,IAAM,IAAS,MAAM,EAAa,GAAI,EAAK,EAGrC,IACJ,EAAO,mBAAmB,KAAA,IACtB,EAAE,GACF,MAAM,EAAsB,GAAI,EAAO,eAAe;AAK5D,OAAI,EAAS,SAAS,KAAK,CAAC,EAAM,kBAAkB,CAAC,EAAM,eACzD,OAAU,MACR,SAAS,EAAK,SAAS,EAAS,OAAO,oHAExC;AAIH,OAAI,CADc,MAAM,GAAgB,GAAM,GAAU,EAAM,EAC9C;AACd,MAAW,WAAW;AACtB;;GAGF,IAAI,IAAwB,EAAE;AAC9B,OAAI,EAAM,kBAAkB,EAAS,SAAS,GAAG;IAI/C,IAAM,IAAa,MAAM,EAAsB,IAHhC,MAAM,EAAa,GAAI,EAAM,eAAe,EAGD,SAAS;AACnE,QAAQ,MAAM,EAAa,GAAI,EAAO,gBAAiB,EAAW;;GAMpE,IAAM,IACJ,EAAO,mBAAmB,KAAA,IACtB,EAAE,GACF,MAAM,EAAsB,GAAI,EAAO,eAAe;AAC5D,KAA2B,GAAM,GAAO,GAAU,EAAU;GAE5D,IAAM,IAAS,MAAM,EAAG,WAAW,EAAK;AAExC,GAAI,EAAM,WAAW,SACnB,EAAW;IACT,SAAS;IACT;IACA,SAAS,EAAM,kBAAkB;IACjC,eAAe;IACf,iBAAiB,EAAM,iBAAiB,EAAE,GAAG,EAAU,KAAK,MAAM,EAAE,MAAM;IAC1E,YAAY,EAAO,eAAe,KAAA,IAAY,OAAO,EAAO,WAAW,UAAU;IACjF,iBAAiB,EAAO;IACxB,eAAe,EAAO;IACtB,6BAA6B,EAAO;IACrC,CAAC,GAEF,EAAW,GAAa,GAAM,EAAM,gBAAgB,GAAO,EAAU,CAAC;YAEhE;AACR,SAAM,EAAG,OAAO;;GAElB,EAEK;;AAST,SAAS,EACP,GACA,GACA,GACA,GACM;AACN,KAAI,EAAM,gBAAgB;AACxB,MAAI,EAAU,WAAW,EAAG;AAC5B,QAAU,MACR,SAAS,EAAK,eAAe,EAAU,OAAO,8BACzC,EAAU,KAAK,MAAM,EAAE,MAAM,CAAC,KAAK,KAAK,CAAC,qHAE/C;;CAGH,IAAM,IAAc,IAAI,IAAI,EAAS,KAAK,MAAM,EAAE,GAAG,CAAC,EAChD,IAAa,EAAU,QAAQ,MAAM,CAAC,EAAY,IAAI,EAAE,GAAG,CAAC;AAC9D,OAAW,WAAW,EAC1B,OAAU,MACR,SAAS,EAAK,WAAW,EAAW,OAAO,4CACtC,EAAW,KAAK,MAAM,EAAE,MAAM,CAAC,KAAK,KAAK,CAAC,4HAEhD;;AAIH,eAAe,GACb,GACA,GACA,GACkB;AAClB,KAAI,EAAM,MAAO,QAAO;CAExB,IAAM,IAAO,EAAM,iBACf,GAAG,EAAS,OAAO,4BAA4B,EAAM,eAAe,KACpE,EAAS,SAAS,IAChB,GAAG,EAAS,OAAO,2CACnB;AAGN,CADA,QAAQ,OAAO,MAAM,gBAAgB,EAAK,KAAK,EAAK,KAAK,EACrD,EAAS,SAAS,KACpB,QAAQ,OAAO,MAAM,EAAS,KAAK,MAAM,OAAO,EAAE,QAAQ,CAAC,KAAK,KAAK,GAAG,KAAK;CAG/E,IAAM,IAAK,EAAgB;EAAE,OAAO,QAAQ;EAAO,QAAQ,QAAQ;EAAQ,CAAC;AAC5E,KAAI;AAIF,UAHe,MAAM,IAAI,SAAiB,MAAY;AACpD,KAAG,SAAS,mBAAmB,EAAQ;IACvC,EACY,aAAa,KAAK;WACxB;AACR,IAAG,OAAO;;;AAId,SAAS,GACP,GACA,GACA,GACA,GACQ;CACR,IAAM,IAAkB,EAAE;AAoB1B,QAlBI,IACE,EAAM,SAAS,KACjB,EAAM,KAAK,SAAS,EAAM,OAAO,mBAAmB,EAAK,MAAM,EAAQ,GAAG,EAC1E,EAAM,KACJ,EACE;EAAC;EAAM;EAAQ;EAAiB,EAChC,EAAM,KAAK,MAAM;EAAC,EAAE;EAAI,EAAE;EAAa,EAAE;EAAY,CAAC,CACvD,CACF,IAED,EAAM,KAAK,QAAQ,EAAK,2BAA2B,GAE5C,EAAS,SAAS,MAC3B,EAAM,KAAK,WAAW,EAAS,OAAO,2BAA2B,EAAK,GAAG,EACzE,EAAM,KAAK,EAAS,KAAK,MAAM,OAAO,EAAE,QAAQ,CAAC,KAAK,KAAK,CAAC,GAG9D,EAAM,KAAK,iBAAiB,EAAK,IAAI,EAC9B,EAAM,KAAK,KAAK;;;;ACpMzB,SAAgB,EAAgB,GAA+B;CAC7D,IAAM,IAAM,EAAI,MAAM;AACtB,KAAI,EAAI,SAAS,IAAI,CAAE,QAAO,EAAmB,EAAI;CAErD,IAAI,IAAyB,EAAqB,EAAI;AAEtD,KADI,MAAO,KAAA,KAAa,QAAQ,KAAK,EAAI,KAAE,IAAK,OAAO,EAAI,GACvD,MAAO,KAAA,KAAa,MAAO,MAAM,GAAkB,EAAG,CACxD,OAAU,MAAM,6BAA6B,IAAM;AAErD,QAAO,EAAuB,EAAG;;;;ACJnC,SAAwB,KAA2C;CACjE,IAAM,IAAM,IAAI,EAAQ,iBAAiB,CAAC,YACxC,mRAID;AAsDD,QApDA,EAAW,GAAK,GAAgB,EAAE,GAAkB,CAAC,EACrD,EAAI,UACF,IAAI,EAAO,yBAAyB,6CAA6C,CAC9E,UAAU,GAAqB,CAC/B,qBAAqB,CACzB,EACD,EAAI,UACF,IAAI,EAAO,eAAe,6DAA6D,CACpF,UAAU,GAAoB,CAC9B,QAAQ,EAAE,CACd,EACD,EAAI,UACF,IAAI,EACF,yBACA,iIAED,CAAC,UAAU,EAAgB,CAC7B,EACD,EAAI,OACF,iBACA,8EACA,GACD,EACD,EAAI,OAAO,aAAa,0DAA0D,GAAM,EAExF,EAAI,OAAO,OAAO,MAAU;EAC1B,IAAM,IAAK,MAAM,EAAc,EAAM;AACrC,MAAI;GACF,IAAM,IAAY,MAAM,EAAG,WAAW,iBAAiB,OAAO,MAAO;IACnE,IAAM,IAAO,MAAM,EAAG,oBAAoB,EAAM,MAAM;KACpD,OAAO,EAAM;KACb,OAAO,EAAM;KACb,YAAY,EAAM;KACnB,CAAC;AACF,WAAO,MAAM,QAAQ,IACnB,EAAK,IAAI,OAAO,OAAc;KAC5B;KACA,IAAI,EAAM,SAAS,MAAM,EAAG,cAAc,EAAS,GAAG,GAAG,KAAA;KAC1D,EAAE,CACJ;KACD;AAEF,QAAK,IAAM,EAAE,aAAU,WAAQ,GAAW;IACxC,IAAM,IAAO,KAAK,UAAU,GAAO,GAAU,EAAM,YAAY,EAAG,CAAC,GAAG;AAEtE,IAAK,QAAQ,OAAO,MAAM,EAAK,IAAE,MAAM,EAAK,QAAQ,QAAQ,QAAQ;;YAE9D;AACR,SAAM,EAAG,OAAO;;GAElB,EAEK;;AAGT,SAAS,GACP,GACA,GACA,GACyB;AACzB,QAAO;EACL,IAAI,EAAmB,EAAE,GAAG;EAC5B,MAAM,EAAqB,EAAE,KAAK;EAClC,MAAM,EAAE;EACR,YAAY,EAAmB,EAAE,mBAAmB;EACpD,cAAc,EAAE;EAChB,eAAe,EAAE;EACjB,OAAO,EAAE;EACT,OAAO,EAAE;EACT,MAAM,EAAE,SAAS,KAAA,KAAa,EAAE,KAAK,WAAW,IAAI,OAAO,EAAY,EAAE,KAAK;EAE9E,GAAI,IACA;GAAE,OAAO,EAAmB,EAAE,MAAM;GAAE,QAAQ,EAAE,OAAO,IAAI,GAAY;GAAE,GACzE,EAAE;EACN,GAAI,MAAO,KAAA,IACP,EAAE,GACF,EAAE,IAAI,OAAO,YAAY,EAAG,KAAK,MAAM,CAAC,EAAE,KAAK,EAAY,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE;EAC7E;;AAGH,SAAS,GAAY,GAAuC;AAC1D,QAAO;EACL,MAAM,EAAE;EACR,MAAM,EAAE;EACR,QAAQ,EAAE;EACV,OAAO,EAAmB,EAAE,MAAM;EAClC,OAAO,EAAmB,EAAE,MAAM;EAClC,cAAc,EAAE;EACjB;;AAGH,SAAS,EAAmB,GAA6C;AACvE,QAAO,EAA0B,EAAG,GAAG,EAAmB,EAAG,GAAG;;AAIlE,SAAS,EAAY,GAA4B;CAC/C,IAAI;AACJ,KAAI;AACF,MAAO,IAAI,YAAY,SAAS,EAAE,OAAO,IAAM,CAAC,CAAC,OAAO,EAAM;SACxD;AACN,SAAO,EAAE,QAAQ,OAAO,KAAK,EAAM,CAAC,SAAS,SAAS,EAAE;;AAE1D,KAAI;AACF,SAAO,KAAK,MAAM,EAAK;SACjB;AACN,SAAO;;;AAIX,SAAS,GAAqB,GAA6B;CACzD,IAAM,IAAM,EAAM,YAAY,IAAI;AAClC,KAAI,KAAO,KAAK,MAAQ,EAAM,SAAS,EACrC,OAAM,IAAI,EAAqB,kDAAkD,IAAQ;AAC3F,QAAO,EAAa,EAAM,MAAM,GAAG,EAAI,EAAE,EAAM,MAAM,IAAM,EAAE,CAAC;;AAGhE,SAAS,GAAoB,GAAuB;CAClD,IAAM,IAAI,OAAO,EAAM;AACvB,KAAI,CAAC,OAAO,UAAU,EAAE,IAAI,IAAI,EAC9B,OAAM,IAAI,EAAqB,+BAA+B,IAAQ;AACxE,QAAO;;;;AC3IT,IAAM,KACJ;AAGF,SAAwB,KAA8C;CACpE,IAAM,IAAM,IAAI,EAAQ,qBAAqB,CAAC,YAC5C,0JAGD;AA4DD,QA1DA,EAAI,SACF,oBACA,0HAED,EACD,EAAW,GAAK,GAAgB,EAAE,GAAkB,CAAC,EACrD,EAAI,UACF,IAAI,EAAO,iCAAiC,wCAAwC,CAAC,QACnF,GACD,CACF,EACD,EAAI,OACF,kBACA,6EACA,GACD,EAED,EAAI,OAAO,OAAO,GAAkB,MAAU;EAE5C,IAAM,IAAM,EAAO,IAAI,EAAgB,EAEjC,IAAK,MAAM,EAAc,EAAM;AACrC,MAAI;GACF,IAAM,IAAS,MAAM,EAAG,YAAY,oBAAoB,OAAO,MAAO;IACpE,IAAM,IAAQ,MAAM,QAAQ,IAC1B,EAAI,IAAI,OAAO,OAAQ;KACrB;KACA,MAAM,EAAM,cACR,MAAM,EAAG,wBAAwB,GAAI,GAAK,GAC1C,MAAM,EAAG,gBAAgB,GAAI,IAAM,GAAK;KAC7C,EAAE,CACJ,EAEK,IAAU,EAAM,QAAQ,MAAM,EAAE,SAAS,KAAA,EAAU,CAAC,KAAK,MAAM,EAAE,GAAG,EACpE,IAAY,EAAM,SAAS,MAAO,EAAE,SAAS,KAAA,IAAY,EAAE,GAAG,CAAC,EAAE,KAAK,CAAE,EACxE,IAAU,EAAU,QAAQ,MAAM,CAAC,EAA0B,EAAE,MAAM,CAAC,EACtE,IAAU,EAAU,QAAQ,MAAM,EAA0B,EAAE,MAAM,CAAC;AAE3E,QAAI,EAAQ,SAAS,GAAG;KAEtB,IAAM,IAAQ,EAAG,YAAY,EAAM,aAAa;AAChD,UAAK,IAAM,KAAK,EAAS,GAAG,iBAAiB,EAAE,IAAI,EAAM;AACzD,WAAM,EAAG,QAAQ;;AAGnB,WAAO;KAAE;KAAS;KAAS;KAAS;KACpC;AAEF,QAAK,IAAM,KAAM,EAAO,QACtB,GAAW,WAAW,EAAmB,EAAG,CAAC,2BAA2B;AAC1E,QAAK,IAAM,KAAK,EAAO,QACrB,GAAW,WAAW,EAAmB,EAAE,GAAG,CAAC,iCAAiC;AAClF,QAAK,IAAM,KAAK,EAAO,QAAS,GAAW,aAAa,EAAmB,EAAE,GAAG,GAAG;YAC3E;AACR,SAAM,EAAG,OAAO;;GAElB,EAEK;;;;AChET,SAAgB,IAAwB;CACtC,IAAM,IAAU,IAAI,GAAS;AAC7B,GAAQ,KAAK,SAAS,CAAC,YAAY,gCAAgC;CAEnE,IAAM,IAAU,IAAI,EAAQ,UAAU,CAAC,YAAY,kBAAkB;AAMrE,CALA,EAAQ,WAAW,GAAoB,CAAC,EACxC,EAAQ,WAAW,GAAoB,CAAC,EACxC,EAAQ,WAAW,GAAyB,CAAC,EAC7C,EAAQ,WAAW,GAAsB,CAAC,EAC1C,EAAQ,WAAW,GAAsB,CAAC,EAC1C,EAAQ,WAAW,EAAQ;CAE3B,IAAM,IAAQ,IAAI,EAAQ,QAAQ,CAAC,YACjC,qDACD;AAID,CAHA,EAAM,WAAW,GAAyB,CAAC,EAC3C,EAAM,WAAW,GAAsB,CAAC,EACxC,EAAM,WAAW,GAAwB,CAAC,EAC1C,EAAQ,WAAW,EAAM;CAEzB,IAAM,IAAc,IAAI,EAAQ,cAAc,CAAC,YAC7C,wEACD;AAKD,QAJA,EAAY,WAAW,IAAiC,CAAC,EACzD,EAAY,WAAW,IAAoC,CAAC,EAC5D,EAAQ,WAAW,EAAY,EAExB;;AAGT,eAAsB,GAAI,IAAiB,QAAQ,MAAqB;AACtE,OAAM,GAAc,CAAC,WAAW,EAAK"}
|
package/dist/cmd-opts.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { Command, Option } from 'commander';
|
|
|
2
2
|
import { OutputFormat } from './output';
|
|
3
3
|
/** Add one or more groups of options to a command, preserving group order. */
|
|
4
4
|
export declare function addOptions(cmd: Command, ...optionGroups: Option[][]): Command;
|
|
5
|
+
/** Server address only (for commands with a fixed output format). */
|
|
6
|
+
export declare const AddressOptions: () => Option[];
|
|
5
7
|
export declare const GlobalOptions: (formatDefault?: OutputFormat, allowedFormats?: OutputFormat[]) => Option[];
|
|
6
8
|
export declare const UserAuthOptions: () => Option[];
|
|
7
9
|
/** Admin credentials only (for purely admin commands like copy-project). */
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { SignedResourceId } from '@milaboratories/pl-client';
|
|
2
|
+
/**
|
|
3
|
+
* Admin and controller roles do not need resource signatures: an ID without a signature
|
|
4
|
+
* is accepted by the backend for these roles.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseResourceId(raw: string): SignedResourceId;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platforma-sdk/pl-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "CLI for Platforma server state manipulation",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"bin": {
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"commander": "^15.0.0",
|
|
26
|
-
"@milaboratories/pl-middle-layer": "1.74.
|
|
27
|
-
"@milaboratories/pl-client": "3.
|
|
26
|
+
"@milaboratories/pl-middle-layer": "1.74.7",
|
|
27
|
+
"@milaboratories/pl-client": "3.19.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/node": "~24.5.2",
|
|
@@ -34,8 +34,13 @@
|
|
|
34
34
|
"vite-plugin-dts": "^5.0.3",
|
|
35
35
|
"vitest": "^4.1.3",
|
|
36
36
|
"@milaboratories/build-configs": "2.0.2",
|
|
37
|
-
"@milaboratories/ts-
|
|
38
|
-
"@milaboratories/ts-
|
|
37
|
+
"@milaboratories/ts-configs": "1.4.0",
|
|
38
|
+
"@milaboratories/ts-builder": "1.7.2"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/milaboratory/platforma.git",
|
|
43
|
+
"directory": "tools/pl-cli"
|
|
39
44
|
},
|
|
40
45
|
"scripts": {
|
|
41
46
|
"build": "vite build",
|
package/src/cli.ts
CHANGED
|
@@ -7,6 +7,8 @@ import projectDeleteCommand from "./cmd/project/delete";
|
|
|
7
7
|
import adminCopyProjectCommand from "./cmd/admin/copy-project";
|
|
8
8
|
import adminUserListCommand from "./cmd/admin/user-list";
|
|
9
9
|
import adminDeleteUserCommand from "./cmd/admin/delete-user";
|
|
10
|
+
import maintenanceListResourcesCommand from "./cmd/maintenance/list-resources";
|
|
11
|
+
import maintenanceSetResourceErrorCommand from "./cmd/maintenance/set-resource-error";
|
|
10
12
|
|
|
11
13
|
export function buildProgram(): Command {
|
|
12
14
|
const program = new Command();
|
|
@@ -28,6 +30,13 @@ export function buildProgram(): Command {
|
|
|
28
30
|
admin.addCommand(adminDeleteUserCommand());
|
|
29
31
|
program.addCommand(admin);
|
|
30
32
|
|
|
33
|
+
const maintenance = new Command("maintenance").description(
|
|
34
|
+
"Low-level server state repair (requires admin/controller credentials)",
|
|
35
|
+
);
|
|
36
|
+
maintenance.addCommand(maintenanceListResourcesCommand());
|
|
37
|
+
maintenance.addCommand(maintenanceSetResourceErrorCommand());
|
|
38
|
+
program.addCommand(maintenance);
|
|
39
|
+
|
|
31
40
|
return program;
|
|
32
41
|
}
|
|
33
42
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { once } from "node:events";
|
|
2
|
+
import { Command, InvalidArgumentError, Option } from "commander";
|
|
3
|
+
import {
|
|
4
|
+
isNotNullSignedResourceId,
|
|
5
|
+
resourceIdToString,
|
|
6
|
+
resourceType,
|
|
7
|
+
resourceTypeToString,
|
|
8
|
+
type FieldData,
|
|
9
|
+
type KeyValue,
|
|
10
|
+
type OptionalSignedResourceId,
|
|
11
|
+
type ResourceData,
|
|
12
|
+
type ResourceType,
|
|
13
|
+
} from "@milaboratories/pl-client";
|
|
14
|
+
import { connectClient } from "../../base_command";
|
|
15
|
+
import { addOptions, AddressOptions, AdminAuthOptions } from "../../cmd-opts";
|
|
16
|
+
import { parseResourceId } from "../../resource_id";
|
|
17
|
+
|
|
18
|
+
export default function maintenanceListResourcesCommand(): Command {
|
|
19
|
+
const cmd = new Command("list-resources").description(
|
|
20
|
+
"List resources of one type as JSON lines, in ascending order of resource ID. " +
|
|
21
|
+
"Resource data and key-value values are printed as JSON when they are valid JSON, " +
|
|
22
|
+
'as text when they are valid UTF-8, and as {"base64": ...} in other cases. ' +
|
|
23
|
+
"Requires admin/controller credentials.",
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
addOptions(cmd, AddressOptions(), AdminAuthOptions());
|
|
27
|
+
cmd.addOption(
|
|
28
|
+
new Option("--type <name:version>", "Resource type to list (e.g. 'Blob/work:1')")
|
|
29
|
+
.argParser(parseResourceTypeArg)
|
|
30
|
+
.makeOptionMandatory(),
|
|
31
|
+
);
|
|
32
|
+
cmd.addOption(
|
|
33
|
+
new Option("--limit <n>", "Maximum number of resources to list. 0 lists all resources")
|
|
34
|
+
.argParser(parseNonNegativeInt)
|
|
35
|
+
.default(0),
|
|
36
|
+
);
|
|
37
|
+
cmd.addOption(
|
|
38
|
+
new Option(
|
|
39
|
+
"--after <resource-id>",
|
|
40
|
+
"List only resources with IDs greater than this ID. " +
|
|
41
|
+
"To get the next page, give the ID of the last resource of the previous page",
|
|
42
|
+
).argParser(parseResourceId),
|
|
43
|
+
);
|
|
44
|
+
cmd.option(
|
|
45
|
+
"--load-fields",
|
|
46
|
+
"Load the fields and the error of each resource ('fields' and 'error' keys)",
|
|
47
|
+
false,
|
|
48
|
+
);
|
|
49
|
+
cmd.option("--load-kv", "Load the key-value entries of each resource ('kv' key)", false);
|
|
50
|
+
|
|
51
|
+
cmd.action(async (flags) => {
|
|
52
|
+
const pl = await connectClient(flags);
|
|
53
|
+
try {
|
|
54
|
+
const resources = await pl.withReadTx("listResources", async (tx) => {
|
|
55
|
+
const list = await tx.listResourcesByType(flags.type, {
|
|
56
|
+
limit: flags.limit,
|
|
57
|
+
after: flags.after,
|
|
58
|
+
loadFields: flags.loadFields,
|
|
59
|
+
});
|
|
60
|
+
return await Promise.all(
|
|
61
|
+
list.map(async (resource) => ({
|
|
62
|
+
resource,
|
|
63
|
+
kv: flags.loadKv ? await tx.listKeyValues(resource.id) : undefined,
|
|
64
|
+
})),
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
for (const { resource, kv } of resources) {
|
|
69
|
+
const line = JSON.stringify(toJson(resource, flags.loadFields, kv)) + "\n";
|
|
70
|
+
// Wait for a slow reader: else the output collects in memory a second time.
|
|
71
|
+
if (!process.stdout.write(line)) await once(process.stdout, "drain");
|
|
72
|
+
}
|
|
73
|
+
} finally {
|
|
74
|
+
await pl.close();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return cmd;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toJson(
|
|
82
|
+
r: ResourceData,
|
|
83
|
+
withFields: boolean,
|
|
84
|
+
kv: KeyValue[] | undefined,
|
|
85
|
+
): Record<string, unknown> {
|
|
86
|
+
return {
|
|
87
|
+
id: resourceIdToString(r.id),
|
|
88
|
+
type: resourceTypeToString(r.type),
|
|
89
|
+
kind: r.kind,
|
|
90
|
+
originalId: optionalIdToString(r.originalResourceId),
|
|
91
|
+
inputsLocked: r.inputsLocked,
|
|
92
|
+
outputsLocked: r.outputsLocked,
|
|
93
|
+
ready: r.resourceReady,
|
|
94
|
+
final: r.final,
|
|
95
|
+
data: r.data === undefined || r.data.length === 0 ? null : decodeBytes(r.data),
|
|
96
|
+
// The backend sends the resource error as a field: without fields it is not known.
|
|
97
|
+
...(withFields
|
|
98
|
+
? { error: optionalIdToString(r.error), fields: r.fields.map(fieldToJson) }
|
|
99
|
+
: {}),
|
|
100
|
+
...(kv === undefined
|
|
101
|
+
? {}
|
|
102
|
+
: { kv: Object.fromEntries(kv.map((e) => [e.key, decodeBytes(e.value)])) }),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function fieldToJson(f: FieldData): Record<string, unknown> {
|
|
107
|
+
return {
|
|
108
|
+
name: f.name,
|
|
109
|
+
type: f.type,
|
|
110
|
+
status: f.status,
|
|
111
|
+
value: optionalIdToString(f.value),
|
|
112
|
+
error: optionalIdToString(f.error),
|
|
113
|
+
valueIsFinal: f.valueIsFinal,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function optionalIdToString(id: OptionalSignedResourceId): string | null {
|
|
118
|
+
return isNotNullSignedResourceId(id) ? resourceIdToString(id) : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** JSON value when the bytes are valid JSON, text when valid UTF-8, {"base64": ...} otherwise. */
|
|
122
|
+
function decodeBytes(bytes: Uint8Array): unknown {
|
|
123
|
+
let text: string;
|
|
124
|
+
try {
|
|
125
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
126
|
+
} catch {
|
|
127
|
+
return { base64: Buffer.from(bytes).toString("base64") };
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
return JSON.parse(text);
|
|
131
|
+
} catch {
|
|
132
|
+
return text;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseResourceTypeArg(value: string): ResourceType {
|
|
137
|
+
const sep = value.lastIndexOf(":");
|
|
138
|
+
if (sep <= 0 || sep === value.length - 1)
|
|
139
|
+
throw new InvalidArgumentError(`expected <name>:<version>, e.g. 'Blob/work:1': ${value}`);
|
|
140
|
+
return resourceType(value.slice(0, sep), value.slice(sep + 1));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseNonNegativeInt(value: string): number {
|
|
144
|
+
const n = Number(value);
|
|
145
|
+
if (!Number.isInteger(n) || n < 0)
|
|
146
|
+
throw new InvalidArgumentError(`not a non-negative integer: ${value}`);
|
|
147
|
+
return n;
|
|
148
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Command, Option } from "commander";
|
|
2
|
+
import { isNotNullSignedResourceId, resourceIdToString } from "@milaboratories/pl-client";
|
|
3
|
+
import { connectClient } from "../../base_command";
|
|
4
|
+
import { addOptions, AddressOptions, AdminAuthOptions } from "../../cmd-opts";
|
|
5
|
+
import { outputText } from "../../output";
|
|
6
|
+
import { parseResourceId } from "../../resource_id";
|
|
7
|
+
|
|
8
|
+
const DefaultErrorMessage =
|
|
9
|
+
"Manual error set by administrator to trigger blocks re-run. " +
|
|
10
|
+
"Run block once again to get rid of it.";
|
|
11
|
+
|
|
12
|
+
export default function maintenanceSetResourceErrorCommand(): Command {
|
|
13
|
+
const cmd = new Command("set-resource-error").description(
|
|
14
|
+
"Put an error on one or more resources in a single transaction. " +
|
|
15
|
+
"Resources that already have an error are skipped. " +
|
|
16
|
+
"Requires admin/controller credentials.",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
cmd.argument(
|
|
20
|
+
"<resource-id...>",
|
|
21
|
+
"Resource IDs: as printed by the server (NG:0x1A2B), as decimal numbers, " +
|
|
22
|
+
"or as signed IDs (<decimal ID>|<signature hex>)",
|
|
23
|
+
);
|
|
24
|
+
addOptions(cmd, AddressOptions(), AdminAuthOptions());
|
|
25
|
+
cmd.addOption(
|
|
26
|
+
new Option("-m, --error-message <message>", "Error message to put on the resources").default(
|
|
27
|
+
DefaultErrorMessage,
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
cmd.option(
|
|
31
|
+
"--skip-missing",
|
|
32
|
+
"Skip resources that do not exist instead of failing the whole transaction",
|
|
33
|
+
false,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
cmd.action(async (rawIds: string[], flags) => {
|
|
37
|
+
// Parse all IDs before the connection, so that a typo does not leave a half-done job.
|
|
38
|
+
const ids = rawIds.map(parseResourceId);
|
|
39
|
+
|
|
40
|
+
const pl = await connectClient(flags);
|
|
41
|
+
try {
|
|
42
|
+
const result = await pl.withWriteTx("setResourceError", async (tx) => {
|
|
43
|
+
const found = await Promise.all(
|
|
44
|
+
ids.map(async (id) => ({
|
|
45
|
+
id,
|
|
46
|
+
data: flags.skipMissing
|
|
47
|
+
? await tx.getResourceDataIfExists(id, true)
|
|
48
|
+
: await tx.getResourceData(id, true, true),
|
|
49
|
+
})),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const missing = found.filter((r) => r.data === undefined).map((r) => r.id);
|
|
53
|
+
const resources = found.flatMap((r) => (r.data === undefined ? [] : [r.data]));
|
|
54
|
+
const targets = resources.filter((r) => !isNotNullSignedResourceId(r.error));
|
|
55
|
+
const skipped = resources.filter((r) => isNotNullSignedResourceId(r.error));
|
|
56
|
+
|
|
57
|
+
if (targets.length > 0) {
|
|
58
|
+
// One error resource for all targets: the backend links it to each target separately.
|
|
59
|
+
const error = tx.createError(flags.errorMessage);
|
|
60
|
+
for (const r of targets) tx.setResourceError(r.id, error);
|
|
61
|
+
await tx.commit();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { targets, skipped, missing };
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
for (const id of result.missing)
|
|
68
|
+
outputText(`skipped ${resourceIdToString(id)}: resource does not exist`);
|
|
69
|
+
for (const r of result.skipped)
|
|
70
|
+
outputText(`skipped ${resourceIdToString(r.id)}: resource already has an error`);
|
|
71
|
+
for (const r of result.targets) outputText(`error set ${resourceIdToString(r.id)}`);
|
|
72
|
+
} finally {
|
|
73
|
+
await pl.close();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return cmd;
|
|
78
|
+
}
|
package/src/cmd-opts.ts
CHANGED
|
@@ -9,13 +9,18 @@ export function addOptions(cmd: Command, ...optionGroups: Option[][]): Command {
|
|
|
9
9
|
return cmd;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/** Server address only (for commands with a fixed output format). */
|
|
13
|
+
export const AddressOptions = (): Option[] => [
|
|
14
|
+
new Option("-a, --address <url>", "Platforma server address")
|
|
15
|
+
.env("PL_ADDRESS")
|
|
16
|
+
.makeOptionMandatory(),
|
|
17
|
+
];
|
|
18
|
+
|
|
12
19
|
export const GlobalOptions = (
|
|
13
20
|
formatDefault: OutputFormat = "text",
|
|
14
21
|
allowedFormats: OutputFormat[] = ["text", "json"],
|
|
15
22
|
): Option[] => [
|
|
16
|
-
|
|
17
|
-
.env("PL_ADDRESS")
|
|
18
|
-
.makeOptionMandatory(),
|
|
23
|
+
...AddressOptions(),
|
|
19
24
|
new Option("-f, --format <format>", "Output format")
|
|
20
25
|
.choices(allowedFormats)
|
|
21
26
|
.default(formatDefault),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
asSignedResourceId,
|
|
3
|
+
createSignedResourceId,
|
|
4
|
+
isLocalResourceId,
|
|
5
|
+
resourceIdFromString,
|
|
6
|
+
type SignedResourceId,
|
|
7
|
+
} from "@milaboratories/pl-client";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Admin and controller roles do not need resource signatures: an ID without a signature
|
|
11
|
+
* is accepted by the backend for these roles.
|
|
12
|
+
*/
|
|
13
|
+
export function parseResourceId(raw: string): SignedResourceId {
|
|
14
|
+
const str = raw.trim();
|
|
15
|
+
if (str.includes("|")) return asSignedResourceId(str);
|
|
16
|
+
|
|
17
|
+
let id: bigint | undefined = resourceIdFromString(str);
|
|
18
|
+
if (id === undefined && /^\d+$/.test(str)) id = BigInt(str);
|
|
19
|
+
if (id === undefined || id === 0n || isLocalResourceId(id))
|
|
20
|
+
throw new Error(`not a global resource ID: ${raw}`);
|
|
21
|
+
|
|
22
|
+
return createSignedResourceId(id);
|
|
23
|
+
}
|