@platforma-sdk/pl-cli 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
- import { PlClient, ResourceId } from '@milaboratories/pl-client';
2
+ import { PlClient, SignedResourceId } from '@milaboratories/pl-client';
3
3
  /** Base command with dual-mode connection: user auth or admin + target-user. */
4
4
  export declare abstract class PlCommand extends Command {
5
5
  static baseFlags: {
@@ -37,7 +37,7 @@ export declare abstract class PlCommand extends Command {
37
37
  "target-user"?: string;
38
38
  }): Promise<{
39
39
  pl: PlClient;
40
- projectListRid: ResourceId;
40
+ projectListRid: SignedResourceId;
41
41
  }>;
42
42
  protected finally(_: Error | undefined): Promise<void>;
43
43
  }
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
- import { a as e, c as t, d as n, f as r, g as i, h as a, i as o, l as s, m as c, n as l, o as u, p as d, r as f, s as p, t as m, u as h } from "./output-BLm2ZDFN.js";
1
+ import { a as e, c as t, d as n, f as r, g as i, h as a, i as o, l as s, m as c, n as l, o as u, p as d, r as f, s as p, t as m, u as h } from "./output-BGnfm0z-.js";
2
2
  import { Args as g, Command as _, Flags as v } from "@oclif/core";
3
- import { field as y, toGlobalResourceId as b } from "@milaboratories/pl-client";
3
+ import { field as y, resourceIdToString as b } from "@milaboratories/pl-client";
4
4
  import { ProjectMetaKey as x } from "@milaboratories/pl-middle-layer";
5
5
  import { randomUUID as S } from "node:crypto";
6
6
  import { createInterface as C } from "node:readline";
@@ -94,7 +94,7 @@ var w = {
94
94
  let { flags: t } = await this.parse(e), { pl: r, projectListRid: i } = await this.connect(t), a = await n(r, i);
95
95
  if (t.format === "json") f(a.map((e) => ({
96
96
  id: e.id,
97
- rid: e.rid,
97
+ rid: b(e.rid),
98
98
  label: e.label,
99
99
  created: e.created.toISOString(),
100
100
  lastModified: e.lastModified.toISOString()
@@ -106,11 +106,13 @@ var w = {
106
106
  }
107
107
  o(l([
108
108
  "ID",
109
+ "RID",
109
110
  "LABEL",
110
111
  "CREATED",
111
112
  "LAST MODIFIED"
112
113
  ], a.map((e) => [
113
- e.id.substring(0, 8) + "...",
114
+ e.id,
115
+ b(e.rid),
114
116
  e.label,
115
117
  m(e.created),
116
118
  m(e.lastModified)
@@ -126,20 +128,20 @@ var w = {
126
128
  }) };
127
129
  static flags = { ...O.baseFlags };
128
130
  async run() {
129
- let { args: t, flags: n } = await this.parse(e), { pl: r, projectListRid: i } = await this.connect(n), { id: a } = await c(r, i, t.project), l = await s(r, i, a);
131
+ let { args: t, flags: n } = await this.parse(e), { pl: r, projectListRid: i } = await this.connect(n), { id: a, rid: l } = await c(r, i, t.project), u = await s(r, a, l);
130
132
  if (n.format === "json") f({
131
- id: l.id,
132
- rid: l.rid,
133
- label: l.label,
134
- schemaVersion: l.schemaVersion,
135
- blockCount: l.blockCount,
136
- blockIds: l.blockIds,
137
- created: l.created.toISOString(),
138
- lastModified: l.lastModified.toISOString()
133
+ id: u.id,
134
+ rid: b(u.rid),
135
+ label: u.label,
136
+ schemaVersion: u.schemaVersion,
137
+ blockCount: u.blockCount,
138
+ blockIds: u.blockIds,
139
+ created: u.created.toISOString(),
140
+ lastModified: u.lastModified.toISOString()
139
141
  });
140
142
  else {
141
- if (o(`Project: ${l.label}`), o(`ID: ${l.id}`), o(`RID: ${l.rid}`), o(`Schema: ${l.schemaVersion ?? "(unknown)"}`), o(`Blocks: ${l.blockCount}`), l.blockIds.length > 0) for (let e of l.blockIds) o(` - ${e}`);
142
- o(`Created: ${m(l.created)}`), o(`Last Modified: ${m(l.lastModified)}`);
143
+ if (o(`Project: ${u.label}`), o(`ID: ${u.id}`), o(`RID: ${b(u.rid)}`), o(`Schema: ${u.schemaVersion ?? "(unknown)"}`), o(`Blocks: ${u.blockCount}`), u.blockIds.length > 0) for (let e of u.blockIds) o(` - ${e}`);
144
+ o(`Created: ${m(u.created)}`), o(`Last Modified: ${m(u.lastModified)}`);
143
145
  }
144
146
  }
145
147
  },
@@ -170,16 +172,19 @@ var w = {
170
172
  c = o.includes(i.name) ? e(i.name, o) : i.name;
171
173
  } else c = e(a, o);
172
174
  let d = await p(n, l, { label: c });
173
- return n.createField(y(s, u), "Dynamic", d), await n.commit(), {
174
- rid: await b(d),
175
+ n.createField(y(s, u), "Dynamic", d), await n.commit();
176
+ let f = await d.globalId;
177
+ return {
178
+ id: b(f),
179
+ rid: f,
175
180
  label: c
176
181
  };
177
182
  });
178
183
  i.format === "json" ? f({
179
- id: u,
180
- rid: String(d.rid),
184
+ id: d.id,
185
+ rid: b(d.rid),
181
186
  label: d.label
182
- }) : o(`Duplicated project as "${d.label}" (id: ${u})`);
187
+ }) : o(`Duplicated project as "${d.label}" (id: ${d.id})`);
183
188
  }
184
189
  },
185
190
  "project:rename": class e extends O {
@@ -201,7 +206,7 @@ var w = {
201
206
  let { args: t, flags: n } = await this.parse(e), { pl: r, projectListRid: i } = await this.connect(n), { id: a, rid: s } = await c(r, i, t.project);
202
207
  await d(r, s, n.name), n.format === "json" ? f({
203
208
  id: a,
204
- rid: String(s),
209
+ rid: b(s),
205
210
  label: n.name
206
211
  }) : o(`Renamed project to "${n.name}"`);
207
212
  }
@@ -220,7 +225,7 @@ var w = {
220
225
  })
221
226
  };
222
227
  async run() {
223
- let { args: t, flags: n } = await this.parse(e), { pl: r, projectListRid: i } = await this.connect(n), { id: a } = await c(r, i, t.project), l = await s(r, i, a);
228
+ let { args: t, flags: n } = await this.parse(e), { pl: r, projectListRid: i } = await this.connect(n), { id: a, rid: l, fieldName: d } = await c(r, i, t.project), p = await s(r, a, l);
224
229
  if (!n.force) {
225
230
  let e = C({
226
231
  input: process.stdin,
@@ -228,7 +233,7 @@ var w = {
228
233
  });
229
234
  try {
230
235
  if ((await new Promise((t) => {
231
- e.question(`Delete project "${l.label}" (${l.blockCount} blocks)? [y/N] `, t);
236
+ e.question(`Delete project "${p.label}" (${p.blockCount} blocks)? [y/N] `, t);
232
237
  })).toLowerCase() !== "y") {
233
238
  o("Aborted.");
234
239
  return;
@@ -237,11 +242,11 @@ var w = {
237
242
  e.close();
238
243
  }
239
244
  }
240
- await u(r, i, a), n.format === "json" ? f({
245
+ await u(r, i, d), n.format === "json" ? f({
241
246
  deleted: !0,
242
247
  id: a,
243
- label: l.label
244
- }) : o(`Deleted project "${l.label}"`);
248
+ label: p.label
249
+ }) : o(`Deleted project "${p.label}"`);
245
250
  }
246
251
  },
247
252
  "admin:copy-project": class n extends O {
@@ -277,18 +282,21 @@ var w = {
277
282
  s = e(i.name ?? a, o);
278
283
  }
279
284
  let c = await p(n, u, { label: s });
280
- return n.createField(y(d.projectListRid, m), "Dynamic", c), await n.commit(), {
281
- rid: await b(c),
285
+ n.createField(y(d.projectListRid, m), "Dynamic", c), await n.commit();
286
+ let l = await c.globalId;
287
+ return {
288
+ id: b(l),
289
+ rid: l,
282
290
  label: s
283
291
  };
284
292
  });
285
293
  i.format === "json" ? f({
286
- id: m,
287
- rid: String(h.rid),
294
+ id: h.id,
295
+ rid: b(h.rid),
288
296
  label: h.label,
289
297
  sourceUser: i["source-user"],
290
298
  targetUser: s
291
- }) : o(`Copied project from ${i["source-user"]} to ${s} as "${h.label}" (id: ${m})`);
299
+ }) : o(`Copied project from ${i["source-user"]} to ${s} as "${h.label}" (id: ${h.id}, rid: ${b(h.rid)})`);
292
300
  }
293
301
  }
