@platforma-sdk/pl-cli 0.9.42 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 m, s as h, t as g, u as _, v, x as y, y as b } from "./output-BAuWMUzX.js";
2
- import { Command as x, Option as S } from "commander";
3
- import { field as C, resourceIdToString as w } from "@milaboratories/pl-client";
4
- import { randomUUID as T } from "node:crypto";
5
- import { ProjectMetaKey as E } from "@milaboratories/pl-middle-layer";
6
- import { createInterface as D } from "node:readline";
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 O(e) {
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
- }) : y({
14
+ }) : ne({
14
15
  address: e.address,
15
16
  user: e.user,
16
17
  password: e.password
17
18
  });
18
19
  }
19
- async function k(e) {
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 O(e), s;
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 A(e, ...t) {
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 j = (e = "text", t = ["text", "json"]) => [new S("-a, --address <url>", "Platforma server address").env("PL_ADDRESS").makeOptionMandatory(), new S("-f, --format <format>", "Output format").choices(t).default(e)], M = () => [new S("-u, --user <user>", "Username for authentication").env("PL_USER"), new S("-p, --password <password>", "Password for authentication").env("PL_PASSWORD")], N = () => [new S("--admin-user <user>", "Admin/controller username").env("PL_ADMIN_USER").makeOptionMandatory(), new S("--admin-password <password>", "Admin/controller password").env("PL_ADMIN_PASSWORD").makeOptionMandatory()], P = () => [
36
- new S("--admin-user <user>", "Admin/controller username (enables admin mode)").env("PL_ADMIN_USER"),
37
- new S("--admin-password <password>", "Admin/controller password").env("PL_ADMIN_PASSWORD"),
38
- new S("--target-user <user>", "Operate on this user's data (requires admin credentials)").env("PL_TARGET_USER")
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 F() {
43
- let e = new x("list").description("List all projects for the authenticated user.");
44
- return A(e, j(), M(), P()), e.action(async (e) => {
45
- let { pl: n, projectListRid: r } = await k(e);
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: w(e.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
- w(e.rid),
69
+ E(e.rid),
69
70
  e.label,
70
- g(e.created),
71
- g(e.lastModified)
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 I() {
82
- let e = new x("info").description("Show detailed information about a project.");
83
- return e.argument("<project>", "Project ID or label"), A(e, j(), M(), P()), e.action(async (e, n) => {
84
- let { pl: r, projectListRid: a } = await k(n);
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 b(r, a, e), l = await i(r, o, s);
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: w(l.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: ${w(l.rid)}`), t(`Schema: ${l.schemaVersion ?? "(unknown)"}`), t(`Blocks: ${l.blockCount}`), l.blockIds.length > 0) for (let e of l.blockIds) t(` - ${e}`);
99
- t(`Created: ${g(l.created)}`), t(`Last Modified: ${g(l.lastModified)}`);
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 L() {
109
- let e = new x("duplicate").description("Duplicate a project within the same user. Auto-renames on collision by default.");
110
- return e.argument("<project>", "Project ID or label"), A(e, j(), M(), P()), e.option("-n, --name <name>", "Name for the duplicate"), e.addOption(new S("--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) => {
111
- let { pl: i, projectListRid: a } = await k(n);
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 b(i, a, e), s = T(), l = await i.withWriteTx("duplicateProject", async (e) => {
114
- let t = await e.getKValueString(o, E), i = JSON.parse(t).label, c = await _(e, a), l;
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: w(d),
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: w(l.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 R() {
141
- let e = new x("rename").description("Rename a project.");
142
- return e.argument("<project>", "Project ID or label"), A(e, j(), M(), P()), e.addOption(new S("-n, --name <name>", "New name for the project").makeOptionMandatory()), e.action(async (e, n) => {
143
- let { pl: r, projectListRid: i } = await k(n);
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 b(r, i, e);
146
- await v(r, o, n.name), n.format === "json" ? c({
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: w(o),
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 z() {
159
- let e = new x("delete").description("Delete a project. This permanently destroys all project data.");
160
- return e.argument("<project>", "Project ID or label"), A(e, j(), M(), P()), e.option("--force", "Skip confirmation", !1), e.action(async (e, n) => {
161
- let { pl: r, projectListRid: a } = await k(n);
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 b(r, a, e), u = await i(r, o, s);
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 = D({
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 h(r, a, l), n.format === "json" ? c({
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 B() {
193
- let e = new x("copy-project").description("Copy a project from one user to another. Requires admin/controller credentials.");
194
- return A(e, j(), N()), e.addOption(new S("--source-user <user>", "Username of the source project owner").makeOptionMandatory()), e.addOption(new S("--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 S("--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) => {
195
- let n = await O(e);
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 b(n, a.projectListRid, e.sourceProject), l = i === e.sourceUser ? a : await o(n, i), u = T(), d = await n.withWriteTx("adminCopyProject", async (t) => {
198
- let n = await t.getKValueString(s, E), i = JSON.parse(n).label, a = await _(t, l.projectListRid), o = e.name ?? i;
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: w(d),
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: w(d.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: ${w(d.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 V() {
227
- let e = new x("user-list").description("List all known users' logins from a live server. Requires admin/controller credentials.");
228
- return A(e, j("csv", [
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
- ]), N()), e.action(async (e) => {
233
- let n = await O(e);
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")) : m(["login"], r.map((e) => [e]));
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 H() {
245
- let n = new x("delete-user").description("Delete a user account. Optionally re-homes the user's projects to another user first. Requires admin/controller credentials.");
246
- return n.argument("<user>", "Username of the account to delete"), A(n, j(), N()), n.addOption(new S("--move-projects-to <user>", "Re-home the deleted user's projects to this user before deleting the account")), n.addOption(new S("--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) => {
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 O(r);
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 W(n, o, r)) {
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
- U(n, r, o, d);
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(G(n, r.moveProjectsTo, u, d));
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 U(e, t, n, r) {
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 W(e, t, n) {
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 = D({
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 G(e, t, n, r) {
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 K() {
321
- let e = new x();
443
+ function $() {
444
+ let e = new v();
322
445
  e.name("pl-cli").description("Platforma command-line client");
323
- let t = new x("project").description("Manage projects");
324
- t.addCommand(F()), t.addCommand(I()), t.addCommand(L()), t.addCommand(R()), t.addCommand(z()), e.addCommand(t);
325
- let n = new x("admin").description("Admin operations (requires controller credentials)");
326
- return n.addCommand(B()), n.addCommand(V()), n.addCommand(H()), e.addCommand(n), e;
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 q(e = process.argv) {
329
- await K().parseAsync(e);
453
+ async function pe(e = process.argv) {
454
+ await $().parseAsync(e);
330
455
  }
331
456
  //#endregion
332
- export { K as buildProgram, q as run };
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":"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"}
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export default function maintenanceListResourcesCommand(): Command;
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export default function maintenanceSetResourceErrorCommand(): Command;
@@ -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.9.42",
3
+ "version": "0.10.1",
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-client": "3.18.2",
27
- "@milaboratories/pl-middle-layer": "1.74.6"
26
+ "@milaboratories/pl-middle-layer": "1.74.8",
27
+ "@milaboratories/pl-client": "3.19.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/node": "~24.5.2",
@@ -37,6 +37,11 @@
37
37
  "@milaboratories/build-configs": "2.0.2",
38
38
  "@milaboratories/ts-configs": "1.4.0"
39
39
  },
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/milaboratory/platforma.git",
43
+ "directory": "tools/pl-cli"
44
+ },
40
45
  "scripts": {
41
46
  "build": "vite build",
42
47
  "test": "vitest run --passWithNoTests",
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
- new Option("-a, --address <url>", "Platforma server address")
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
+ }