@platforma-sdk/pl-cli 0.8.29 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +196 -120
- package/dist/cli.js.map +1 -1
- package/dist/cmd/admin/delete-user.d.ts +2 -0
- package/dist/index.js +2 -2
- package/dist/output-BAuWMUzX.js +232 -0
- package/dist/output-BAuWMUzX.js.map +1 -0
- package/dist/project_ops.d.ts +54 -0
- package/package.json +4 -4
- package/src/cli.ts +2 -0
- package/src/cmd/admin/delete-user.ts +210 -0
- package/src/project_ops.ts +182 -1
- package/dist/output-KEq0q5fS.js +0 -166
- package/dist/output-KEq0q5fS.js.map +0 -1
package/src/project_ops.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { PlClient, SignedResourceId, PlTransaction } from "@milaboratories/pl-client";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
field,
|
|
4
|
+
isNotFoundError,
|
|
5
|
+
isNullSignedResourceId,
|
|
6
|
+
resourceIdToString,
|
|
7
|
+
} from "@milaboratories/pl-client";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
3
9
|
import {
|
|
4
10
|
ProjectMetaKey,
|
|
5
11
|
ProjectCreatedTimestamp,
|
|
@@ -7,6 +13,7 @@ import {
|
|
|
7
13
|
SchemaVersionKey,
|
|
8
14
|
ProjectStructureKey,
|
|
9
15
|
ProjectsField,
|
|
16
|
+
ProjectsResourceType,
|
|
10
17
|
duplicateProject,
|
|
11
18
|
} from "@milaboratories/pl-middle-layer";
|
|
12
19
|
import type { ProjectMeta } from "@milaboratories/pl-middle-layer";
|
|
@@ -69,6 +76,53 @@ export async function listProjects(
|
|
|
69
76
|
});
|
|
70
77
|
}
|
|
71
78
|
|
|
79
|
+
/** A project's identity plus its label — what a whole-root operation needs per project. */
|
|
80
|
+
export interface ProjectIdentityWithLabel extends ProjectIdentity {
|
|
81
|
+
label: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Lists every project in a project list with the field name holding it, which
|
|
86
|
+
* {@link listProjects} omits. Whole-root operations (moving a user's projects out before the
|
|
87
|
+
* account is deleted) need that field name to detach each project from its old owner.
|
|
88
|
+
*/
|
|
89
|
+
export async function listProjectIdentities(
|
|
90
|
+
pl: PlClient,
|
|
91
|
+
projectListRid: SignedResourceId,
|
|
92
|
+
): Promise<ProjectIdentityWithLabel[]> {
|
|
93
|
+
return await pl.withReadTx("listProjectIdentities", async (tx) => {
|
|
94
|
+
return await listProjectIdentitiesInTx(tx, projectListRid);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* {@link listProjectIdentities} against a transaction the caller already holds, so a write that
|
|
100
|
+
* acts on the list can read it in the same transaction rather than trusting an earlier snapshot.
|
|
101
|
+
*/
|
|
102
|
+
export async function listProjectIdentitiesInTx(
|
|
103
|
+
tx: PlTransaction,
|
|
104
|
+
projectListRid: SignedResourceId,
|
|
105
|
+
): Promise<ProjectIdentityWithLabel[]> {
|
|
106
|
+
const data = await tx.getResourceData(projectListRid, true);
|
|
107
|
+
const projects: ProjectIdentityWithLabel[] = [];
|
|
108
|
+
|
|
109
|
+
for (const f of data.fields) {
|
|
110
|
+
if (isNullSignedResourceId(f.value)) continue;
|
|
111
|
+
|
|
112
|
+
const metaStr = await tx.getKValueStringIfExists(f.value, ProjectMetaKey);
|
|
113
|
+
const meta: ProjectMeta = metaStr ? JSON.parse(metaStr) : { label: "(unknown)" };
|
|
114
|
+
|
|
115
|
+
projects.push({
|
|
116
|
+
id: resourceIdToString(f.value),
|
|
117
|
+
rid: f.value,
|
|
118
|
+
fieldName: f.name,
|
|
119
|
+
label: meta.label,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return projects;
|
|
124
|
+
}
|
|
125
|
+
|
|
72
126
|
/** Get detailed info about a project. */
|
|
73
127
|
export async function getProjectInfo(
|
|
74
128
|
pl: PlClient,
|
|
@@ -211,6 +265,71 @@ export async function deleteProject(
|
|
|
211
265
|
});
|
|
212
266
|
}
|
|
213
267
|
|
|
268
|
+
/** One project moved by {@link moveProjects}, as it ended up in the target root. */
|
|
269
|
+
export interface MovedProject {
|
|
270
|
+
id: string;
|
|
271
|
+
/** Label the project had in the source root. */
|
|
272
|
+
sourceLabel: string;
|
|
273
|
+
/** Label it carries in the target root — differs when the name collided and was deduplicated. */
|
|
274
|
+
targetLabel: string;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Re-attaches projects from one user's project list to another's, keeping the same project
|
|
279
|
+
* resources — no copy is made, so nothing is duplicated and no data is rewritten.
|
|
280
|
+
*
|
|
281
|
+
* This works across roots because the backend permits a reference between differently coloured
|
|
282
|
+
* resources when the caller holds write access to both, which admin credentials do. Names that
|
|
283
|
+
* collide in the target are deduplicated the way {@link duplicateProject} callers deduplicate
|
|
284
|
+
* theirs, so a move never silently shadows a project the target user already has.
|
|
285
|
+
*
|
|
286
|
+
* The whole batch is one transaction: either every project lands in the target root, or none
|
|
287
|
+
* moves and both roots are untouched.
|
|
288
|
+
*/
|
|
289
|
+
export async function moveProjects(
|
|
290
|
+
pl: PlClient,
|
|
291
|
+
sourceProjectListRid: SignedResourceId,
|
|
292
|
+
targetProjectListRid: SignedResourceId,
|
|
293
|
+
): Promise<MovedProject[]> {
|
|
294
|
+
return await pl.withWriteTx("moveProjects", async (tx) => {
|
|
295
|
+
// Read here rather than taken from the caller: a list read before an operator answered a
|
|
296
|
+
// confirmation prompt describes the account as it was minutes ago, and a project attached
|
|
297
|
+
// since then would be left on the source root — where the account deletion that follows
|
|
298
|
+
// destroys it. Moving what this transaction sees narrows that to the transaction itself.
|
|
299
|
+
const projects = await listProjectIdentitiesInTx(tx, sourceProjectListRid);
|
|
300
|
+
if (projects.length === 0) return [];
|
|
301
|
+
|
|
302
|
+
const takenLabels = await getExistingLabelsInTx(tx, targetProjectListRid);
|
|
303
|
+
const moved: MovedProject[] = [];
|
|
304
|
+
|
|
305
|
+
for (const project of projects) {
|
|
306
|
+
const metaStr = await tx.getKValueString(project.rid, ProjectMetaKey);
|
|
307
|
+
const meta: ProjectMeta = JSON.parse(metaStr);
|
|
308
|
+
const sourceLabel = meta.label;
|
|
309
|
+
|
|
310
|
+
let targetLabel = sourceLabel;
|
|
311
|
+
if (takenLabels.includes(targetLabel)) {
|
|
312
|
+
targetLabel = deduplicateName(sourceLabel, takenLabels);
|
|
313
|
+
tx.setKValue(project.rid, ProjectMetaKey, JSON.stringify({ ...meta, label: targetLabel }));
|
|
314
|
+
tx.setKValue(project.rid, ProjectLastModifiedTimestamp, String(Date.now()));
|
|
315
|
+
}
|
|
316
|
+
// Reserved even when the label was free, so two source projects with the same name do not
|
|
317
|
+
// both keep it.
|
|
318
|
+
takenLabels.push(targetLabel);
|
|
319
|
+
|
|
320
|
+
// Attached to the target before being detached from the source: the project keeps a
|
|
321
|
+
// reference throughout, so it is never momentarily unreferenced.
|
|
322
|
+
tx.createField(field(targetProjectListRid, randomUUID()), "Dynamic", project.rid);
|
|
323
|
+
tx.removeField(field(sourceProjectListRid, project.fieldName));
|
|
324
|
+
|
|
325
|
+
moved.push({ id: project.id, sourceLabel, targetLabel });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
await tx.commit();
|
|
329
|
+
return moved;
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
214
333
|
/** Get the project list ResourceId for the connected user. */
|
|
215
334
|
export async function getProjectListRid(pl: PlClient): Promise<SignedResourceId> {
|
|
216
335
|
return await pl.withReadTx("getProjectList", async (tx) => {
|
|
@@ -225,6 +344,68 @@ export async function getProjectListRid(pl: PlClient): Promise<SignedResourceId>
|
|
|
225
344
|
});
|
|
226
345
|
}
|
|
227
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Resolves a user's root and, if they have one, their project list.
|
|
349
|
+
*
|
|
350
|
+
* Unlike {@link navigateToUserRoot} a missing project list is not an error here: a user who has
|
|
351
|
+
* never opened the app has a root and no list, and that account is still a legitimate target for
|
|
352
|
+
* whole-account operations. Callers that need a list to write into use
|
|
353
|
+
* {@link ensureUserProjectList}.
|
|
354
|
+
*/
|
|
355
|
+
export async function openUserRoot(
|
|
356
|
+
pl: PlClient,
|
|
357
|
+
username: string,
|
|
358
|
+
): Promise<{ userRoot: SignedResourceId; projectListRid: SignedResourceId | undefined }> {
|
|
359
|
+
let userRoot: SignedResourceId | undefined;
|
|
360
|
+
try {
|
|
361
|
+
userRoot = await pl.getUserRoot({ login: username });
|
|
362
|
+
} catch (e) {
|
|
363
|
+
// The backend reports an unknown login as NOT_FOUND on the root, which reads as a missing
|
|
364
|
+
// resource rather than a missing user. Name the actual problem.
|
|
365
|
+
if (isNotFoundError(e)) {
|
|
366
|
+
throw new Error(`User "${username}" not found on this server.`);
|
|
367
|
+
}
|
|
368
|
+
throw e;
|
|
369
|
+
}
|
|
370
|
+
if (userRoot === undefined) {
|
|
371
|
+
throw new Error(`User "${username}" not found on this server (no root resource).`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return await pl.withReadTx("openUserRoot", async (tx) => {
|
|
375
|
+
const projectsField = await tx.getFieldIfExists(field(userRoot, ProjectsField));
|
|
376
|
+
if (projectsField === undefined || isNullSignedResourceId(projectsField.value)) {
|
|
377
|
+
return { userRoot, projectListRid: undefined };
|
|
378
|
+
}
|
|
379
|
+
return { userRoot, projectListRid: projectsField.value };
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Returns a user's project list, creating an empty one when the root has none — the same lazy
|
|
385
|
+
* creation the middle layer performs for the signed-in user, but against an impersonated root, so
|
|
386
|
+
* projects can be re-homed to a user who has never opened the app.
|
|
387
|
+
*/
|
|
388
|
+
export async function ensureUserProjectList(
|
|
389
|
+
pl: PlClient,
|
|
390
|
+
userRoot: SignedResourceId,
|
|
391
|
+
): Promise<SignedResourceId> {
|
|
392
|
+
return await pl.withWriteTx("ensureUserProjectList", async (tx) => {
|
|
393
|
+
const projectsField = field(userRoot, ProjectsField);
|
|
394
|
+
tx.createField(projectsField, "Dynamic");
|
|
395
|
+
const fieldData = await tx.getField(projectsField);
|
|
396
|
+
if (!isNullSignedResourceId(fieldData.value)) {
|
|
397
|
+
await tx.commit();
|
|
398
|
+
return fieldData.value;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const list = tx.createEphemeral(ProjectsResourceType);
|
|
402
|
+
tx.lock(list);
|
|
403
|
+
tx.setField(projectsField, list);
|
|
404
|
+
await tx.commit();
|
|
405
|
+
return await list.globalId;
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
228
409
|
/**
|
|
229
410
|
* Navigates to a specific user's project list resource ID.
|
|
230
411
|
* Tries ListUserResources first (new backend), falls back to SHA256 named resource lookup.
|
package/dist/output-KEq0q5fS.js
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import { PlClient as e, UnauthenticatedPlClient as t, field as n, isNullSignedResourceId as r, plAddressToConfig as i, resourceIdToString as a } from "@milaboratories/pl-client";
|
|
2
|
-
import { ProjectCreatedTimestamp as o, ProjectLastModifiedTimestamp as s, ProjectMetaKey as c, ProjectStructureKey as l, ProjectsField as u, SchemaVersionKey as d, duplicateProject as f } from "@milaboratories/pl-middle-layer";
|
|
3
|
-
//#region src/connection.ts
|
|
4
|
-
async function p(n) {
|
|
5
|
-
let r = i(n.address);
|
|
6
|
-
n.user !== void 0 && (r.user = n.user), n.password !== void 0 && (r.password = n.password);
|
|
7
|
-
let a = await t.build(r), o;
|
|
8
|
-
if (await a.requireAuth()) {
|
|
9
|
-
if (r.user === void 0 || r.password === void 0) throw Error("Server requires authentication but no credentials provided. Use --user/--password flags or PL_USER/PL_PASSWORD env vars.");
|
|
10
|
-
o = await a.login(r.user, r.password);
|
|
11
|
-
} else o = {};
|
|
12
|
-
return await e.init(r, { authInformation: o });
|
|
13
|
-
}
|
|
14
|
-
async function m(n) {
|
|
15
|
-
let r = i(n.address), a = await (await t.build(r)).login(n.adminUser, n.adminPassword);
|
|
16
|
-
return await e.init(r, { authInformation: a });
|
|
17
|
-
}
|
|
18
|
-
//#endregion
|
|
19
|
-
//#region src/project_ops.ts
|
|
20
|
-
async function h(e, t) {
|
|
21
|
-
return await e.withReadTx("listProjects", async (e) => {
|
|
22
|
-
let n = await e.getResourceData(t, !0), i = [];
|
|
23
|
-
for (let t of n.fields) {
|
|
24
|
-
if (r(t.value)) continue;
|
|
25
|
-
let [n, l, u] = await Promise.all([
|
|
26
|
-
e.getKValueStringIfExists(t.value, c),
|
|
27
|
-
e.getKValueStringIfExists(t.value, o),
|
|
28
|
-
e.getKValueStringIfExists(t.value, s)
|
|
29
|
-
]), d = n ? JSON.parse(n) : { label: "(unknown)" }, f = a(t.value);
|
|
30
|
-
i.push({
|
|
31
|
-
id: f,
|
|
32
|
-
rid: t.value,
|
|
33
|
-
label: d.label,
|
|
34
|
-
created: l ? new Date(Number(l)) : /* @__PURE__ */ new Date(0),
|
|
35
|
-
lastModified: u ? new Date(Number(u)) : /* @__PURE__ */ new Date(0)
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
return i.sort((e, t) => t.lastModified.getTime() - e.lastModified.getTime()), i;
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
async function g(e, t, n) {
|
|
42
|
-
return await e.withReadTx("getProjectInfo", async (e) => {
|
|
43
|
-
let r = await e.listKeyValuesString(n), i = r.find((e) => e.key === c), a = r.find((e) => e.key === o), u = r.find((e) => e.key === s), f = r.find((e) => e.key === d), p = r.find((e) => e.key === l), m = i ? JSON.parse(i.value) : { label: "(unknown)" }, h = f ? JSON.parse(f.value) : void 0, g = [];
|
|
44
|
-
if (p) {
|
|
45
|
-
let e = JSON.parse(p.value);
|
|
46
|
-
for (let t of e.groups ?? []) for (let e of t.blocks ?? []) g.push(e.id);
|
|
47
|
-
}
|
|
48
|
-
return {
|
|
49
|
-
id: t,
|
|
50
|
-
rid: n,
|
|
51
|
-
label: m.label,
|
|
52
|
-
created: a ? new Date(Number(a.value)) : /* @__PURE__ */ new Date(0),
|
|
53
|
-
lastModified: u ? new Date(Number(u.value)) : /* @__PURE__ */ new Date(0),
|
|
54
|
-
schemaVersion: h,
|
|
55
|
-
blockCount: g.length,
|
|
56
|
-
blockIds: g
|
|
57
|
-
};
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
async function _(e, t, n) {
|
|
61
|
-
return await e.withReadTx("resolveProject", async (e) => {
|
|
62
|
-
let i = await e.getResourceData(t, !0);
|
|
63
|
-
for (let e of i.fields) {
|
|
64
|
-
if (r(e.value)) continue;
|
|
65
|
-
let t = a(e.value);
|
|
66
|
-
if (t === n) return {
|
|
67
|
-
id: t,
|
|
68
|
-
rid: e.value,
|
|
69
|
-
fieldName: e.name
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
for (let t of i.fields) {
|
|
73
|
-
if (r(t.value)) continue;
|
|
74
|
-
let i = await e.getKValueStringIfExists(t.value, c);
|
|
75
|
-
if (i && JSON.parse(i).label === n) return {
|
|
76
|
-
id: a(t.value),
|
|
77
|
-
rid: t.value,
|
|
78
|
-
fieldName: t.name
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
throw Error(`Project "${n}" not found (searched by id and label).`);
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
async function v(e, t) {
|
|
85
|
-
let n = await e.getResourceData(t, !0), i = [];
|
|
86
|
-
for (let t of n.fields) {
|
|
87
|
-
if (r(t.value)) continue;
|
|
88
|
-
let n = await e.getKValueStringIfExists(t.value, c);
|
|
89
|
-
if (n) {
|
|
90
|
-
let e = JSON.parse(n);
|
|
91
|
-
i.push(e.label);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return i;
|
|
95
|
-
}
|
|
96
|
-
function y(e, t) {
|
|
97
|
-
let n = `${e} (Copy)`, r = 2;
|
|
98
|
-
for (; t.includes(n);) n = `${e} (Copy ${r})`, r++;
|
|
99
|
-
return n;
|
|
100
|
-
}
|
|
101
|
-
async function b(e, t, n) {
|
|
102
|
-
await e.withWriteTx("renameProject", async (e) => {
|
|
103
|
-
let r = await e.getKValueString(t, c), i = {
|
|
104
|
-
...JSON.parse(r),
|
|
105
|
-
label: n
|
|
106
|
-
};
|
|
107
|
-
e.setKValue(t, c, JSON.stringify(i)), e.setKValue(t, s, String(Date.now())), await e.commit();
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
async function x(e, t, r) {
|
|
111
|
-
await e.withWriteTx("deleteProject", async (e) => {
|
|
112
|
-
e.removeField(n(t, r)), await e.commit();
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
async function S(e) {
|
|
116
|
-
return await e.withReadTx("getProjectList", async (e) => {
|
|
117
|
-
let t = await e.getField({
|
|
118
|
-
resourceId: e.clientRoot,
|
|
119
|
-
fieldName: u
|
|
120
|
-
});
|
|
121
|
-
if (r(t.value)) throw Error("No project list found for this user.");
|
|
122
|
-
return t.value;
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
async function C(e, t) {
|
|
126
|
-
let n;
|
|
127
|
-
if (n = await e.getUserRoot({ login: t }), n === void 0) throw Error(`User "${t}" not found on this server (no root resource).`);
|
|
128
|
-
return await e.withReadTx("navigateToUserProjects", async (e) => {
|
|
129
|
-
let i = await e.getField({
|
|
130
|
-
resourceId: n,
|
|
131
|
-
fieldName: u
|
|
132
|
-
});
|
|
133
|
-
if (r(i.value)) throw Error(`User "${t}" has no project list.`);
|
|
134
|
-
return {
|
|
135
|
-
userRoot: n,
|
|
136
|
-
projectListRid: i.value
|
|
137
|
-
};
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
//#endregion
|
|
141
|
-
//#region src/output.ts
|
|
142
|
-
function w(e) {
|
|
143
|
-
console.log(e);
|
|
144
|
-
}
|
|
145
|
-
function T(e) {
|
|
146
|
-
console.log(JSON.stringify(e, null, 2));
|
|
147
|
-
}
|
|
148
|
-
function E(e, t) {
|
|
149
|
-
let n = (e) => /[",\r\n]/.test(e) ? `"${e.replace(/"/g, "\"\"")}"` : e, r = [e, ...t].map((e) => e.map(n).join(","));
|
|
150
|
-
console.log(r.join("\n"));
|
|
151
|
-
}
|
|
152
|
-
function D(e, t) {
|
|
153
|
-
let n = e.map((e, n) => Math.max(e.length, ...t.map((e) => (e[n] ?? "").length))), r = n.map((e) => "-".repeat(e)).join(" ");
|
|
154
|
-
return [
|
|
155
|
-
e.map((e, t) => e.padEnd(n[t])).join(" "),
|
|
156
|
-
r,
|
|
157
|
-
...t.map((e) => e.map((e, t) => (e ?? "").padEnd(n[t])).join(" "))
|
|
158
|
-
].join("\n");
|
|
159
|
-
}
|
|
160
|
-
function O(e) {
|
|
161
|
-
return e.getTime() === 0 ? "(unknown)" : e.toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
|
|
162
|
-
}
|
|
163
|
-
//#endregion
|
|
164
|
-
export { p as _, w as a, f as c, S as d, h as f, m as g, _ as h, T as i, v as l, b as m, D as n, y as o, C as p, E as r, x as s, O as t, g as u };
|
|
165
|
-
|
|
166
|
-
//# sourceMappingURL=output-KEq0q5fS.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"output-KEq0q5fS.js","names":[],"sources":["../src/connection.ts","../src/project_ops.ts","../src/output.ts"],"sourcesContent":["import {\n PlClient,\n UnauthenticatedPlClient,\n plAddressToConfig,\n type AuthInformation,\n type PlClientConfig,\n} from \"@milaboratories/pl-client\";\n\nexport interface PlConnectionOptions {\n address: string;\n user?: string;\n password?: string;\n}\n\nexport interface AdminConnectionOptions {\n address: string;\n adminUser: string;\n adminPassword: string;\n}\n\n/** Creates an authenticated PlClient from address + credentials. */\nexport async function createPlConnection(opts: PlConnectionOptions): Promise<PlClient> {\n const config: PlClientConfig = plAddressToConfig(opts.address);\n\n if (opts.user !== undefined) config.user = opts.user;\n if (opts.password !== undefined) config.password = opts.password;\n\n const unauth = await UnauthenticatedPlClient.build(config);\n let authInformation: AuthInformation;\n\n if (await unauth.requireAuth()) {\n if (config.user === undefined || config.password === undefined) {\n throw new Error(\n \"Server requires authentication but no credentials provided. \" +\n \"Use --user/--password flags or PL_USER/PL_PASSWORD env vars.\",\n );\n }\n authInformation = await unauth.login(config.user, config.password);\n } else {\n authInformation = {};\n }\n\n return await PlClient.init(config, { authInformation });\n}\n\n/** Creates a PlClient with admin/controller credentials. */\nexport async function createAdminPlConnection(opts: AdminConnectionOptions): Promise<PlClient> {\n const config: PlClientConfig = plAddressToConfig(opts.address);\n\n const unauth = await UnauthenticatedPlClient.build(config);\n const authInformation: AuthInformation = await unauth.login(opts.adminUser, opts.adminPassword);\n\n return await PlClient.init(config, { authInformation });\n}\n","import type { PlClient, SignedResourceId, PlTransaction } from \"@milaboratories/pl-client\";\nimport { field, isNullSignedResourceId, resourceIdToString } from \"@milaboratories/pl-client\";\nimport {\n ProjectMetaKey,\n ProjectCreatedTimestamp,\n ProjectLastModifiedTimestamp,\n SchemaVersionKey,\n ProjectStructureKey,\n ProjectsField,\n duplicateProject,\n} from \"@milaboratories/pl-middle-layer\";\nimport type { ProjectMeta } from \"@milaboratories/pl-middle-layer\";\n\nexport interface ProjectEntry {\n id: string;\n rid: SignedResourceId;\n label: string;\n created: Date;\n lastModified: Date;\n}\n\nexport interface ProjectIdentity {\n /** Project ID (resourceIdToString of resource ID) */\n id: string;\n /** Signed resource ID for use in transactions */\n rid: SignedResourceId;\n /** UUID field name in the project list resource */\n fieldName: string;\n}\n\nexport interface ProjectInfo extends ProjectEntry {\n schemaVersion: string | undefined;\n blockCount: number;\n blockIds: string[];\n}\n\n/** List all projects from a project list resource. */\nexport async function listProjects(\n pl: PlClient,\n projectListRid: SignedResourceId,\n): Promise<ProjectEntry[]> {\n return await pl.withReadTx(\"listProjects\", async (tx) => {\n const data = await tx.getResourceData(projectListRid, true);\n const entries: ProjectEntry[] = [];\n\n for (const f of data.fields) {\n if (isNullSignedResourceId(f.value)) continue;\n\n const [metaStr, createdStr, modifiedStr] = await Promise.all([\n tx.getKValueStringIfExists(f.value, ProjectMetaKey),\n tx.getKValueStringIfExists(f.value, ProjectCreatedTimestamp),\n tx.getKValueStringIfExists(f.value, ProjectLastModifiedTimestamp),\n ]);\n\n const meta: ProjectMeta = metaStr ? JSON.parse(metaStr) : { label: \"(unknown)\" };\n\n const projectId = resourceIdToString(f.value);\n entries.push({\n id: projectId,\n rid: f.value,\n label: meta.label,\n created: createdStr ? new Date(Number(createdStr)) : new Date(0),\n lastModified: modifiedStr ? new Date(Number(modifiedStr)) : new Date(0),\n });\n }\n\n entries.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());\n return entries;\n });\n}\n\n/** Get detailed info about a project. */\nexport async function getProjectInfo(\n pl: PlClient,\n projectId: string,\n rid: SignedResourceId,\n): Promise<ProjectInfo> {\n return await pl.withReadTx(\"getProjectInfo\", async (tx) => {\n const kvs = await tx.listKeyValuesString(rid);\n\n const metaKV = kvs.find((kv: { key: string }) => kv.key === ProjectMetaKey);\n const createdKV = kvs.find((kv: { key: string }) => kv.key === ProjectCreatedTimestamp);\n const modifiedKV = kvs.find((kv: { key: string }) => kv.key === ProjectLastModifiedTimestamp);\n const schemaKV = kvs.find((kv: { key: string }) => kv.key === SchemaVersionKey);\n const structureKV = kvs.find((kv: { key: string }) => kv.key === ProjectStructureKey);\n\n const meta: ProjectMeta = metaKV ? JSON.parse(metaKV.value) : { label: \"(unknown)\" };\n const schemaVersion = schemaKV ? JSON.parse(schemaKV.value) : undefined;\n\n const blockIds: string[] = [];\n if (structureKV) {\n const structure = JSON.parse(structureKV.value);\n for (const group of structure.groups ?? []) {\n for (const block of group.blocks ?? []) {\n blockIds.push(block.id);\n }\n }\n }\n\n return {\n id: projectId,\n rid: rid,\n label: meta.label,\n created: createdKV ? new Date(Number(createdKV.value)) : new Date(0),\n lastModified: modifiedKV ? new Date(Number(modifiedKV.value)) : new Date(0),\n schemaVersion,\n blockCount: blockIds.length,\n blockIds,\n };\n });\n}\n\n/** Resolve a project identifier (id or label) to its project ID, ResourceId and field name. */\nexport async function resolveProject(\n pl: PlClient,\n projectListRid: SignedResourceId,\n identifier: string,\n): Promise<ProjectIdentity> {\n return await pl.withReadTx(\"resolveProject\", async (tx) => {\n const data = await tx.getResourceData(projectListRid, true);\n for (const f of data.fields) {\n if (isNullSignedResourceId(f.value)) continue;\n const projectId = resourceIdToString(f.value);\n if (projectId === identifier) {\n return { id: projectId, rid: f.value, fieldName: f.name };\n }\n }\n // Fallback: match by label\n for (const f of data.fields) {\n if (isNullSignedResourceId(f.value)) continue;\n const metaStr = await tx.getKValueStringIfExists(f.value, ProjectMetaKey);\n if (metaStr) {\n const meta: ProjectMeta = JSON.parse(metaStr);\n if (meta.label === identifier) {\n return { id: resourceIdToString(f.value), rid: f.value, fieldName: f.name };\n }\n }\n }\n\n throw new Error(`Project \"${identifier}\" not found (searched by id and label).`);\n });\n}\n\n/** Read all project labels within an existing transaction. */\nexport async function getExistingLabelsInTx(\n tx: PlTransaction,\n projectListRid: SignedResourceId,\n): Promise<string[]> {\n const data = await tx.getResourceData(projectListRid, true);\n const labels: string[] = [];\n for (const f of data.fields) {\n if (isNullSignedResourceId(f.value)) continue;\n const metaStr = await tx.getKValueStringIfExists(f.value, ProjectMetaKey);\n if (metaStr) {\n const meta: ProjectMeta = JSON.parse(metaStr);\n labels.push(meta.label);\n }\n }\n return labels;\n}\n\n/** Get all project labels from a project list. */\nexport async function getProjectLabels(\n pl: PlClient,\n projectListRid: SignedResourceId,\n): Promise<string[]> {\n return await pl.withReadTx(\"getProjectLabels\", async (tx) => {\n return getExistingLabelsInTx(tx, projectListRid);\n });\n}\n\n/**\n * Deduplicates a project name against existing labels.\n * \"X\" → \"X (Copy)\" → \"X (Copy 2)\" → ...\n */\nexport function deduplicateName(baseName: string, existingLabels: string[]): string {\n let candidate = `${baseName} (Copy)`;\n let i = 2;\n while (existingLabels.includes(candidate)) {\n candidate = `${baseName} (Copy ${i})`;\n i++;\n }\n return candidate;\n}\n\n/** Rename a project (update its label). */\nexport async function renameProject(\n pl: PlClient,\n projectRid: SignedResourceId,\n newLabel: string,\n): Promise<void> {\n await pl.withWriteTx(\"renameProject\", async (tx) => {\n const metaStr = await tx.getKValueString(projectRid, ProjectMetaKey);\n const meta: ProjectMeta = JSON.parse(metaStr);\n const updated: ProjectMeta = { ...meta, label: newLabel };\n tx.setKValue(projectRid, ProjectMetaKey, JSON.stringify(updated));\n tx.setKValue(projectRid, ProjectLastModifiedTimestamp, String(Date.now()));\n await tx.commit();\n });\n}\n\n/** Delete a project from the project list. */\nexport async function deleteProject(\n pl: PlClient,\n projectListRid: SignedResourceId,\n fieldName: string,\n): Promise<void> {\n await pl.withWriteTx(\"deleteProject\", async (tx) => {\n tx.removeField(field(projectListRid, fieldName));\n await tx.commit();\n });\n}\n\n/** Get the project list ResourceId for the connected user. */\nexport async function getProjectListRid(pl: PlClient): Promise<SignedResourceId> {\n return await pl.withReadTx(\"getProjectList\", async (tx) => {\n const fieldData = await tx.getField({\n resourceId: tx.clientRoot,\n fieldName: ProjectsField,\n });\n if (isNullSignedResourceId(fieldData.value)) {\n throw new Error(\"No project list found for this user.\");\n }\n return fieldData.value;\n });\n}\n\n/**\n * Navigates to a specific user's project list resource ID.\n * Tries ListUserResources first (new backend), falls back to SHA256 named resource lookup.\n */\nexport async function navigateToUserRoot(\n pl: PlClient,\n username: string,\n): Promise<{ userRoot: SignedResourceId; projectListRid: SignedResourceId }> {\n let userRootRid: SignedResourceId | undefined;\n\n userRootRid = await pl.getUserRoot({ login: username });\n if (userRootRid === undefined) {\n throw new Error(`User \"${username}\" not found on this server (no root resource).`);\n }\n\n // Read the projects field from the resolved user root\n return await pl.withReadTx(\"navigateToUserProjects\", async (tx) => {\n const projectsFieldData = await tx.getField({\n resourceId: userRootRid,\n fieldName: ProjectsField,\n });\n\n if (isNullSignedResourceId(projectsFieldData.value)) {\n throw new Error(`User \"${username}\" has no project list.`);\n }\n\n return { userRoot: userRootRid, projectListRid: projectsFieldData.value };\n });\n}\n\n// Re-export duplicateProject from pl-middle-layer for use in commands\nexport { duplicateProject };\n","export type OutputFormat = \"text\" | \"json\" | \"csv\";\n\n/** Outputs a line of text to stdout. */\nexport function outputText(message: string): void {\n console.log(message);\n}\n\n/** Outputs data as formatted JSON to stdout. */\nexport function outputJson(data: unknown): void {\n console.log(JSON.stringify(data, null, 2));\n}\n\n/** Outputs a header row + data rows as RFC-4180-ish CSV to stdout. */\nexport function outputCsv(headers: string[], rows: string[][]): void {\n const esc = (v: string) => (/[\",\\r\\n]/.test(v) ? `\"${v.replace(/\"/g, '\"\"')}\"` : v);\n const lines = [headers, ...rows].map((r) => r.map(esc).join(\",\"));\n console.log(lines.join(\"\\n\"));\n}\n\n/** Formats a table as aligned text columns. */\nexport function formatTable(headers: string[], rows: string[][]): string {\n const colWidths = headers.map((h, i) =>\n Math.max(h.length, ...rows.map((r) => (r[i] ?? \"\").length)),\n );\n\n const sep = colWidths.map((w) => \"-\".repeat(w)).join(\" \");\n const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(\" \");\n const dataLines = rows.map((row) =>\n row.map((cell, i) => (cell ?? \"\").padEnd(colWidths[i])).join(\" \"),\n );\n\n return [headerLine, sep, ...dataLines].join(\"\\n\");\n}\n\n/** Formats a Date as a short human-readable string. */\nexport function formatDate(d: Date): string {\n if (d.getTime() === 0) return \"(unknown)\";\n return d\n .toISOString()\n .replace(\"T\", \" \")\n .replace(/\\.\\d+Z$/, \"Z\");\n}\n"],"mappings":"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"}
|