294
302
  };
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../src/cmd-opts.ts","../src/base_command.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/index.ts"],"sourcesContent":["import { Flags } from \"@oclif/core\";\n\nexport const GlobalFlags = {\n address: Flags.string({\n char: \"a\",\n summary: \"Platforma server address\",\n helpValue: \"<url>\",\n env: \"PL_ADDRESS\",\n required: true,\n }),\n format: Flags.string({\n char: \"f\",\n summary: \"Output format\",\n options: [\"text\", \"json\"],\n default: \"text\",\n }),\n};\n\nexport const UserAuthFlags = {\n user: Flags.string({\n char: \"u\",\n summary: \"Username for authentication\",\n env: \"PL_USER\",\n }),\n password: Flags.string({\n char: \"p\",\n summary: \"Password for authentication\",\n env: \"PL_PASSWORD\",\n }),\n};\n\n/** Admin credentials only (for purely admin commands like copy-project). */\nexport const AdminAuthFlags = {\n \"admin-user\": Flags.string({\n summary: \"Admin/controller username\",\n env: \"PL_ADMIN_USER\",\n required: true,\n }),\n \"admin-password\": Flags.string({\n summary: \"Admin/controller password\",\n env: \"PL_ADMIN_PASSWORD\",\n required: true,\n }),\n};\n\n/** Admin credentials + target user (for regular commands that can optionally operate on another user). */\nexport const AdminTargetFlags = {\n \"admin-user\": Flags.string({\n summary: \"Admin/controller username (enables admin mode)\",\n env: \"PL_ADMIN_USER\",\n }),\n \"admin-password\": Flags.string({\n summary: \"Admin/controller password\",\n env: \"PL_ADMIN_PASSWORD\",\n }),\n \"target-user\": Flags.string({\n summary: \"Operate on this user's data (requires admin credentials)\",\n env: \"PL_TARGET_USER\",\n }),\n};\n","import { Command } from \"@oclif/core\";\nimport type { PlClient, ResourceId } from \"@milaboratories/pl-client\";\nimport { createPlConnection, createAdminPlConnection } from \"./connection\";\nimport { getProjectListRid, navigateToUserRoot } from \"./project_ops\";\nimport { GlobalFlags, UserAuthFlags, AdminTargetFlags } from \"./cmd-opts\";\n\n/** Base command with dual-mode connection: user auth or admin + target-user. */\nexport abstract class PlCommand extends Command {\n static baseFlags = {\n ...GlobalFlags,\n ...UserAuthFlags,\n ...AdminTargetFlags,\n };\n\n private _pl?: PlClient;\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 protected async connectClient(flags: {\n address: string;\n user?: string;\n password?: string;\n \"admin-user\"?: string;\n \"admin-password\"?: string;\n }): Promise<PlClient> {\n if (this._pl) throw new Error(\"connectClient() called twice\");\n\n if (flags[\"admin-user\"] && flags[\"admin-password\"]) {\n this._pl = await createAdminPlConnection({\n address: flags.address,\n adminUser: flags[\"admin-user\"],\n adminPassword: flags[\"admin-password\"],\n });\n } else {\n this._pl = await createPlConnection({\n address: flags.address,\n user: flags.user,\n password: flags.password,\n });\n }\n\n return this._pl;\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 protected async connect(flags: {\n address: string;\n user?: string;\n password?: string;\n \"admin-user\"?: string;\n \"admin-password\"?: string;\n \"target-user\"?: string;\n }): Promise<{ pl: PlClient; projectListRid: ResourceId }> {\n const hasAdminUser = !!flags[\"admin-user\"];\n const hasAdminPassword = !!flags[\"admin-password\"];\n const hasTarget = !!flags[\"target-user\"];\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 this.connectClient(flags);\n\n let projectListRid: ResourceId;\n if (hasTarget) {\n const nav = await navigateToUserRoot(pl, flags[\"target-user\"]!);\n projectListRid = nav.projectListRid;\n } else {\n projectListRid = await getProjectListRid(pl);\n }\n\n return { pl, projectListRid };\n }\n\n protected async finally(_: Error | undefined): Promise<void> {\n if (this._pl) {\n await this._pl.close();\n }\n }\n}\n","import { PlCommand } from \"../../base_command\";\nimport { listProjects } from \"../../project_ops\";\nimport { formatTable, formatDate, outputJson, outputText } from \"../../output\";\n\nexport default class ProjectList extends PlCommand {\n static override description = \"List all projects for the authenticated user.\";\n\n static override flags = {\n ...PlCommand.baseFlags,\n };\n\n public async run(): Promise<void> {\n const { flags } = await this.parse(ProjectList);\n const { pl, projectListRid } = await this.connect(flags);\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: 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\", \"LABEL\", \"CREATED\", \"LAST MODIFIED\"],\n projects.map((p) => [\n p.id.substring(0, 8) + \"...\",\n p.label,\n formatDate(p.created),\n formatDate(p.lastModified),\n ]),\n ),\n );\n outputText(`\\n${projects.length} project(s)`);\n }\n }\n}\n","import { Args } from \"@oclif/core\";\nimport { PlCommand } from \"../../base_command\";\nimport { resolveProject, getProjectInfo } from \"../../project_ops\";\nimport { formatDate, outputJson, outputText } from \"../../output\";\n\nexport default class ProjectInfo extends PlCommand {\n static override description = \"Show detailed information about a project.\";\n\n static override args = {\n project: Args.string({\n description: \"Project ID or label\",\n required: true,\n }),\n };\n\n static override flags = {\n ...PlCommand.baseFlags,\n };\n\n public async run(): Promise<void> {\n const { args, flags } = await this.parse(ProjectInfo);\n const { pl, projectListRid } = await this.connect(flags);\n const { id } = await resolveProject(pl, projectListRid, args.project);\n const info = await getProjectInfo(pl, projectListRid, id);\n\n if (flags.format === \"json\") {\n outputJson({\n id: info.id,\n rid: 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: ${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 }\n}\n","import { Args, Flags } from \"@oclif/core\";\nimport { field, toGlobalResourceId } from \"@milaboratories/pl-client\";\nimport { ProjectMetaKey } from \"@milaboratories/pl-middle-layer\";\nimport { randomUUID } from \"node:crypto\";\nimport { PlCommand } from \"../../base_command\";\nimport {\n resolveProject,\n deduplicateName,\n duplicateProject,\n getExistingLabelsInTx,\n} from \"../../project_ops\";\nimport { outputJson, outputText } from \"../../output\";\n\nexport default class ProjectDuplicate extends PlCommand {\n static override description =\n \"Duplicate a project within the same user. Auto-renames on collision by default.\";\n\n static override args = {\n project: Args.string({\n description: \"Project ID or label\",\n required: true,\n }),\n };\n\n static override flags = {\n ...PlCommand.baseFlags,\n name: Flags.string({\n char: \"n\",\n summary: \"Name for the duplicate\",\n helpValue: \"<name>\",\n }),\n \"auto-rename\": Flags.boolean({\n summary: \"Auto-rename on collision (default: true)\",\n default: true,\n allowNo: true,\n }),\n };\n\n public async run(): Promise<void> {\n const { args, flags } = await this.parse(ProjectDuplicate);\n const { pl, projectListRid } = await this.connect(flags);\n\n const { rid: sourceRid } = await resolveProject(pl, projectListRid, args.project);\n\n const newId = randomUUID();\n\n const newRid = 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[\"auto-rename\"] && 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 return { rid: await toGlobalResourceId(newPrj), label: newLabel };\n });\n\n if (flags.format === \"json\") {\n outputJson({ id: newId, rid: String(newRid.rid), label: newRid.label });\n } else {\n outputText(`Duplicated project as \"${newRid.label}\" (id: ${newId})`);\n }\n }\n}\n","import { Args, Flags } from \"@oclif/core\";\nimport { PlCommand } from \"../../base_command\";\nimport { resolveProject, renameProject } from \"../../project_ops\";\nimport { outputJson, outputText } from \"../../output\";\n\nexport default class ProjectRename extends PlCommand {\n static override description = \"Rename a project.\";\n\n static override args = {\n project: Args.string({\n description: \"Project ID or label\",\n required: true,\n }),\n };\n\n static override flags = {\n ...PlCommand.baseFlags,\n name: Flags.string({\n char: \"n\",\n summary: \"New name for the project\",\n helpValue: \"<name>\",\n required: true,\n }),\n };\n\n public async run(): Promise<void> {\n const { args, flags } = await this.parse(ProjectRename);\n const { pl, projectListRid } = await this.connect(flags);\n const { id, rid } = await resolveProject(pl, projectListRid, args.project);\n\n await renameProject(pl, rid, flags.name);\n\n if (flags.format === \"json\") {\n outputJson({ id, rid: String(rid), label: flags.name });\n } else {\n outputText(`Renamed project to \"${flags.name}\"`);\n }\n }\n}\n","import { Args, Flags } from \"@oclif/core\";\nimport { createInterface } from \"node:readline\";\nimport { PlCommand } from \"../../base_command\";\nimport { resolveProject, deleteProject, getProjectInfo } from \"../../project_ops\";\nimport { outputJson, outputText } from \"../../output\";\n\nexport default class ProjectDelete extends PlCommand {\n static override description = \"Delete a project. This permanently destroys all project data.\";\n\n static override args = {\n project: Args.string({\n description: \"Project ID or label\",\n required: true,\n }),\n };\n\n static override flags = {\n ...PlCommand.baseFlags,\n force: Flags.boolean({\n summary: \"Skip confirmation\",\n default: false,\n }),\n };\n\n public async run(): Promise<void> {\n const { args, flags } = await this.parse(ProjectDelete);\n const { pl, projectListRid } = await this.connect(flags);\n const { id } = await resolveProject(pl, projectListRid, args.project);\n\n const info = await getProjectInfo(pl, projectListRid, id);\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, id);\n\n if (flags.format === \"json\") {\n outputJson({ deleted: true, id, label: info.label });\n } else {\n outputText(`Deleted project \"${info.label}\"`);\n }\n }\n}\n","import { Flags } from \"@oclif/core\";\nimport { field, toGlobalResourceId } from \"@milaboratories/pl-client\";\nimport { ProjectMetaKey } from \"@milaboratories/pl-middle-layer\";\nimport { randomUUID } from \"node:crypto\";\nimport { PlCommand } from \"../../base_command\";\nimport { GlobalFlags, AdminAuthFlags } 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 class AdminCopyProject extends PlCommand {\n static override description =\n \"Copy a project from one user to another. Requires admin/controller credentials.\";\n\n static override flags = {\n ...GlobalFlags,\n ...AdminAuthFlags,\n \"source-user\": Flags.string({\n summary: \"Username of the source project owner\",\n required: true,\n }),\n \"source-project\": Flags.string({\n summary: \"Source project ID or label\",\n required: true,\n }),\n \"target-user\": Flags.string({\n summary: \"Username of the target user (defaults to source-user for same-user copy)\",\n }),\n name: Flags.string({\n char: \"n\",\n summary: \"Name for the copied project\",\n helpValue: \"<name>\",\n }),\n \"auto-rename\": Flags.boolean({\n summary: \"Auto-rename on collision (default: true)\",\n default: true,\n allowNo: true,\n }),\n };\n\n public async run(): Promise<void> {\n const { flags } = await this.parse(AdminCopyProject);\n const pl = await this.connectClient(flags);\n\n const targetUser = flags[\"target-user\"] ?? flags[\"source-user\"];\n\n const source = await navigateToUserRoot(pl, flags[\"source-user\"]);\n const { rid: sourceRid } = await resolveProject(\n pl,\n source.projectListRid,\n flags[\"source-project\"],\n );\n\n const target =\n targetUser === flags[\"source-user\"] ? 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[\"auto-rename\"]) {\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 return { rid: await toGlobalResourceId(newPrj), label: newLabel };\n });\n\n if (flags.format === \"json\") {\n outputJson({\n id: newId,\n rid: String(result.rid),\n label: result.label,\n sourceUser: flags[\"source-user\"],\n targetUser,\n });\n } else {\n outputText(\n `Copied project from ${flags[\"source-user\"]} to ${targetUser} as \"${result.label}\" (id: ${newId})`,\n );\n }\n }\n}\n","// Command index for oclif. Run `pnpm run oclif:index` to regenerate.\n\nimport ProjectList from \"./project/list\";\nimport ProjectInfo from \"./project/info\";\nimport ProjectDuplicate from \"./project/duplicate\";\nimport ProjectRename from \"./project/rename\";\nimport ProjectDelete from \"./project/delete\";\nimport AdminCopyProject from \"./admin/copy-project\";\n\nexport const COMMANDS = {\n \"project:list\": ProjectList,\n \"project:info\": ProjectInfo,\n \"project:duplicate\": ProjectDuplicate,\n \"project:rename\": ProjectRename,\n \"project:delete\": ProjectDelete,\n \"admin:copy-project\": AdminCopyProject,\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/cmd-opts.ts","../src/base_command.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/index.ts"],"sourcesContent":["import { Flags } from \"@oclif/core\";\n\nexport const GlobalFlags = {\n address: Flags.string({\n char: \"a\",\n summary: \"Platforma server address\",\n helpValue: \"<url>\",\n env: \"PL_ADDRESS\",\n required: true,\n }),\n format: Flags.string({\n char: \"f\",\n summary: \"Output format\",\n options: [\"text\", \"json\"],\n default: \"text\",\n }),\n};\n\nexport const UserAuthFlags = {\n user: Flags.string({\n char: \"u\",\n summary: \"Username for authentication\",\n env: \"PL_USER\",\n }),\n password: Flags.string({\n char: \"p\",\n summary: \"Password for authentication\",\n env: \"PL_PASSWORD\",\n }),\n};\n\n/** Admin credentials only (for purely admin commands like copy-project). */\nexport const AdminAuthFlags = {\n \"admin-user\": Flags.string({\n summary: \"Admin/controller username\",\n env: \"PL_ADMIN_USER\",\n required: true,\n }),\n \"admin-password\": Flags.string({\n summary: \"Admin/controller password\",\n env: \"PL_ADMIN_PASSWORD\",\n required: true,\n }),\n};\n\n/** Admin credentials + target user (for regular commands that can optionally operate on another user). */\nexport const AdminTargetFlags = {\n \"admin-user\": Flags.string({\n summary: \"Admin/controller username (enables admin mode)\",\n env: \"PL_ADMIN_USER\",\n }),\n \"admin-password\": Flags.string({\n summary: \"Admin/controller password\",\n env: \"PL_ADMIN_PASSWORD\",\n }),\n \"target-user\": Flags.string({\n summary: \"Operate on this user's data (requires admin credentials)\",\n env: \"PL_TARGET_USER\",\n }),\n};\n","import { Command } from \"@oclif/core\";\nimport type { PlClient, SignedResourceId } from \"@milaboratories/pl-client\";\nimport { createPlConnection, createAdminPlConnection } from \"./connection\";\nimport { getProjectListRid, navigateToUserRoot } from \"./project_ops\";\nimport { GlobalFlags, UserAuthFlags, AdminTargetFlags } from \"./cmd-opts\";\n\n/** Base command with dual-mode connection: user auth or admin + target-user. */\nexport abstract class PlCommand extends Command {\n static baseFlags = {\n ...GlobalFlags,\n ...UserAuthFlags,\n ...AdminTargetFlags,\n };\n\n private _pl?: PlClient;\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 protected async connectClient(flags: {\n address: string;\n user?: string;\n password?: string;\n \"admin-user\"?: string;\n \"admin-password\"?: string;\n }): Promise<PlClient> {\n if (this._pl) throw new Error(\"connectClient() called twice\");\n\n if (flags[\"admin-user\"] && flags[\"admin-password\"]) {\n this._pl = await createAdminPlConnection({\n address: flags.address,\n adminUser: flags[\"admin-user\"],\n adminPassword: flags[\"admin-password\"],\n });\n } else {\n this._pl = await createPlConnection({\n address: flags.address,\n user: flags.user,\n password: flags.password,\n });\n }\n\n return this._pl;\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 protected async connect(flags: {\n address: string;\n user?: string;\n password?: string;\n \"admin-user\"?: string;\n \"admin-password\"?: string;\n \"target-user\"?: string;\n }): Promise<{ pl: PlClient; projectListRid: SignedResourceId }> {\n const hasAdminUser = !!flags[\"admin-user\"];\n const hasAdminPassword = !!flags[\"admin-password\"];\n const hasTarget = !!flags[\"target-user\"];\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 this.connectClient(flags);\n\n let projectListRid: SignedResourceId;\n if (hasTarget) {\n const nav = await navigateToUserRoot(pl, flags[\"target-user\"]!);\n projectListRid = nav.projectListRid;\n } else {\n projectListRid = await getProjectListRid(pl);\n }\n\n return { pl, projectListRid };\n }\n\n protected async finally(_: Error | undefined): Promise<void> {\n if (this._pl) {\n await this._pl.close();\n }\n }\n}\n","import { PlCommand } from \"../../base_command\";\nimport { listProjects } from \"../../project_ops\";\nimport { formatTable, formatDate, outputJson, outputText } from \"../../output\";\nimport { resourceIdToString } from \"@milaboratories/pl-client\";\n\nexport default class ProjectList extends PlCommand {\n static override description = \"List all projects for the authenticated user.\";\n\n static override flags = {\n ...PlCommand.baseFlags,\n };\n\n public async run(): Promise<void> {\n const { flags } = await this.parse(ProjectList);\n const { pl, projectListRid } = await this.connect(flags);\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 }\n}\n","import { Args } from \"@oclif/core\";\nimport { PlCommand } from \"../../base_command\";\nimport { resolveProject, getProjectInfo } from \"../../project_ops\";\nimport { formatDate, outputJson, outputText } from \"../../output\";\nimport { resourceIdToString } from \"@milaboratories/pl-client\";\n\nexport default class ProjectInfo extends PlCommand {\n static override description = \"Show detailed information about a project.\";\n\n static override args = {\n project: Args.string({\n description: \"Project ID or label\",\n required: true,\n }),\n };\n\n static override flags = {\n ...PlCommand.baseFlags,\n };\n\n public async run(): Promise<void> {\n const { args, flags } = await this.parse(ProjectInfo);\n const { pl, projectListRid } = await this.connect(flags);\n const { id, rid } = await resolveProject(pl, projectListRid, args.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 }\n}\n","import { Args, Flags } from \"@oclif/core\";\nimport { field, resourceIdToString } from \"@milaboratories/pl-client\";\nimport { ProjectMetaKey } from \"@milaboratories/pl-middle-layer\";\nimport { randomUUID } from \"node:crypto\";\nimport { PlCommand } from \"../../base_command\";\nimport {\n resolveProject,\n deduplicateName,\n duplicateProject,\n getExistingLabelsInTx,\n} from \"../../project_ops\";\nimport { outputJson, outputText } from \"../../output\";\n\nexport default class ProjectDuplicate extends PlCommand {\n static override description =\n \"Duplicate a project within the same user. Auto-renames on collision by default.\";\n\n static override args = {\n project: Args.string({\n description: \"Project ID or label\",\n required: true,\n }),\n };\n\n static override flags = {\n ...PlCommand.baseFlags,\n name: Flags.string({\n char: \"n\",\n summary: \"Name for the duplicate\",\n helpValue: \"<name>\",\n }),\n \"auto-rename\": Flags.boolean({\n summary: \"Auto-rename on collision (default: true)\",\n default: true,\n allowNo: true,\n }),\n };\n\n public async run(): Promise<void> {\n const { args, flags } = await this.parse(ProjectDuplicate);\n const { pl, projectListRid } = await this.connect(flags);\n\n const { rid: sourceRid } = await resolveProject(pl, projectListRid, args.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[\"auto-rename\"] && 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 }\n}\n","import { Args, Flags } from \"@oclif/core\";\nimport { PlCommand } from \"../../base_command\";\nimport { resolveProject, renameProject } from \"../../project_ops\";\nimport { outputJson, outputText } from \"../../output\";\nimport { resourceIdToString } from \"@milaboratories/pl-client\";\n\nexport default class ProjectRename extends PlCommand {\n static override description = \"Rename a project.\";\n\n static override args = {\n project: Args.string({\n description: \"Project ID or label\",\n required: true,\n }),\n };\n\n static override flags = {\n ...PlCommand.baseFlags,\n name: Flags.string({\n char: \"n\",\n summary: \"New name for the project\",\n helpValue: \"<name>\",\n required: true,\n }),\n };\n\n public async run(): Promise<void> {\n const { args, flags } = await this.parse(ProjectRename);\n const { pl, projectListRid } = await this.connect(flags);\n const { id, rid } = await resolveProject(pl, projectListRid, args.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 }\n}\n","import { Args, Flags } from \"@oclif/core\";\nimport { createInterface } from \"node:readline\";\nimport { PlCommand } from \"../../base_command\";\nimport { resolveProject, deleteProject, getProjectInfo } from \"../../project_ops\";\nimport { outputJson, outputText } from \"../../output\";\n\nexport default class ProjectDelete extends PlCommand {\n static override description = \"Delete a project. This permanently destroys all project data.\";\n\n static override args = {\n project: Args.string({\n description: \"Project ID or label\",\n required: true,\n }),\n };\n\n static override flags = {\n ...PlCommand.baseFlags,\n force: Flags.boolean({\n summary: \"Skip confirmation\",\n default: false,\n }),\n };\n\n public async run(): Promise<void> {\n const { args, flags } = await this.parse(ProjectDelete);\n const { pl, projectListRid } = await this.connect(flags);\n const { id, rid, fieldName } = await resolveProject(pl, projectListRid, args.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 }\n}\n","import { Flags } from \"@oclif/core\";\nimport { field, resourceIdToString } from \"@milaboratories/pl-client\";\nimport { ProjectMetaKey } from \"@milaboratories/pl-middle-layer\";\nimport { randomUUID } from \"node:crypto\";\nimport { PlCommand } from \"../../base_command\";\nimport { GlobalFlags, AdminAuthFlags } 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 class AdminCopyProject extends PlCommand {\n static override description =\n \"Copy a project from one user to another. Requires admin/controller credentials.\";\n\n static override flags = {\n ...GlobalFlags,\n ...AdminAuthFlags,\n \"source-user\": Flags.string({\n summary: \"Username of the source project owner\",\n required: true,\n }),\n \"source-project\": Flags.string({\n summary: \"Source project ID or label\",\n required: true,\n }),\n \"target-user\": Flags.string({\n summary: \"Username of the target user (defaults to source-user for same-user copy)\",\n }),\n name: Flags.string({\n char: \"n\",\n summary: \"Name for the copied project\",\n helpValue: \"<name>\",\n }),\n \"auto-rename\": Flags.boolean({\n summary: \"Auto-rename on collision (default: true)\",\n default: true,\n allowNo: true,\n }),\n };\n\n public async run(): Promise<void> {\n const { flags } = await this.parse(AdminCopyProject);\n const pl = await this.connectClient(flags);\n\n const targetUser = flags[\"target-user\"] ?? flags[\"source-user\"];\n\n const source = await navigateToUserRoot(pl, flags[\"source-user\"]);\n const { rid: sourceRid } = await resolveProject(\n pl,\n source.projectListRid,\n flags[\"source-project\"],\n );\n\n const target =\n targetUser === flags[\"source-user\"] ? 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[\"auto-rename\"]) {\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[\"source-user\"],\n targetUser,\n });\n } else {\n outputText(\n `Copied project from ${flags[\"source-user\"]} to ${targetUser} as \"${result.label}\" (id: ${result.id}, rid: ${resourceIdToString(result.rid)})`,\n );\n }\n }\n}\n","// Command index for oclif. Run `pnpm run oclif:index` to regenerate.\n\nimport ProjectList from \"./project/list\";\nimport ProjectInfo from \"./project/info\";\nimport ProjectDuplicate from \"./project/duplicate\";\nimport ProjectRename from \"./project/rename\";\nimport ProjectDelete from \"./project/delete\";\nimport AdminCopyProject from \"./admin/copy-project\";\n\nexport const COMMANDS = {\n \"project:list\": ProjectList,\n \"project:info\": ProjectInfo,\n \"project:duplicate\": ProjectDuplicate,\n \"project:rename\": ProjectRename,\n \"project:delete\": ProjectDelete,\n \"admin:copy-project\": AdminCopyProject,\n};\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as e, c as t, d as n, f as r, g as i, h as a, i as o, l as s, m as c, n as l, o as u, p as d, r as f, s as p, t as m, u as h } from "./output-BLm2ZDFN.js";
1
+ import { a as e, c as t, d as n, f as r, g as i, h as a, i as o, l as s, m as c, n as l, o as u, p as d, r as f, s as p, t as m, u as h } from "./output-BGnfm0z-.js";
2
2
  export { a as createAdminPlConnection, i as createPlConnection, e as deduplicateName, u as deleteProject, p as duplicateProject, m as formatDate, l as formatTable, t as getExistingLabelsInTx, s as getProjectInfo, h as getProjectListRid, n as listProjects, r as navigateToUserRoot, f as outputJson, o as outputText, d as renameProject, c as resolveProject };
@@ -1,84 +1,87 @@
1
- import { PlClient as e, UnauthenticatedPlClient as t, field as n, isNullResourceId as r, isUnimplementedError as i, plAddressToConfig as a } from "@milaboratories/pl-client";
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
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
- import { createHash as p } from "node:crypto";
4
3
  //#region src/connection.ts
5
- async function m(n) {
6
- let r = a(n.address);
4
+ async function p(n) {
5
+ let r = i(n.address);
7
6
  n.user !== void 0 && (r.user = n.user), n.password !== void 0 && (r.password = n.password);
8
- let i = await t.build(r), o;
9
- if (await i.requireAuth()) {
7
+ let a = await t.build(r), o;
8
+ if (await a.requireAuth()) {
10
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.");
11
- o = await i.login(r.user, r.password);
10
+ o = await a.login(r.user, r.password);
12
11
  } else o = {};
13
12
  return await e.init(r, { authInformation: o });
14
13
  }
15
- async function h(n) {
16
- let r = a(n.address), i = await (await t.build(r)).login(n.adminUser, n.adminPassword);
17
- return await e.init(r, { authInformation: i });
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 });
18
17
  }
19
18
  //#endregion
20
19
  //#region src/project_ops.ts
21
- async function g(e, t) {
20
+ async function h(e, t) {
22
21
  return await e.withReadTx("listProjects", async (e) => {
23
22
  let n = await e.getResourceData(t, !0), i = [];
24
23
  for (let t of n.fields) {
25
24
  if (r(t.value)) continue;
26
- let [n, a, l] = await Promise.all([
25
+ let [n, l, u] = await Promise.all([
27
26
  e.getKValueStringIfExists(t.value, c),
28
27
  e.getKValueStringIfExists(t.value, o),
29
28
  e.getKValueStringIfExists(t.value, s)
30
- ]), u = n ? JSON.parse(n) : { label: "(unknown)" };
29
+ ]), d = n ? JSON.parse(n) : { label: "(unknown)" }, f = a(t.value);
31
30
  i.push({
32
- id: t.name,
33
- rid: String(t.value),
34
- label: u.label,
35
- created: a ? new Date(Number(a)) : /* @__PURE__ */ new Date(0),
36
- lastModified: l ? new Date(Number(l)) : /* @__PURE__ */ new Date(0)
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)
37
36
  });
38
37
  }
39
38
  return i.sort((e, t) => t.lastModified.getTime() - e.lastModified.getTime()), i;
40
39
  });
41
40
  }
42
- async function _(e, t, i) {
41
+ async function g(e, t, n) {
43
42
  return await e.withReadTx("getProjectInfo", async (e) => {
44
- let a = await e.getField(n(t, i));
45
- if (r(a.value)) throw Error(`Project "${i}" not found.`);
46
- let u = a.value, f = await e.listKeyValuesString(u), p = f.find((e) => e.key === c), m = f.find((e) => e.key === o), h = f.find((e) => e.key === s), g = f.find((e) => e.key === d), _ = f.find((e) => e.key === l), v = p ? JSON.parse(p.value) : { label: "(unknown)" }, y = g ? JSON.parse(g.value) : void 0, b = [];
47
- if (_) {
48
- let e = JSON.parse(_.value);
49
- for (let t of e.groups ?? []) for (let e of t.blocks ?? []) b.push(e.id);
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);
50
47
  }
51
48
  return {
52
- id: i,
53
- rid: String(u),
54
- label: v.label,
55
- created: m ? new Date(Number(m.value)) : /* @__PURE__ */ new Date(0),
56
- lastModified: h ? new Date(Number(h.value)) : /* @__PURE__ */ new Date(0),
57
- schemaVersion: y,
58
- blockCount: b.length,
59
- blockIds: b
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
60
57
  };
61
58
  });
62
59
  }
63
- async function v(e, t, n) {
60
+ async function _(e, t, n) {
64
61
  return await e.withReadTx("resolveProject", async (e) => {
65
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
+ }
66
72
  for (let t of i.fields) {
67
73
  if (r(t.value)) continue;
68
- if (t.name === n) return {
69
- id: t.name,
70
- rid: t.value
71
- };
72
74
  let i = await e.getKValueStringIfExists(t.value, c);
73
75
  if (i && JSON.parse(i).label === n) return {
74
- id: t.name,
75
- rid: t.value
76
+ id: a(t.value),
77
+ rid: t.value,
78
+ fieldName: t.name
76
79
  };
77
80
  }
78
81
  throw Error(`Project "${n}" not found (searched by id and label).`);
79
82
  });
80
83
  }
81
- async function y(e, t) {
84
+ async function v(e, t) {
82
85
  let n = await e.getResourceData(t, !0), i = [];
83
86
  for (let t of n.fields) {
84
87
  if (r(t.value)) continue;
@@ -90,12 +93,12 @@ async function y(e, t) {
90
93
  }
91
94
  return i;
92
95
  }
93
- function b(e, t) {
96
+ function y(e, t) {
94
97
  let n = `${e} (Copy)`, r = 2;
95
98
  for (; t.includes(n);) n = `${e} (Copy ${r})`, r++;
96
99
  return n;
97
100
  }
98
- async function x(e, t, n) {
101
+ async function b(e, t, n) {
99
102
  await e.withWriteTx("renameProject", async (e) => {
100
103
  let r = await e.getKValueString(t, c), i = {
101
104
  ...JSON.parse(r),
@@ -104,12 +107,12 @@ async function x(e, t, n) {
104
107
  e.setKValue(t, c, JSON.stringify(i)), e.setKValue(t, s, String(Date.now())), await e.commit();
105
108
  });
106
109
  }
107
- async function S(e, t, r) {
110
+ async function x(e, t, r) {
108
111
  await e.withWriteTx("deleteProject", async (e) => {
109
112
  e.removeField(n(t, r)), await e.commit();
110
113
  });
111
114
  }
112
- async function C(e) {
115
+ async function S(e) {
113
116
  return await e.withReadTx("getProjectList", async (e) => {
114
117
  let t = await e.getField({
115
118
  resourceId: e.clientRoot,
@@ -119,21 +122,13 @@ async function C(e) {
119
122
  return t.value;
120
123
  });
121
124
  }
122
- async function w(e, t) {
125
+ async function C(e, t) {
123
126
  let n;
124
- try {
125
- n = await e.getUserRoot({ login: t });
126
- } catch (e) {
127
- if (!i(e)) throw e;
128
- }
129
- if (n === void 0) {
130
- let r = p("sha256").update(t).digest("hex");
131
- n = await e.withReadTx("navigateToUserRoot:legacy", async (e) => {
132
- if (!await e.checkResourceNameExists(r)) throw Error(`User "${t}" not found on this server (no root resource).`);
133
- return await e.getResourceByName(r);
134
- });
135
- }
136
- return await e.withReadTx("navigateToUserRoot", async (e) => {
127
+ if (n = await e.getUserRoot({
128
+ login: t,
129
+ doNotCreate: !0
130
+ }), n === void 0) throw Error(`User "${t}" not found on this server (no root resource).`);
131
+ return await e.withReadTx("navigateToUserProjects", async (e) => {
137
132
  let i = await e.getField({
138
133
  resourceId: n,
139
134
  fieldName: u
@@ -147,13 +142,13 @@ async function w(e, t) {
147
142
  }
148
143
  //#endregion
149
144
  //#region src/output.ts
150
- function T(e) {
145
+ function w(e) {
151
146
  console.log(e);
152
147
  }
153
- function E(e) {
148
+ function T(e) {
154
149
  console.log(JSON.stringify(e, null, 2));
155
150
  }
156
- function D(e, t) {
151
+ function E(e, t) {
157
152
  let n = e.map((e, n) => Math.max(e.length, ...t.map((e) => (e[n] ?? "").length))), r = n.map((e) => "-".repeat(e)).join(" ");
158
153
  return [
159
154
  e.map((e, t) => e.padEnd(n[t])).join(" "),
@@ -161,10 +156,10 @@ function D(e, t) {
161
156
  ...t.map((e) => e.map((e, t) => (e ?? "").padEnd(n[t])).join(" "))
162
157
  ].join("\n");
163
158
  }
164
- function O(e) {
159
+ function D(e) {
165
160
  return e.getTime() === 0 ? "(unknown)" : e.toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
166
161
  }
167
162
  //#endregion
168
- export { b as a, y as c, g as d, w as f, m as g, h, T as i, _ as l, v as m, D as n, S as o, x as p, E as r, f as s, O as t, C as u };
163
+ export { y as a, v as c, h as d, C as f, p as g, m as h, w as i, g as l, _ as m, E as n, x as o, b as p, T as r, f as s, D as t, S as u };
169
164
 
170
- //# sourceMappingURL=output-BLm2ZDFN.js.map
165
+ //# sourceMappingURL=output-BGnfm0z-.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output-BGnfm0z-.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, doNotCreate: true });\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\";\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/** 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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"}
@@ -1,47 +1,52 @@
1
- import { PlClient, ResourceId, PlTransaction } from '@milaboratories/pl-client';
1
+ import { PlClient, SignedResourceId, PlTransaction } from '@milaboratories/pl-client';
2
2
  import { duplicateProject } from '@milaboratories/pl-middle-layer';
3
3
  export interface ProjectEntry {
4
4
  id: string;
5
- rid: string;
5
+ rid: SignedResourceId;
6
6
  label: string;
7
7
  created: Date;
8
8
  lastModified: Date;
9
9
  }
10
+ export interface ProjectIdentity {
11
+ /** Project ID (resourceIdToString of resource ID) */
12
+ id: string;
13
+ /** Signed resource ID for use in transactions */
14
+ rid: SignedResourceId;
15
+ /** UUID field name in the project list resource */
16
+ fieldName: string;
17
+ }
10
18
  export interface ProjectInfo extends ProjectEntry {
11
19
  schemaVersion: string | undefined;
12
20
  blockCount: number;
13
21
  blockIds: string[];
14
22
  }
15
23
  /** List all projects from a project list resource. */
16
- export declare function listProjects(pl: PlClient, projectListRid: ResourceId): Promise<ProjectEntry[]>;
24
+ export declare function listProjects(pl: PlClient, projectListRid: SignedResourceId): Promise<ProjectEntry[]>;
17
25
  /** Get detailed info about a project. */
18
- export declare function getProjectInfo(pl: PlClient, projectListRid: ResourceId, projectId: string): Promise<ProjectInfo>;
19
- /** Resolve a project identifier (id or label) to its field ID and ResourceId. */
20
- export declare function resolveProject(pl: PlClient, projectListRid: ResourceId, identifier: string): Promise<{
21
- id: string;
22
- rid: ResourceId;
23
- }>;
26
+ export declare function getProjectInfo(pl: PlClient, projectId: string, rid: SignedResourceId): Promise<ProjectInfo>;
27
+ /** Resolve a project identifier (id or label) to its project ID, ResourceId and field name. */
28
+ export declare function resolveProject(pl: PlClient, projectListRid: SignedResourceId, identifier: string): Promise<ProjectIdentity>;
24
29
  /** Read all project labels within an existing transaction. */
25
- export declare function getExistingLabelsInTx(tx: PlTransaction, projectListRid: ResourceId): Promise<string[]>;
30
+ export declare function getExistingLabelsInTx(tx: PlTransaction, projectListRid: SignedResourceId): Promise<string[]>;
26
31
  /** Get all project labels from a project list. */
27
- export declare function getProjectLabels(pl: PlClient, projectListRid: ResourceId): Promise<string[]>;
32
+ export declare function getProjectLabels(pl: PlClient, projectListRid: SignedResourceId): Promise<string[]>;
28
33
  /**
29
34
  * Deduplicates a project name against existing labels.
30
35
  * "X" → "X (Copy)" → "X (Copy 2)" → ...
31
36
  */
32
37
  export declare function deduplicateName(baseName: string, existingLabels: string[]): string;
33
38
  /** Rename a project (update its label). */
34
- export declare function renameProject(pl: PlClient, projectRid: ResourceId, newLabel: string): Promise<void>;
39
+ export declare function renameProject(pl: PlClient, projectRid: SignedResourceId, newLabel: string): Promise<void>;
35
40
  /** Delete a project from the project list. */
36
- export declare function deleteProject(pl: PlClient, projectListRid: ResourceId, projectId: string): Promise<void>;
41
+ export declare function deleteProject(pl: PlClient, projectListRid: SignedResourceId, fieldName: string): Promise<void>;
37
42
  /** Get the project list ResourceId for the connected user. */
38
- export declare function getProjectListRid(pl: PlClient): Promise<ResourceId>;
43
+ export declare function getProjectListRid(pl: PlClient): Promise<SignedResourceId>;
39
44
  /**
40
45
  * Navigates to a specific user's project list resource ID.
41
46
  * Tries ListUserResources first (new backend), falls back to SHA256 named resource lookup.
42
47
  */
43
48
  export declare function navigateToUserRoot(pl: PlClient, username: string): Promise<{
44
- userRoot: ResourceId;
45
- projectListRid: ResourceId;
49
+ userRoot: SignedResourceId;
50
+ projectListRid: SignedResourceId;
46
51
  }>;
47
52
  export { duplicateProject };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platforma-sdk/pl-cli",
3
- "version": "0.5.5",
3
+ "version": "0.6.0",
4
4
  "description": "CLI for Platforma server state manipulation",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {
@@ -23,8 +23,8 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@oclif/core": "^4.0.37",
26
- "@milaboratories/pl-client": "3.2.5",
27
- "@milaboratories/pl-middle-layer": "1.58.3"
26
+ "@milaboratories/pl-client": "3.3.0",
27
+ "@milaboratories/pl-middle-layer": "1.59.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/node": "~24.5.2",
@@ -33,10 +33,10 @@
33
33
  "vite": "^8.0.6",
34
34
  "vite-plugin-dts": "^4.5.3",
35
35
  "vitest": "^4.1.3",
36
- "@milaboratories/oclif-index": "1.1.1",
37
36
  "@milaboratories/ts-configs": "1.2.3",
38
37
  "@milaboratories/build-configs": "2.0.0",
39
- "@milaboratories/ts-builder": "1.3.2"
38
+ "@milaboratories/oclif-index": "1.1.1",
39
+ "@milaboratories/ts-builder": "1.4.0"
40
40
  },
41
41
  "oclif": {
42
42
  "bin": "pl-cli",
@@ -1,5 +1,5 @@
1
1
  import { Command } from "@oclif/core";
2
- import type { PlClient, ResourceId } from "@milaboratories/pl-client";
2
+ import type { PlClient, SignedResourceId } from "@milaboratories/pl-client";
3
3
  import { createPlConnection, createAdminPlConnection } from "./connection";
4
4
  import { getProjectListRid, navigateToUserRoot } from "./project_ops";
5
5
  import { GlobalFlags, UserAuthFlags, AdminTargetFlags } from "./cmd-opts";
@@ -56,7 +56,7 @@ export abstract class PlCommand extends Command {
56
56
  "admin-user"?: string;
57
57
  "admin-password"?: string;
58
58
  "target-user"?: string;
59
- }): Promise<{ pl: PlClient; projectListRid: ResourceId }> {
59
+ }): Promise<{ pl: PlClient; projectListRid: SignedResourceId }> {
60
60
  const hasAdminUser = !!flags["admin-user"];
61
61
  const hasAdminPassword = !!flags["admin-password"];
62
62
  const hasTarget = !!flags["target-user"];
@@ -71,7 +71,7 @@ export abstract class PlCommand extends Command {
71
71
 
72
72
  const pl = await this.connectClient(flags);
73
73
 
74
- let projectListRid: ResourceId;
74
+ let projectListRid: SignedResourceId;
75
75
  if (hasTarget) {
76
76
  const nav = await navigateToUserRoot(pl, flags["target-user"]!);
77
77
  projectListRid = nav.projectListRid;
@@ -1,5 +1,5 @@
1
1
  import { Flags } from "@oclif/core";
2
- import { field, toGlobalResourceId } from "@milaboratories/pl-client";
2
+ import { field, resourceIdToString } from "@milaboratories/pl-client";
3
3
  import { ProjectMetaKey } from "@milaboratories/pl-middle-layer";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { PlCommand } from "../../base_command";
@@ -81,20 +81,22 @@ export default class AdminCopyProject extends PlCommand {
81
81
  tx.createField(field(target.projectListRid, newId), "Dynamic", newPrj);
82
82
  await tx.commit();
83
83
 
84
- return { rid: await toGlobalResourceId(newPrj), label: newLabel };
84
+ const prjResourceId = await newPrj.globalId;
85
+ const prjId = resourceIdToString(prjResourceId);
86
+ return { id: prjId, rid: prjResourceId, label: newLabel };
85
87
  });
86
88
 
87
89
  if (flags.format === "json") {
88
90
  outputJson({
89
- id: newId,
90
- rid: String(result.rid),
91
+ id: result.id,
92
+ rid: resourceIdToString(result.rid),
91
93
  label: result.label,
92
94
  sourceUser: flags["source-user"],
93
95
  targetUser,
94
96
  });
95
97
  } else {
96
98
  outputText(
97
- `Copied project from ${flags["source-user"]} to ${targetUser} as "${result.label}" (id: ${newId})`,
99
+ `Copied project from ${flags["source-user"]} to ${targetUser} as "${result.label}" (id: ${result.id}, rid: ${resourceIdToString(result.rid)})`,
98
100
  );
99
101
  }
100
102
  }
@@ -25,9 +25,9 @@ export default class ProjectDelete extends PlCommand {
25
25
  public async run(): Promise<void> {
26
26
  const { args, flags } = await this.parse(ProjectDelete);
27
27
  const { pl, projectListRid } = await this.connect(flags);
28
- const { id } = await resolveProject(pl, projectListRid, args.project);
28
+ const { id, rid, fieldName } = await resolveProject(pl, projectListRid, args.project);
29
29
 
30
- const info = await getProjectInfo(pl, projectListRid, id);
30
+ const info = await getProjectInfo(pl, id, rid);
31
31
 
32
32
  if (!flags.force) {
33
33
  const rl = createInterface({ input: process.stdin, output: process.stderr });
@@ -47,7 +47,7 @@ export default class ProjectDelete extends PlCommand {
47
47
  }
48
48
  }
49
49
 
50
- await deleteProject(pl, projectListRid, id);
50
+ await deleteProject(pl, projectListRid, fieldName);
51
51
 
52
52
  if (flags.format === "json") {
53
53
  outputJson({ deleted: true, id, label: info.label });
@@ -1,5 +1,5 @@
1
1
  import { Args, Flags } from "@oclif/core";
2
- import { field, toGlobalResourceId } from "@milaboratories/pl-client";
2
+ import { field, resourceIdToString } from "@milaboratories/pl-client";
3
3
  import { ProjectMetaKey } from "@milaboratories/pl-middle-layer";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { PlCommand } from "../../base_command";
@@ -44,7 +44,7 @@ export default class ProjectDuplicate extends PlCommand {
44
44
 
45
45
  const newId = randomUUID();
46
46
 
47
- const newRid = await pl.withWriteTx("duplicateProject", async (tx) => {
47
+ const result = await pl.withWriteTx("duplicateProject", async (tx) => {
48
48
  const sourceMetaStr = await tx.getKValueString(sourceRid, ProjectMetaKey);
49
49
  const sourceMeta = JSON.parse(sourceMetaStr);
50
50
  const sourceLabel: string = sourceMeta.label;
@@ -68,13 +68,15 @@ export default class ProjectDuplicate extends PlCommand {
68
68
  tx.createField(field(projectListRid, newId), "Dynamic", newPrj);
69
69
  await tx.commit();
70
70
 
71
- return { rid: await toGlobalResourceId(newPrj), label: newLabel };
71
+ const projectResourceId = await newPrj.globalId;
72
+ const projectId = resourceIdToString(projectResourceId);
73
+ return { id: projectId, rid: projectResourceId, label: newLabel };
72
74
  });
73
75
 
74
76
  if (flags.format === "json") {
75
- outputJson({ id: newId, rid: String(newRid.rid), label: newRid.label });
77
+ outputJson({ id: result.id, rid: resourceIdToString(result.rid), label: result.label });
76
78
  } else {
77
- outputText(`Duplicated project as "${newRid.label}" (id: ${newId})`);
79
+ outputText(`Duplicated project as "${result.label}" (id: ${result.id})`);
78
80
  }
79
81
  }
80
82
  }
@@ -2,6 +2,7 @@ import { Args } from "@oclif/core";
2
2
  import { PlCommand } from "../../base_command";
3
3
  import { resolveProject, getProjectInfo } from "../../project_ops";
4
4
  import { formatDate, outputJson, outputText } from "../../output";
5
+ import { resourceIdToString } from "@milaboratories/pl-client";
5
6
 
6
7
  export default class ProjectInfo extends PlCommand {
7
8
  static override description = "Show detailed information about a project.";
@@ -20,13 +21,13 @@ export default class ProjectInfo extends PlCommand {
20
21
  public async run(): Promise<void> {
21
22
  const { args, flags } = await this.parse(ProjectInfo);
22
23
  const { pl, projectListRid } = await this.connect(flags);
23
- const { id } = await resolveProject(pl, projectListRid, args.project);
24
- const info = await getProjectInfo(pl, projectListRid, id);
24
+ const { id, rid } = await resolveProject(pl, projectListRid, args.project);
25
+ const info = await getProjectInfo(pl, id, rid);
25
26
 
26
27
  if (flags.format === "json") {
27
28
  outputJson({
28
29
  id: info.id,
29
- rid: info.rid,
30
+ rid: resourceIdToString(info.rid),
30
31
  label: info.label,
31
32
  schemaVersion: info.schemaVersion,
32
33
  blockCount: info.blockCount,
@@ -37,7 +38,7 @@ export default class ProjectInfo extends PlCommand {
37
38
  } else {
38
39
  outputText(`Project: ${info.label}`);
39
40
  outputText(`ID: ${info.id}`);
40
- outputText(`RID: ${info.rid}`);
41
+ outputText(`RID: ${resourceIdToString(info.rid)}`);
41
42
  outputText(`Schema: ${info.schemaVersion ?? "(unknown)"}`);
42
43
  outputText(`Blocks: ${info.blockCount}`);
43
44
  if (info.blockIds.length > 0) {
@@ -1,6 +1,7 @@
1
1
  import { PlCommand } from "../../base_command";
2
2
  import { listProjects } from "../../project_ops";
3
3
  import { formatTable, formatDate, outputJson, outputText } from "../../output";
4
+ import { resourceIdToString } from "@milaboratories/pl-client";
4
5
 
5
6
  export default class ProjectList extends PlCommand {
6
7
  static override description = "List all projects for the authenticated user.";
@@ -18,7 +19,7 @@ export default class ProjectList extends PlCommand {
18
19
  outputJson(
19
20
  projects.map((p) => ({
20
21
  id: p.id,
21
- rid: p.rid,
22
+ rid: resourceIdToString(p.rid),
22
23
  label: p.label,
23
24
  created: p.created.toISOString(),
24
25
  lastModified: p.lastModified.toISOString(),
@@ -32,9 +33,10 @@ export default class ProjectList extends PlCommand {
32
33
 
33
34
  outputText(
34
35
  formatTable(
35
- ["ID", "LABEL", "CREATED", "LAST MODIFIED"],
36
+ ["ID", "RID", "LABEL", "CREATED", "LAST MODIFIED"],
36
37
  projects.map((p) => [
37
- p.id.substring(0, 8) + "...",
38
+ p.id,
39
+ resourceIdToString(p.rid),
38
40
  p.label,
39
41
  formatDate(p.created),
40
42
  formatDate(p.lastModified),
@@ -2,6 +2,7 @@ import { Args, Flags } from "@oclif/core";
2
2
  import { PlCommand } from "../../base_command";
3
3
  import { resolveProject, renameProject } from "../../project_ops";
4
4
  import { outputJson, outputText } from "../../output";
5
+ import { resourceIdToString } from "@milaboratories/pl-client";
5
6
 
6
7
  export default class ProjectRename extends PlCommand {
7
8
  static override description = "Rename a project.";
@@ -31,7 +32,7 @@ export default class ProjectRename extends PlCommand {
31
32
  await renameProject(pl, rid, flags.name);
32
33
 
33
34
  if (flags.format === "json") {
34
- outputJson({ id, rid: String(rid), label: flags.name });
35
+ outputJson({ id, rid: resourceIdToString(rid), label: flags.name });
35
36
  } else {
36
37
  outputText(`Renamed project to "${flags.name}"`);
37
38
  }
@@ -1,5 +1,5 @@
1
- import type { PlClient, ResourceId, PlTransaction } from "@milaboratories/pl-client";
2
- import { field, isNullResourceId, isUnimplementedError } from "@milaboratories/pl-client";
1
+ import type { PlClient, SignedResourceId, PlTransaction } from "@milaboratories/pl-client";
2
+ import { field, isNullSignedResourceId, resourceIdToString } from "@milaboratories/pl-client";
3
3
  import {
4
4
  ProjectMetaKey,
5
5
  ProjectCreatedTimestamp,
@@ -10,16 +10,24 @@ import {
10
10
  duplicateProject,
11
11
  } from "@milaboratories/pl-middle-layer";
12
12
  import type { ProjectMeta } from "@milaboratories/pl-middle-layer";
13
- import { createHash } from "node:crypto";
14
13
 
15
14
  export interface ProjectEntry {
16
15
  id: string;
17
- rid: string;
16
+ rid: SignedResourceId;
18
17
  label: string;
19
18
  created: Date;
20
19
  lastModified: Date;
21
20
  }
22
21
 
22
+ export interface ProjectIdentity {
23
+ /** Project ID (resourceIdToString of resource ID) */
24
+ id: string;
25
+ /** Signed resource ID for use in transactions */
26
+ rid: SignedResourceId;
27
+ /** UUID field name in the project list resource */
28
+ fieldName: string;
29
+ }
30
+
23
31
  export interface ProjectInfo extends ProjectEntry {
24
32
  schemaVersion: string | undefined;
25
33
  blockCount: number;
@@ -29,14 +37,14 @@ export interface ProjectInfo extends ProjectEntry {
29
37
  /** List all projects from a project list resource. */
30
38
  export async function listProjects(
31
39
  pl: PlClient,
32
- projectListRid: ResourceId,
40
+ projectListRid: SignedResourceId,
33
41
  ): Promise<ProjectEntry[]> {
34
42
  return await pl.withReadTx("listProjects", async (tx) => {
35
43
  const data = await tx.getResourceData(projectListRid, true);
36
44
  const entries: ProjectEntry[] = [];
37
45
 
38
46
  for (const f of data.fields) {
39
- if (isNullResourceId(f.value)) continue;
47
+ if (isNullSignedResourceId(f.value)) continue;
40
48
 
41
49
  const [metaStr, createdStr, modifiedStr] = await Promise.all([
42
50
  tx.getKValueStringIfExists(f.value, ProjectMetaKey),
@@ -46,9 +54,10 @@ export async function listProjects(
46
54
 
47
55
  const meta: ProjectMeta = metaStr ? JSON.parse(metaStr) : { label: "(unknown)" };
48
56
 
57
+ const projectId = resourceIdToString(f.value);
49
58
  entries.push({
50
- id: f.name,
51
- rid: String(f.value),
59
+ id: projectId,
60
+ rid: f.value,
52
61
  label: meta.label,
53
62
  created: createdStr ? new Date(Number(createdStr)) : new Date(0),
54
63
  lastModified: modifiedStr ? new Date(Number(modifiedStr)) : new Date(0),
@@ -63,16 +72,10 @@ export async function listProjects(
63
72
  /** Get detailed info about a project. */
64
73
  export async function getProjectInfo(
65
74
  pl: PlClient,
66
- projectListRid: ResourceId,
67
75
  projectId: string,
76
+ rid: SignedResourceId,
68
77
  ): Promise<ProjectInfo> {
69
78
  return await pl.withReadTx("getProjectInfo", async (tx) => {
70
- const fieldData = await tx.getField(field(projectListRid, projectId));
71
- if (isNullResourceId(fieldData.value)) {
72
- throw new Error(`Project "${projectId}" not found.`);
73
- }
74
-
75
- const rid = fieldData.value;
76
79
  const kvs = await tx.listKeyValuesString(rid);
77
80
 
78
81
  const metaKV = kvs.find((kv: { key: string }) => kv.key === ProjectMetaKey);
@@ -96,7 +99,7 @@ export async function getProjectInfo(
96
99
 
97
100
  return {
98
101
  id: projectId,
99
- rid: String(rid),
102
+ rid: rid,
100
103
  label: meta.label,
101
104
  created: createdKV ? new Date(Number(createdKV.value)) : new Date(0),
102
105
  lastModified: modifiedKV ? new Date(Number(modifiedKV.value)) : new Date(0),
@@ -107,24 +110,29 @@ export async function getProjectInfo(
107
110
  });
108
111
  }
109
112
 
110
- /** Resolve a project identifier (id or label) to its field ID and ResourceId. */
113
+ /** Resolve a project identifier (id or label) to its project ID, ResourceId and field name. */
111
114
  export async function resolveProject(
112
115
  pl: PlClient,
113
- projectListRid: ResourceId,
116
+ projectListRid: SignedResourceId,
114
117
  identifier: string,
115
- ): Promise<{ id: string; rid: ResourceId }> {
118
+ ): Promise<ProjectIdentity> {
116
119
  return await pl.withReadTx("resolveProject", async (tx) => {
117
120
  const data = await tx.getResourceData(projectListRid, true);
118
121
  for (const f of data.fields) {
119
- if (isNullResourceId(f.value)) continue;
120
- if (f.name === identifier) {
121
- return { id: f.name, rid: f.value };
122
+ if (isNullSignedResourceId(f.value)) continue;
123
+ const projectId = resourceIdToString(f.value);
124
+ if (projectId === identifier) {
125
+ return { id: projectId, rid: f.value, fieldName: f.name };
122
126
  }
127
+ }
128
+ // Fallback: match by label
129
+ for (const f of data.fields) {
130
+ if (isNullSignedResourceId(f.value)) continue;
123
131
  const metaStr = await tx.getKValueStringIfExists(f.value, ProjectMetaKey);
124
132
  if (metaStr) {
125
133
  const meta: ProjectMeta = JSON.parse(metaStr);
126
134
  if (meta.label === identifier) {
127
- return { id: f.name, rid: f.value };
135
+ return { id: resourceIdToString(f.value), rid: f.value, fieldName: f.name };
128
136
  }
129
137
  }
130
138
  }
@@ -136,12 +144,12 @@ export async function resolveProject(
136
144
  /** Read all project labels within an existing transaction. */
137
145
  export async function getExistingLabelsInTx(
138
146
  tx: PlTransaction,
139
- projectListRid: ResourceId,
147
+ projectListRid: SignedResourceId,
140
148
  ): Promise<string[]> {
141
149
  const data = await tx.getResourceData(projectListRid, true);
142
150
  const labels: string[] = [];
143
151
  for (const f of data.fields) {
144
- if (isNullResourceId(f.value)) continue;
152
+ if (isNullSignedResourceId(f.value)) continue;
145
153
  const metaStr = await tx.getKValueStringIfExists(f.value, ProjectMetaKey);
146
154
  if (metaStr) {
147
155
  const meta: ProjectMeta = JSON.parse(metaStr);
@@ -154,7 +162,7 @@ export async function getExistingLabelsInTx(
154
162
  /** Get all project labels from a project list. */
155
163
  export async function getProjectLabels(
156
164
  pl: PlClient,
157
- projectListRid: ResourceId,
165
+ projectListRid: SignedResourceId,
158
166
  ): Promise<string[]> {
159
167
  return await pl.withReadTx("getProjectLabels", async (tx) => {
160
168
  return getExistingLabelsInTx(tx, projectListRid);
@@ -178,7 +186,7 @@ export function deduplicateName(baseName: string, existingLabels: string[]): str
178
186
  /** Rename a project (update its label). */
179
187
  export async function renameProject(
180
188
  pl: PlClient,
181
- projectRid: ResourceId,
189
+ projectRid: SignedResourceId,
182
190
  newLabel: string,
183
191
  ): Promise<void> {
184
192
  await pl.withWriteTx("renameProject", async (tx) => {
@@ -194,23 +202,23 @@ export async function renameProject(
194
202
  /** Delete a project from the project list. */
195
203
  export async function deleteProject(
196
204
  pl: PlClient,
197
- projectListRid: ResourceId,
198
- projectId: string,
205
+ projectListRid: SignedResourceId,
206
+ fieldName: string,
199
207
  ): Promise<void> {
200
208
  await pl.withWriteTx("deleteProject", async (tx) => {
201
- tx.removeField(field(projectListRid, projectId));
209
+ tx.removeField(field(projectListRid, fieldName));
202
210
  await tx.commit();
203
211
  });
204
212
  }
205
213
 
206
214
  /** Get the project list ResourceId for the connected user. */
207
- export async function getProjectListRid(pl: PlClient): Promise<ResourceId> {
215
+ export async function getProjectListRid(pl: PlClient): Promise<SignedResourceId> {
208
216
  return await pl.withReadTx("getProjectList", async (tx) => {
209
217
  const fieldData = await tx.getField({
210
218
  resourceId: tx.clientRoot,
211
219
  fieldName: ProjectsField,
212
220
  });
213
- if (isNullResourceId(fieldData.value)) {
221
+ if (isNullSignedResourceId(fieldData.value)) {
214
222
  throw new Error("No project list found for this user.");
215
223
  }
216
224
  return fieldData.value;
@@ -224,40 +232,26 @@ export async function getProjectListRid(pl: PlClient): Promise<ResourceId> {
224
232
  export async function navigateToUserRoot(
225
233
  pl: PlClient,
226
234
  username: string,
227
- ): Promise<{ userRoot: ResourceId; projectListRid: ResourceId }> {
228
- let userRootRid: ResourceId | undefined;
229
-
230
- // Try new ListUserResources API first
231
- try {
232
- userRootRid = await pl.getUserRoot({ login: username });
233
- } catch (err) {
234
- if (!isUnimplementedError(err)) throw err;
235
- // Backend doesn't support ListUserResources — fall through to legacy
236
- }
235
+ ): Promise<{ userRoot: SignedResourceId; projectListRid: SignedResourceId }> {
236
+ let userRootRid: SignedResourceId | undefined;
237
237
 
238
- // Legacy fallback: SHA256 named resource lookup
238
+ userRootRid = await pl.getUserRoot({ login: username, doNotCreate: true });
239
239
  if (userRootRid === undefined) {
240
- const rootName = createHash("sha256").update(username).digest("hex");
241
- userRootRid = await pl.withReadTx("navigateToUserRoot:legacy", async (tx) => {
242
- if (!(await tx.checkResourceNameExists(rootName))) {
243
- throw new Error(`User "${username}" not found on this server (no root resource).`);
244
- }
245
- return await tx.getResourceByName(rootName);
246
- });
240
+ throw new Error(`User "${username}" not found on this server (no root resource).`);
247
241
  }
248
242
 
249
243
  // Read the projects field from the resolved user root
250
- return await pl.withReadTx("navigateToUserRoot", async (tx) => {
244
+ return await pl.withReadTx("navigateToUserProjects", async (tx) => {
251
245
  const projectsFieldData = await tx.getField({
252
- resourceId: userRootRid!,
246
+ resourceId: userRootRid,
253
247
  fieldName: ProjectsField,
254
248
  });
255
249
 
256
- if (isNullResourceId(projectsFieldData.value)) {
250
+ if (isNullSignedResourceId(projectsFieldData.value)) {
257
251
  throw new Error(`User "${username}" has no project list.`);
258
252
  }
259
253
 
260
- return { userRoot: userRootRid!, projectListRid: projectsFieldData.value };
254
+ return { userRoot: userRootRid, projectListRid: projectsFieldData.value };
261
255
  });
262
256
  }
263
257
 
@@ -1 +0,0 @@
1
- {"version":3,"file":"output-BLm2ZDFN.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, ResourceId, PlTransaction } from \"@milaboratories/pl-client\";\nimport { field, isNullResourceId, isUnimplementedError } 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\";\nimport { createHash } from \"node:crypto\";\n\nexport interface ProjectEntry {\n id: string;\n rid: string;\n label: string;\n created: Date;\n lastModified: Date;\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: ResourceId,\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 (isNullResourceId(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 entries.push({\n id: f.name,\n rid: String(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 projectListRid: ResourceId,\n projectId: string,\n): Promise<ProjectInfo> {\n return await pl.withReadTx(\"getProjectInfo\", async (tx) => {\n const fieldData = await tx.getField(field(projectListRid, projectId));\n if (isNullResourceId(fieldData.value)) {\n throw new Error(`Project \"${projectId}\" not found.`);\n }\n\n const rid = fieldData.value;\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: String(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 field ID and ResourceId. */\nexport async function resolveProject(\n pl: PlClient,\n projectListRid: ResourceId,\n identifier: string,\n): Promise<{ id: string; rid: ResourceId }> {\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 (isNullResourceId(f.value)) continue;\n if (f.name === identifier) {\n return { id: f.name, rid: f.value };\n }\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: f.name, rid: f.value };\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: ResourceId,\n): Promise<string[]> {\n const data = await tx.getResourceData(projectListRid, true);\n const labels: string[] = [];\n for (const f of data.fields) {\n if (isNullResourceId(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: ResourceId,\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: ResourceId,\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: ResourceId,\n projectId: string,\n): Promise<void> {\n await pl.withWriteTx(\"deleteProject\", async (tx) => {\n tx.removeField(field(projectListRid, projectId));\n await tx.commit();\n });\n}\n\n/** Get the project list ResourceId for the connected user. */\nexport async function getProjectListRid(pl: PlClient): Promise<ResourceId> {\n return await pl.withReadTx(\"getProjectList\", async (tx) => {\n const fieldData = await tx.getField({\n resourceId: tx.clientRoot,\n fieldName: ProjectsField,\n });\n if (isNullResourceId(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: ResourceId; projectListRid: ResourceId }> {\n let userRootRid: ResourceId | undefined;\n\n // Try new ListUserResources API first\n try {\n userRootRid = await pl.getUserRoot({ login: username });\n } catch (err) {\n if (!isUnimplementedError(err)) throw err;\n // Backend doesn't support ListUserResources — fall through to legacy\n }\n\n // Legacy fallback: SHA256 named resource lookup\n if (userRootRid === undefined) {\n const rootName = createHash(\"sha256\").update(username).digest(\"hex\");\n userRootRid = await pl.withReadTx(\"navigateToUserRoot:legacy\", async (tx) => {\n if (!(await tx.checkResourceNameExists(rootName))) {\n throw new Error(`User \"${username}\" not found on this server (no root resource).`);\n }\n return await tx.getResourceByName(rootName);\n });\n }\n\n // Read the projects field from the resolved user root\n return await pl.withReadTx(\"navigateToUserRoot\", async (tx) => {\n const projectsFieldData = await tx.getField({\n resourceId: userRootRid!,\n fieldName: ProjectsField,\n });\n\n if (isNullResourceId(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\";\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/** 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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"}