@platforma-sdk/pl-cli 0.2.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.
@@ -0,0 +1,174 @@
1
+ import { PlClient as e, UnauthenticatedPlClient as t, field as n, isNullResourceId as r, plAddressToConfig as i } from "@milaboratories/pl-client";
2
+ import { ProjectCreatedTimestamp as a, ProjectLastModifiedTimestamp as o, ProjectMetaKey as s, ProjectStructureKey as c, ProjectsField as l, SchemaVersionKey as u, duplicateProject as d } from "@milaboratories/pl-middle-layer";
3
+ import { createHash as f } from "node:crypto";
4
+ /** Creates an authenticated PlClient from address + credentials. */
5
+ async function p(n) {
6
+ let r = i(n.address);
7
+ n.user !== void 0 && (r.user = n.user), n.password !== void 0 && (r.password = n.password);
8
+ let a = await t.build(r), o;
9
+ if (await a.requireAuth()) {
10
+ 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 a.login(r.user, r.password);
12
+ } else o = {};
13
+ return await e.init(r, { authInformation: o });
14
+ }
15
+ /** Creates a PlClient with admin/controller credentials. */
16
+ async function m(n) {
17
+ let r = i(n.address), a = await (await t.build(r)).login(n.adminUser, n.adminPassword);
18
+ return await e.init(r, { authInformation: a });
19
+ }
20
+ /** List all projects from a project list resource. */
21
+ async function h(e, t) {
22
+ return await e.withReadTx("listProjects", async (e) => {
23
+ let n = await e.getResourceData(t, !0), i = [];
24
+ for (let t of n.fields) {
25
+ if (r(t.value)) continue;
26
+ let [n, c, l] = await Promise.all([
27
+ e.getKValueStringIfExists(t.value, s),
28
+ e.getKValueStringIfExists(t.value, a),
29
+ e.getKValueStringIfExists(t.value, o)
30
+ ]), u = n ? JSON.parse(n) : { label: "(unknown)" };
31
+ i.push({
32
+ id: t.name,
33
+ rid: String(t.value),
34
+ label: u.label,
35
+ created: c ? new Date(Number(c)) : /* @__PURE__ */ new Date(0),
36
+ lastModified: l ? new Date(Number(l)) : /* @__PURE__ */ new Date(0)
37
+ });
38
+ }
39
+ return i.sort((e, t) => t.lastModified.getTime() - e.lastModified.getTime()), i;
40
+ });
41
+ }
42
+ /** Get detailed info about a project. */
43
+ async function g(e, t, i) {
44
+ return await e.withReadTx("getProjectInfo", async (e) => {
45
+ let l = await e.getField(n(t, i));
46
+ if (r(l.value)) throw Error(`Project "${i}" not found.`);
47
+ let d = l.value, f = await e.listKeyValuesString(d), p = f.find((e) => e.key === s), m = f.find((e) => e.key === a), h = f.find((e) => e.key === o), g = f.find((e) => e.key === u), _ = f.find((e) => e.key === c), v = p ? JSON.parse(p.value) : { label: "(unknown)" }, y = g ? JSON.parse(g.value) : void 0, b = [];
48
+ if (_) {
49
+ let e = JSON.parse(_.value);
50
+ for (let t of e.groups ?? []) for (let e of t.blocks ?? []) b.push(e.id);
51
+ }
52
+ return {
53
+ id: i,
54
+ rid: String(d),
55
+ label: v.label,
56
+ created: m ? new Date(Number(m.value)) : /* @__PURE__ */ new Date(0),
57
+ lastModified: h ? new Date(Number(h.value)) : /* @__PURE__ */ new Date(0),
58
+ schemaVersion: y,
59
+ blockCount: b.length,
60
+ blockIds: b
61
+ };
62
+ });
63
+ }
64
+ /** Resolve a project identifier (id or label) to its field ID and ResourceId. */
65
+ async function _(e, t, n) {
66
+ return await e.withReadTx("resolveProject", async (e) => {
67
+ let i = await e.getResourceData(t, !0);
68
+ for (let t of i.fields) {
69
+ if (r(t.value)) continue;
70
+ if (t.name === n) return {
71
+ id: t.name,
72
+ rid: t.value
73
+ };
74
+ let i = await e.getKValueStringIfExists(t.value, s);
75
+ if (i && JSON.parse(i).label === n) return {
76
+ id: t.name,
77
+ rid: t.value
78
+ };
79
+ }
80
+ throw Error(`Project "${n}" not found (searched by id and label).`);
81
+ });
82
+ }
83
+ /** Read all project labels within an existing transaction. */
84
+ async function v(e, t) {
85
+ let n = await e.getResourceData(t, !0), i = [];
86
+ for (let t of n.fields) {
87
+ if (r(t.value)) continue;
88
+ let n = await e.getKValueStringIfExists(t.value, s);
89
+ if (n) {
90
+ let e = JSON.parse(n);
91
+ i.push(e.label);
92
+ }
93
+ }
94
+ return i;
95
+ }
96
+ /**
97
+ * Deduplicates a project name against existing labels.
98
+ * "X" → "X (Copy)" → "X (Copy 2)" → ...
99
+ */
100
+ function y(e, t) {
101
+ let n = `${e} (Copy)`, r = 2;
102
+ for (; t.includes(n);) n = `${e} (Copy ${r})`, r++;
103
+ return n;
104
+ }
105
+ /** Rename a project (update its label). */
106
+ async function b(e, t, n) {
107
+ await e.withWriteTx("renameProject", async (e) => {
108
+ let r = await e.getKValueString(t, s), i = {
109
+ ...JSON.parse(r),
110
+ label: n
111
+ };
112
+ e.setKValue(t, s, JSON.stringify(i)), e.setKValue(t, o, String(Date.now())), await e.commit();
113
+ });
114
+ }
115
+ /** Delete a project from the project list. */
116
+ async function x(e, t, r) {
117
+ await e.withWriteTx("deleteProject", async (e) => {
118
+ e.removeField(n(t, r)), await e.commit();
119
+ });
120
+ }
121
+ /** Get the project list ResourceId for the connected user. */
122
+ async function S(e) {
123
+ return await e.withReadTx("getProjectList", async (e) => {
124
+ let t = await e.getField({
125
+ resourceId: e.clientRoot,
126
+ fieldName: l
127
+ });
128
+ if (r(t.value)) throw Error("No project list found for this user.");
129
+ return t.value;
130
+ });
131
+ }
132
+ /**
133
+ * Navigates to a specific user's project list resource ID.
134
+ * Computes SHA256(username) to find the user's root, then reads the "projects" field.
135
+ */
136
+ async function C(e, t) {
137
+ let n = f("sha256").update(t).digest("hex");
138
+ return await e.withReadTx("navigateToUserRoot", async (e) => {
139
+ if (!await e.checkResourceNameExists(n)) throw Error(`User "${t}" not found on this server (no root resource).`);
140
+ let i = await e.getResourceByName(n), a = await e.getField({
141
+ resourceId: i,
142
+ fieldName: l
143
+ });
144
+ if (r(a.value)) throw Error(`User "${t}" has no project list.`);
145
+ return {
146
+ userRoot: i,
147
+ projectListRid: a.value
148
+ };
149
+ });
150
+ }
151
+ /** Outputs a line of text to stdout. */
152
+ function w(e) {
153
+ console.log(e);
154
+ }
155
+ /** Outputs data as formatted JSON to stdout. */
156
+ function T(e) {
157
+ console.log(JSON.stringify(e, null, 2));
158
+ }
159
+ /** Formats a table as aligned text columns. */
160
+ function E(e, t) {
161
+ let n = e.map((e, n) => Math.max(e.length, ...t.map((e) => (e[n] ?? "").length))), r = n.map((e) => "-".repeat(e)).join(" ");
162
+ return [
163
+ e.map((e, t) => e.padEnd(n[t])).join(" "),
164
+ r,
165
+ ...t.map((e) => e.map((e, t) => (e ?? "").padEnd(n[t])).join(" "))
166
+ ].join("\n");
167
+ }
168
+ /** Formats a Date as a short human-readable string. */
169
+ function D(e) {
170
+ return e.getTime() === 0 ? "(unknown)" : e.toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
171
+ }
172
+ 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, d as s, D as t, S as u };
173
+
174
+ //# sourceMappingURL=output-INk3CKvr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output-INk3CKvr.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 } 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 * Computes SHA256(username) to find the user's root, then reads the \"projects\" field.\n */\nexport async function navigateToUserRoot(\n pl: PlClient,\n username: string,\n): Promise<{ userRoot: ResourceId; projectListRid: ResourceId }> {\n const rootName = createHash(\"sha256\").update(username).digest(\"hex\");\n\n return await pl.withReadTx(\"navigateToUserRoot\", async (tx) => {\n if (!(await tx.checkResourceNameExists(rootName))) {\n throw new Error(`User \"${username}\" not found on this server (no root resource).`);\n }\n\n const userRootRid = await tx.getResourceByName(rootName);\n\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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"}
@@ -0,0 +1,9 @@
1
+ export type OutputFormat = "text" | "json";
2
+ /** Outputs a line of text to stdout. */
3
+ export declare function outputText(message: string): void;
4
+ /** Outputs data as formatted JSON to stdout. */
5
+ export declare function outputJson(data: unknown): void;
6
+ /** Formats a table as aligned text columns. */
7
+ export declare function formatTable(headers: string[], rows: string[][]): string;
8
+ /** Formats a Date as a short human-readable string. */
9
+ export declare function formatDate(d: Date): string;
@@ -0,0 +1,47 @@
1
+ import { PlClient, ResourceId, PlTransaction } from '@milaboratories/pl-client';
2
+ import { duplicateProject } from '@milaboratories/pl-middle-layer';
3
+ export interface ProjectEntry {
4
+ id: string;
5
+ rid: string;
6
+ label: string;
7
+ created: Date;
8
+ lastModified: Date;
9
+ }
10
+ export interface ProjectInfo extends ProjectEntry {
11
+ schemaVersion: string | undefined;
12
+ blockCount: number;
13
+ blockIds: string[];
14
+ }
15
+ /** List all projects from a project list resource. */
16
+ export declare function listProjects(pl: PlClient, projectListRid: ResourceId): Promise<ProjectEntry[]>;
17
+ /** 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
+ }>;
24
+ /** Read all project labels within an existing transaction. */
25
+ export declare function getExistingLabelsInTx(tx: PlTransaction, projectListRid: ResourceId): Promise<string[]>;
26
+ /** Get all project labels from a project list. */
27
+ export declare function getProjectLabels(pl: PlClient, projectListRid: ResourceId): Promise<string[]>;
28
+ /**
29
+ * Deduplicates a project name against existing labels.
30
+ * "X" → "X (Copy)" → "X (Copy 2)" → ...
31
+ */
32
+ export declare function deduplicateName(baseName: string, existingLabels: string[]): string;
33
+ /** Rename a project (update its label). */
34
+ export declare function renameProject(pl: PlClient, projectRid: ResourceId, newLabel: string): Promise<void>;
35
+ /** Delete a project from the project list. */
36
+ export declare function deleteProject(pl: PlClient, projectListRid: ResourceId, projectId: string): Promise<void>;
37
+ /** Get the project list ResourceId for the connected user. */
38
+ export declare function getProjectListRid(pl: PlClient): Promise<ResourceId>;
39
+ /**
40
+ * Navigates to a specific user's project list resource ID.
41
+ * Computes SHA256(username) to find the user's root, then reads the "projects" field.
42
+ */
43
+ export declare function navigateToUserRoot(pl: PlClient, username: string): Promise<{
44
+ userRoot: ResourceId;
45
+ projectListRid: ResourceId;
46
+ }>;
47
+ export { duplicateProject };
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@platforma-sdk/pl-cli",
3
+ "version": "0.2.0",
4
+ "description": "CLI for Platforma server state manipulation",
5
+ "license": "UNLICENSED",
6
+ "bin": {
7
+ "pl-cli": "./bin/run.js"
8
+ },
9
+ "files": [
10
+ "./bin/*",
11
+ "./dist/**/*",
12
+ "./src/**/*",
13
+ "./README.md"
14
+ ],
15
+ "type": "module",
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/lib.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/lib.d.ts",
21
+ "import": "./dist/index.js"
22
+ }
23
+ },
24
+ "dependencies": {
25
+ "@oclif/core": "^4.0.37",
26
+ "@milaboratories/pl-client": "2.17.12",
27
+ "@milaboratories/pl-middle-layer": "1.49.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "~24.5.2",
31
+ "rollup-plugin-node-externals": "^8.0.0",
32
+ "typescript": "~5.9.3",
33
+ "vite": "8.0.0-beta.15",
34
+ "vite-plugin-dts": "^4.5.3",
35
+ "vitest": "^4.0.18",
36
+ "@milaboratories/oclif-index": "1.1.1",
37
+ "@milaboratories/build-configs": "1.5.2",
38
+ "@milaboratories/ts-configs": "1.2.2",
39
+ "@milaboratories/ts-builder": "1.3.0"
40
+ },
41
+ "oclif": {
42
+ "bin": "pl-cli",
43
+ "commands": {
44
+ "identifier": "COMMANDS",
45
+ "strategy": "explicit",
46
+ "target": "./dist/cli.js"
47
+ },
48
+ "dirname": "pl-cli",
49
+ "topicSeparator": " ",
50
+ "topics": {
51
+ "admin": {
52
+ "description": "Admin operations (requires controller credentials)"
53
+ },
54
+ "project": {
55
+ "description": "Manage projects"
56
+ }
57
+ }
58
+ },
59
+ "scripts": {
60
+ "oclif:index": "oclif-index --commands-root=./src/cmd/ --index-file=./src/cmd/index.ts && node --run fmt",
61
+ "build": "vite build",
62
+ "test": "vitest run --passWithNoTests",
63
+ "do-pack": "rm -f *.tgz && pnpm pack && mv *.tgz package.tgz",
64
+ "check": "ts-builder check --target node",
65
+ "formatter:check": "ts-builder formatter --check",
66
+ "linter:check": "ts-builder linter --check",
67
+ "types:check": "ts-builder type-check --target node",
68
+ "fmt": "ts-builder format"
69
+ }
70
+ }
@@ -0,0 +1,90 @@
1
+ import { Command } from "@oclif/core";
2
+ import type { PlClient, ResourceId } from "@milaboratories/pl-client";
3
+ import { createPlConnection, createAdminPlConnection } from "./connection";
4
+ import { getProjectListRid, navigateToUserRoot } from "./project_ops";
5
+ import { GlobalFlags, UserAuthFlags, AdminTargetFlags } from "./cmd-opts";
6
+
7
+ /** Base command with dual-mode connection: user auth or admin + target-user. */
8
+ export abstract class PlCommand extends Command {
9
+ static baseFlags = {
10
+ ...GlobalFlags,
11
+ ...UserAuthFlags,
12
+ ...AdminTargetFlags,
13
+ };
14
+
15
+ private _pl?: PlClient;
16
+
17
+ /**
18
+ * Low-level: get an authenticated PlClient without resolving a project list.
19
+ * Use this for commands that navigate to multiple users (e.g. admin copy-project).
20
+ */
21
+ protected async connectClient(flags: {
22
+ address: string;
23
+ user?: string;
24
+ password?: string;
25
+ "admin-user"?: string;
26
+ "admin-password"?: string;
27
+ }): Promise<PlClient> {
28
+ if (this._pl) throw new Error("connectClient() called twice");
29
+
30
+ if (flags["admin-user"] && flags["admin-password"]) {
31
+ this._pl = await createAdminPlConnection({
32
+ address: flags.address,
33
+ adminUser: flags["admin-user"],
34
+ adminPassword: flags["admin-password"],
35
+ });
36
+ } else {
37
+ this._pl = await createPlConnection({
38
+ address: flags.address,
39
+ user: flags.user,
40
+ password: flags.password,
41
+ });
42
+ }
43
+
44
+ return this._pl;
45
+ }
46
+
47
+ /**
48
+ * Connect and resolve the project list for a single user.
49
+ * In admin mode (--admin-user + --admin-password + --target-user), operates on the target user's data.
50
+ * In user mode, operates on the authenticated user's own data.
51
+ */
52
+ protected async connect(flags: {
53
+ address: string;
54
+ user?: string;
55
+ password?: string;
56
+ "admin-user"?: string;
57
+ "admin-password"?: string;
58
+ "target-user"?: string;
59
+ }): Promise<{ pl: PlClient; projectListRid: ResourceId }> {
60
+ const hasAdminUser = !!flags["admin-user"];
61
+ const hasAdminPassword = !!flags["admin-password"];
62
+ const hasTarget = !!flags["target-user"];
63
+
64
+ // Validate flag combinations
65
+ if (hasTarget && !(hasAdminUser && hasAdminPassword)) {
66
+ throw new Error("--target-user requires --admin-user and --admin-password");
67
+ }
68
+ if ((hasAdminUser || hasAdminPassword) && !hasTarget) {
69
+ throw new Error("--admin-user/--admin-password require --target-user for project commands");
70
+ }
71
+
72
+ const pl = await this.connectClient(flags);
73
+
74
+ let projectListRid: ResourceId;
75
+ if (hasTarget) {
76
+ const nav = await navigateToUserRoot(pl, flags["target-user"]!);
77
+ projectListRid = nav.projectListRid;
78
+ } else {
79
+ projectListRid = await getProjectListRid(pl);
80
+ }
81
+
82
+ return { pl, projectListRid };
83
+ }
84
+
85
+ protected async finally(_: Error | undefined): Promise<void> {
86
+ if (this._pl) {
87
+ await this._pl.close();
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,101 @@
1
+ import { Flags } from "@oclif/core";
2
+ import { field, toGlobalResourceId } from "@milaboratories/pl-client";
3
+ import { ProjectMetaKey } from "@milaboratories/pl-middle-layer";
4
+ import { randomUUID } from "node:crypto";
5
+ import { PlCommand } from "../../base_command";
6
+ import { GlobalFlags, AdminAuthFlags } from "../../cmd-opts";
7
+ import {
8
+ resolveProject,
9
+ deduplicateName,
10
+ duplicateProject,
11
+ getExistingLabelsInTx,
12
+ navigateToUserRoot,
13
+ } from "../../project_ops";
14
+ import { outputJson, outputText } from "../../output";
15
+
16
+ export default class AdminCopyProject extends PlCommand {
17
+ static override description =
18
+ "Copy a project from one user to another. Requires admin/controller credentials.";
19
+
20
+ static override flags = {
21
+ ...GlobalFlags,
22
+ ...AdminAuthFlags,
23
+ "source-user": Flags.string({
24
+ summary: "Username of the source project owner",
25
+ required: true,
26
+ }),
27
+ "source-project": Flags.string({
28
+ summary: "Source project ID or label",
29
+ required: true,
30
+ }),
31
+ "target-user": Flags.string({
32
+ summary: "Username of the target user (defaults to source-user for same-user copy)",
33
+ }),
34
+ name: Flags.string({
35
+ char: "n",
36
+ summary: "Name for the copied project",
37
+ helpValue: "<name>",
38
+ }),
39
+ "auto-rename": Flags.boolean({
40
+ summary: "Auto-rename on collision (default: true)",
41
+ default: true,
42
+ allowNo: true,
43
+ }),
44
+ };
45
+
46
+ public async run(): Promise<void> {
47
+ const { flags } = await this.parse(AdminCopyProject);
48
+ const pl = await this.connectClient(flags);
49
+
50
+ const targetUser = flags["target-user"] ?? flags["source-user"];
51
+
52
+ const source = await navigateToUserRoot(pl, flags["source-user"]);
53
+ const { rid: sourceRid } = await resolveProject(
54
+ pl,
55
+ source.projectListRid,
56
+ flags["source-project"],
57
+ );
58
+
59
+ const target =
60
+ targetUser === flags["source-user"] ? source : await navigateToUserRoot(pl, targetUser);
61
+
62
+ const newId = randomUUID();
63
+
64
+ const result = await pl.withWriteTx("adminCopyProject", async (tx) => {
65
+ const sourceMetaStr = await tx.getKValueString(sourceRid, ProjectMetaKey);
66
+ const sourceMeta = JSON.parse(sourceMetaStr);
67
+ const sourceLabel: string = sourceMeta.label;
68
+
69
+ const existingLabels = await getExistingLabelsInTx(tx, target.projectListRid);
70
+
71
+ let newLabel: string = flags.name ?? sourceLabel;
72
+
73
+ if (existingLabels.includes(newLabel)) {
74
+ if (!flags["auto-rename"]) {
75
+ throw new Error(`Project name "${newLabel}" already exists for target user.`);
76
+ }
77
+ newLabel = deduplicateName(flags.name ?? sourceLabel, existingLabels);
78
+ }
79
+
80
+ const newPrj = await duplicateProject(tx, sourceRid, { label: newLabel });
81
+ tx.createField(field(target.projectListRid, newId), "Dynamic", newPrj);
82
+ await tx.commit();
83
+
84
+ return { rid: await toGlobalResourceId(newPrj), label: newLabel };
85
+ });
86
+
87
+ if (flags.format === "json") {
88
+ outputJson({
89
+ id: newId,
90
+ rid: String(result.rid),
91
+ label: result.label,
92
+ sourceUser: flags["source-user"],
93
+ targetUser,
94
+ });
95
+ } else {
96
+ outputText(
97
+ `Copied project from ${flags["source-user"]} to ${targetUser} as "${result.label}" (id: ${newId})`,
98
+ );
99
+ }
100
+ }
101
+ }
@@ -0,0 +1,17 @@
1
+ // Command index for oclif. Run `pnpm run oclif:index` to regenerate.
2
+
3
+ import ProjectList from "./project/list";
4
+ import ProjectInfo from "./project/info";
5
+ import ProjectDuplicate from "./project/duplicate";
6
+ import ProjectRename from "./project/rename";
7
+ import ProjectDelete from "./project/delete";
8
+ import AdminCopyProject from "./admin/copy-project";
9
+
10
+ export const COMMANDS = {
11
+ "project:list": ProjectList,
12
+ "project:info": ProjectInfo,
13
+ "project:duplicate": ProjectDuplicate,
14
+ "project:rename": ProjectRename,
15
+ "project:delete": ProjectDelete,
16
+ "admin:copy-project": AdminCopyProject,
17
+ };
@@ -0,0 +1,58 @@
1
+ import { Args, Flags } from "@oclif/core";
2
+ import { createInterface } from "node:readline";
3
+ import { PlCommand } from "../../base_command";
4
+ import { resolveProject, deleteProject, getProjectInfo } from "../../project_ops";
5
+ import { outputJson, outputText } from "../../output";
6
+
7
+ export default class ProjectDelete extends PlCommand {
8
+ static override description = "Delete a project. This permanently destroys all project data.";
9
+
10
+ static override args = {
11
+ project: Args.string({
12
+ description: "Project ID or label",
13
+ required: true,
14
+ }),
15
+ };
16
+
17
+ static override flags = {
18
+ ...PlCommand.baseFlags,
19
+ force: Flags.boolean({
20
+ summary: "Skip confirmation",
21
+ default: false,
22
+ }),
23
+ };
24
+
25
+ public async run(): Promise<void> {
26
+ const { args, flags } = await this.parse(ProjectDelete);
27
+ const { pl, projectListRid } = await this.connect(flags);
28
+ const { id } = await resolveProject(pl, projectListRid, args.project);
29
+
30
+ const info = await getProjectInfo(pl, projectListRid, id);
31
+
32
+ if (!flags.force) {
33
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
34
+ try {
35
+ const answer = await new Promise<string>((resolve) => {
36
+ rl.question(
37
+ `Delete project "${info.label}" (${info.blockCount} blocks)? [y/N] `,
38
+ resolve,
39
+ );
40
+ });
41
+ if (answer.toLowerCase() !== "y") {
42
+ outputText("Aborted.");
43
+ return;
44
+ }
45
+ } finally {
46
+ rl.close();
47
+ }
48
+ }
49
+
50
+ await deleteProject(pl, projectListRid, id);
51
+
52
+ if (flags.format === "json") {
53
+ outputJson({ deleted: true, id, label: info.label });
54
+ } else {
55
+ outputText(`Deleted project "${info.label}"`);
56
+ }
57
+ }
58
+ }