pacificdb-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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hitesh Reddy K
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # pacificdb-cli
2
+
3
+ Command-line interface for the PacificDB control plane.
4
+ Runs anywhere Node.js ≥ 18 runs — **Linux, macOS, and Windows**.
5
+
6
+ ## Install
7
+
8
+ **Linux / macOS**
9
+ ```bash
10
+ npm install -g pacificdb-cli
11
+ pacificdb login
12
+ ```
13
+
14
+ **Windows (PowerShell or CMD)**
15
+ ```powershell
16
+ npm install -g pacificdb-cli
17
+ pacificdb login
18
+ ```
19
+ npm creates the `pacificdb` / `pacificdb.cmd` shims automatically; no PATH edits needed.
20
+ Config and the login token are stored per-OS at:
21
+
22
+ | OS | Location |
23
+ | --- | --- |
24
+ | Linux | `~/.pacificdb/config.json` |
25
+ | macOS | `/Users/<you>/.pacificdb/config.json` |
26
+ | Windows | `C:\Users\<you>\.pacificdb\config.json` |
27
+
28
+ Override with `PACIFICDB_CONFIG=<path>` or pass `--api-url` / `PACIFICDB_API_URL` per call.
29
+
30
+ ## Commands
31
+
32
+ ```bash
33
+ pacificdb login # prompts for email/password; token stored in ~/.pacificdb/config.json (0600)
34
+ pacificdb logout
35
+
36
+ pacificdb org list
37
+ pacificdb project create --name demo [--org org_id]
38
+ pacificdb project list
39
+ pacificdb cluster create --name c1 --project prj_id [--tier free]
40
+ pacificdb cluster list [--project prj_id]
41
+ pacificdb cluster status --cluster cl_id
42
+ pacificdb database create --name appdb --cluster cl_id
43
+ pacificdb database list --cluster cl_id
44
+ pacificdb dbuser create --database db_id --username app
45
+ pacificdb dbuser rotate --database db_id --dbuser dbu_id
46
+ pacificdb connection-string generate --database db_id --dbuser dbu_id
47
+ pacificdb backup create --cluster cl_id
48
+ pacificdb backup list
49
+ pacificdb restore create --backup bk_id [--cluster cl_id]
50
+ pacificdb metrics cluster --cluster cl_id
51
+
52
+ pacificdb benchmark run # runs the local artifact-backed quick suite (repo checkout required)
53
+ pacificdb benchmark report # prints the public-safe benchmark report
54
+ ```
55
+
56
+ ## Global flags
57
+
58
+ | Flag | Meaning |
59
+ | --- | --- |
60
+ | `--api-url URL` | Control-plane URL (or `PACIFICDB_API_URL`) |
61
+ | `--format json\|table` | Output format (default `table`) |
62
+ | `--no-color` | Disable ANSI colors |
63
+ | `--org / --project / --cluster / --database` | Scope ids (defaults come from config) |
64
+
65
+ ## Security
66
+
67
+ - The session token is stored in `~/.pacificdb/config.json` with `0600`
68
+ permissions; override the location with `PACIFICDB_CONFIG` or inject the
69
+ token via `PACIFICDB_TOKEN` (CI).
70
+ - Secrets (tokens, passwords, connection strings) are redacted from all
71
+ output except the one-time display when a database user is created or
72
+ rotated — that is the only time the server can show the credential.
73
+ - `--password` on the command line is supported for automation but warns,
74
+ since it exposes the secret to shell history; prefer the interactive prompt.
75
+
76
+ ## Exit codes
77
+
78
+ | Code | Meaning |
79
+ | --- | --- |
80
+ | 0 | Success |
81
+ | 1 | Generic failure |
82
+ | 2 | Usage error |
83
+ | 3 | Not authenticated |
84
+ | 4 | Not found / permission denied |
85
+ | 5 | Server or network error |
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '../src/cli.js';
3
+
4
+ run(process.argv.slice(2)).then(
5
+ (code) => process.exit(code ?? 0),
6
+ (error) => {
7
+ process.stderr.write(`error: ${error.message}\n`);
8
+ process.exit(error.exitCode ?? 1);
9
+ }
10
+ );
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "pacificdb-cli",
3
+ "version": "0.2.0",
4
+ "description": "PacificDB command-line interface for the control plane (orgs, projects, clusters, databases, users, backups, metrics, benchmarks)",
5
+ "keywords": [
6
+ "pacificdb",
7
+ "database",
8
+ "cli"
9
+ ],
10
+ "license": "MIT",
11
+ "author": "PacificDB",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/hitesh-reddy-k/database-engine-linux.git",
15
+ "directory": "packages/pacificdb-cli"
16
+ },
17
+ "type": "module",
18
+ "bin": {
19
+ "pacificdb": "bin/pacificdb.js"
20
+ },
21
+ "files": [
22
+ "bin",
23
+ "src",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "scripts": {
31
+ "test": "node --test test/*.test.mjs"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }
package/src/api.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Control-plane API client for the CLI. Maps failures to CliError with
3
+ * meaningful exit codes: 2 usage, 3 auth, 4 not found / permission, 5 server.
4
+ */
5
+
6
+ import { resolveToken } from './config.js';
7
+
8
+ export class CliError extends Error {
9
+ constructor(message, exitCode = 1) {
10
+ super(message);
11
+ this.exitCode = exitCode;
12
+ }
13
+ }
14
+
15
+ export async function api(apiUrl, method, path, body, { token, timeoutMs = 30000 } = {}) {
16
+ const auth = token ?? resolveToken();
17
+ const controller = new AbortController();
18
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
19
+ let res;
20
+ try {
21
+ res = await fetch(`${apiUrl}${path}`, {
22
+ method,
23
+ signal: controller.signal,
24
+ headers: {
25
+ 'content-type': 'application/json',
26
+ ...(auth ? { authorization: `Bearer ${auth}` } : {}),
27
+ },
28
+ body: body === undefined ? undefined : JSON.stringify(body),
29
+ });
30
+ } catch (error) {
31
+ clearTimeout(timer);
32
+ if (error.name === 'AbortError') throw new CliError(`Request timed out after ${timeoutMs}ms`, 5);
33
+ throw new CliError(`Cannot reach PacificDB API at ${apiUrl}: ${error.message}`, 5);
34
+ }
35
+ clearTimeout(timer);
36
+
37
+ let json = {};
38
+ try { json = await res.json(); } catch { /* empty body */ }
39
+
40
+ if (res.ok && json.success !== false) return json.data ?? json;
41
+
42
+ const message = typeof json.error === 'object' ? json.error?.message : json.error;
43
+ if (res.status === 401) throw new CliError(`Not authenticated${message ? `: ${message}` : ''}. Run \`pacificdb login\`.`, 3);
44
+ if (res.status === 403) throw new CliError(`Permission denied${message ? `: ${message}` : ''}`, 4);
45
+ if (res.status === 404) throw new CliError(`Not found${message ? `: ${message}` : ''}`, 4);
46
+ if (res.status === 429) throw new CliError('Rate limited by the API. Try again shortly.', 5);
47
+ throw new CliError(message || `API error (HTTP ${res.status})`, res.status >= 500 ? 5 : 1);
48
+ }
package/src/cli.js ADDED
@@ -0,0 +1,330 @@
1
+ /**
2
+ * pacificdb CLI - command router.
3
+ *
4
+ * Commands (see README for details):
5
+ * login | logout
6
+ * org list
7
+ * project create|list
8
+ * cluster create|list|status
9
+ * database create|list
10
+ * dbuser create|rotate
11
+ * connection-string generate
12
+ * backup create|list
13
+ * restore create
14
+ * metrics cluster
15
+ * benchmark run|report
16
+ *
17
+ * Global flags: --api-url --org --project --cluster --database
18
+ * --format json|table --no-color --quiet
19
+ * Exit codes: 0 ok, 1 generic, 2 usage, 3 auth, 4 not-found/denied, 5 server.
20
+ */
21
+
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+ import { createInterface } from 'node:readline';
25
+ import { api, CliError } from './api.js';
26
+ import { loadConfig, saveConfig, clearToken, resolveApiUrl, configPath } from './config.js';
27
+ import { printResult, color } from './output.js';
28
+
29
+ const USAGE = `pacificdb <command> [subcommand] [flags]
30
+
31
+ Auth
32
+ login [--email x --password y] Log in and store the session token
33
+ logout Revoke the session and clear the token
34
+
35
+ Resources
36
+ org list
37
+ project create --name N [--org ORG_ID]
38
+ project list [--org ORG_ID]
39
+ cluster create --name N --project PRJ [--tier free]
40
+ cluster list [--project PRJ]
41
+ cluster status --cluster CL
42
+ database create --name N --cluster CL
43
+ database list --cluster CL
44
+ dbuser create --database DB --username U
45
+ dbuser rotate --database DB --dbuser DBU
46
+ connection-string generate --database DB --dbuser DBU
47
+ backup create --cluster CL [--type full]
48
+ backup list
49
+ restore create --backup BK [--cluster CL]
50
+ metrics cluster --cluster CL
51
+
52
+ Benchmarks
53
+ benchmark run Run the local artifact-backed quick suite
54
+ benchmark report Print the public-safe benchmark report
55
+
56
+ Global flags
57
+ --api-url URL PacificDB control plane (or PACIFICDB_API_URL)
58
+ --format json|table (default table)
59
+ --no-color Disable ANSI colors
60
+ --help Show this help
61
+ `;
62
+
63
+ export function parseArgs(argv) {
64
+ const positional = [];
65
+ const flags = {};
66
+ for (let i = 0; i < argv.length; i++) {
67
+ const arg = argv[i];
68
+ if (arg === '--help' || arg === '-h') flags.help = true;
69
+ else if (arg === '--no-color') flags.noColor = true;
70
+ else if (arg === '--quiet' || arg === '-q') flags.quiet = true;
71
+ else if (arg.startsWith('--')) {
72
+ const key = arg.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
73
+ const next = argv[i + 1];
74
+ if (next === undefined || next.startsWith('--')) flags[key] = true;
75
+ else { flags[key] = next; i++; }
76
+ } else {
77
+ positional.push(arg);
78
+ }
79
+ }
80
+ if (flags.format && !['json', 'table'].includes(flags.format)) {
81
+ throw new CliError(`Invalid --format "${flags.format}" (expected json or table)`, 2);
82
+ }
83
+ return { positional, flags };
84
+ }
85
+
86
+ async function promptHidden(question) {
87
+ const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
88
+ const muted = { muted: false };
89
+ rl._writeToOutput = function (s) {
90
+ if (muted.muted && s.trim() && !s.includes(question)) return;
91
+ process.stderr.write(s);
92
+ };
93
+ const answer = await new Promise((resolve) => {
94
+ rl.question(question, (a) => { process.stderr.write('\n'); resolve(a); });
95
+ muted.muted = true;
96
+ });
97
+ rl.close();
98
+ return answer;
99
+ }
100
+
101
+ async function prompt(question) {
102
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
103
+ const answer = await new Promise((resolve) => rl.question(question, resolve));
104
+ rl.close();
105
+ return answer;
106
+ }
107
+
108
+ function requireFlag(flags, name, exitCode = 2) {
109
+ const value = flags[name];
110
+ if (!value || value === true) throw new CliError(`--${name} is required`, exitCode);
111
+ return value;
112
+ }
113
+
114
+ function findRepoRoot(startDir) {
115
+ let dir = startDir;
116
+ for (let i = 0; i < 8; i++) {
117
+ if (fs.existsSync(path.join(dir, 'benchmarks', 'reports'))) return dir;
118
+ const parent = path.dirname(dir);
119
+ if (parent === dir) break;
120
+ dir = parent;
121
+ }
122
+ return null;
123
+ }
124
+
125
+ export async function run(argv, { io = process } = {}) {
126
+ const { positional, flags } = parseArgs(argv);
127
+ const [command, subcommand] = positional;
128
+ const format = flags.format || 'table';
129
+ const c = color(!flags.noColor && io.stdout.isTTY);
130
+ const apiUrl = resolveApiUrl(flags.apiUrl);
131
+ const out = (data, opts = {}) => printResult(data, { format, ...opts });
132
+
133
+ if (!command || flags.help) {
134
+ io.stdout.write(USAGE);
135
+ return command ? 0 : 2;
136
+ }
137
+
138
+ switch (`${command} ${subcommand || ''}`.trim()) {
139
+ case 'login': {
140
+ const email = flags.email || (await prompt('email: '));
141
+ const password = flags.password || (await promptHidden('password: '));
142
+ if (flags.password) {
143
+ io.stderr.write(c.yellow('warning: --password exposes the secret to shell history; prefer the prompt\n'));
144
+ }
145
+ const data = await api(apiUrl, 'POST', '/api/v1/auth/login', { email, password }, { token: '' });
146
+ const token = data?.session?.token;
147
+ if (!token) throw new CliError('Login did not return a session token', 3);
148
+ saveConfig({ apiUrl, token, userId: data.user?.id, email: data.user?.email });
149
+ const orgs = (data.organizations || []).map((o) => ({ id: o.id, name: o.name, role: o.role || '' }));
150
+ if (orgs.length === 1) saveConfig({ orgId: orgs[0].id });
151
+ io.stderr.write(c.green(`logged in as ${data.user?.email} (config: ${configPath()})\n`));
152
+ out({ user: { id: data.user?.id, email: data.user?.email }, organizations: orgs }, { rows: orgs, columns: ['id', 'name', 'role'] });
153
+ return 0;
154
+ }
155
+
156
+ case 'logout': {
157
+ try { await api(apiUrl, 'POST', '/api/v1/auth/logout'); } catch { /* already invalid is fine */ }
158
+ clearToken();
159
+ io.stderr.write('logged out\n');
160
+ return 0;
161
+ }
162
+
163
+ case 'org list': {
164
+ const data = await api(apiUrl, 'GET', '/api/v1/orgs');
165
+ const rows = (data.organizations || []).map((o) => ({ id: o.id, name: o.name, plan: o.plan, members: (o.members || []).length }));
166
+ out(data, { rows, columns: ['id', 'name', 'plan', 'members'] });
167
+ return 0;
168
+ }
169
+
170
+ case 'project create': {
171
+ const name = requireFlag(flags, 'name');
172
+ const organizationId = flags.org || loadConfig().orgId;
173
+ if (!organizationId) throw new CliError('--org is required (no default org in config)', 2);
174
+ const data = await api(apiUrl, 'POST', '/api/v1/projects', { name, organizationId });
175
+ out(data, { rows: [{ id: data.project?.id, name: data.project?.name, org: organizationId }] });
176
+ return 0;
177
+ }
178
+
179
+ case 'project list': {
180
+ const organizationId = flags.org || loadConfig().orgId;
181
+ const data = await api(apiUrl, 'GET', `/api/v1/projects${organizationId ? `?organizationId=${organizationId}` : ''}`);
182
+ const rows = (data.projects || []).map((p) => ({ id: p.id, name: p.name, org: p.organizationId, clusters: p.clusterCount ?? '' }));
183
+ out(data, { rows, columns: ['id', 'name', 'org', 'clusters'] });
184
+ return 0;
185
+ }
186
+
187
+ case 'cluster create': {
188
+ const name = requireFlag(flags, 'name');
189
+ const projectId = flags.project || loadConfig().projectId;
190
+ if (!projectId) throw new CliError('--project is required', 2);
191
+ const data = await api(apiUrl, 'POST', '/api/v1/clusters', { name, projectId, tier: flags.tier || 'free', region: flags.region });
192
+ out(data, { rows: [{ id: data.cluster?.id, name: data.cluster?.name, tier: data.cluster?.tier, status: data.cluster?.status }] });
193
+ return 0;
194
+ }
195
+
196
+ case 'cluster list': {
197
+ const projectId = flags.project || '';
198
+ const data = await api(apiUrl, 'GET', `/api/v1/clusters${projectId ? `?projectId=${projectId}` : ''}`);
199
+ const rows = (data.clusters || []).map((cl) => ({ id: cl.id, name: cl.name, tier: cl.tier, region: cl.region, status: cl.status }));
200
+ out(data, { rows, columns: ['id', 'name', 'tier', 'region', 'status'] });
201
+ return 0;
202
+ }
203
+
204
+ case 'cluster status': {
205
+ const clusterId = requireFlag(flags, 'cluster');
206
+ const data = await api(apiUrl, 'GET', `/api/v1/clusters/${clusterId}`);
207
+ const cl = data.cluster || {};
208
+ out(data, {
209
+ rows: [{ id: cl.id, name: cl.name, status: cl.status, tier: cl.tier, nodes: (data.nodes || []).length, alerts: (data.alerts || []).length }],
210
+ });
211
+ return 0;
212
+ }
213
+
214
+ case 'database create': {
215
+ const name = requireFlag(flags, 'name');
216
+ const clusterId = requireFlag(flags, 'cluster');
217
+ const data = await api(apiUrl, 'POST', `/api/v1/clusters/${clusterId}/databases`, { name });
218
+ out(data, { rows: [{ id: data.database?.id, name: data.database?.name, engineProvisioned: data.engineProvisioned }] });
219
+ return 0;
220
+ }
221
+
222
+ case 'database list': {
223
+ const clusterId = requireFlag(flags, 'cluster');
224
+ const data = await api(apiUrl, 'GET', `/api/v1/clusters/${clusterId}/databases`);
225
+ const rows = (data.databases || []).map((d) => ({ id: d.id, name: d.name, status: d.status }));
226
+ out(data, { rows, columns: ['id', 'name', 'status'] });
227
+ return 0;
228
+ }
229
+
230
+ case 'dbuser create': {
231
+ const databaseId = requireFlag(flags, 'database');
232
+ const username = requireFlag(flags, 'username');
233
+ const data = await api(apiUrl, 'POST', `/api/v1/databases/${databaseId}/users`, {
234
+ username,
235
+ roles: flags.roles ? String(flags.roles).split(',') : undefined,
236
+ ipAllowlist: flags.ipAllowlist ? String(flags.ipAllowlist).split(',') : undefined,
237
+ });
238
+ io.stderr.write(c.yellow(`\n${data.warning || 'Store the connection string now; it is shown only once.'}\n\n`));
239
+ out(data, {
240
+ rows: [{ id: data.user?.id, username: data.user?.username, connectionString: data.connectionString }],
241
+ columns: ['id', 'username', 'connectionString'],
242
+ showSecrets: new Set(['connectionString']),
243
+ });
244
+ return 0;
245
+ }
246
+
247
+ case 'dbuser rotate': {
248
+ const databaseId = requireFlag(flags, 'database');
249
+ const dbUserId = requireFlag(flags, 'dbuser');
250
+ const data = await api(apiUrl, 'POST', `/api/v1/databases/${databaseId}/users/${dbUserId}/rotate-password`, {});
251
+ io.stderr.write(c.yellow(`\n${data.warning || 'New connection string shown once; old credentials are revoked.'}\n\n`));
252
+ out(data, {
253
+ rows: [{ id: data.user?.id, revokedSessions: data.revokedSessions, connectionString: data.connectionString }],
254
+ columns: ['id', 'revokedSessions', 'connectionString'],
255
+ showSecrets: new Set(['connectionString']),
256
+ });
257
+ return 0;
258
+ }
259
+
260
+ case 'connection-string generate': {
261
+ const databaseId = requireFlag(flags, 'database');
262
+ const dbUserId = requireFlag(flags, 'dbuser');
263
+ const data = await api(apiUrl, 'POST', `/api/v1/databases/${databaseId}/connection-string`, { dbUserId });
264
+ io.stderr.write(c.yellow(`\n${data.warning || 'Connection string shown once (credentials were rotated).'}\n\n`));
265
+ out(data, {
266
+ rows: [{ dbUserId, connectionString: data.connectionString }],
267
+ columns: ['dbUserId', 'connectionString'],
268
+ showSecrets: new Set(['connectionString']),
269
+ });
270
+ return 0;
271
+ }
272
+
273
+ case 'backup create': {
274
+ const clusterId = flags.cluster || loadConfig().clusterId;
275
+ if (!clusterId) throw new CliError('--cluster is required', 2);
276
+ const data = await api(apiUrl, 'POST', '/api/v1/backups', { clusterId, type: flags.type || 'manual' });
277
+ out(data, { rows: [{ id: data.backup?.id, status: data.backup?.status, type: data.backup?.type }] });
278
+ return 0;
279
+ }
280
+
281
+ case 'backup list': {
282
+ const data = await api(apiUrl, 'GET', '/api/v1/backups');
283
+ const rows = (data.backups || []).map((b) => ({ id: b.id, cluster: b.clusterId, type: b.type, status: b.status, createdAt: b.createdAt }));
284
+ out(data, { rows, columns: ['id', 'cluster', 'type', 'status', 'createdAt'] });
285
+ return 0;
286
+ }
287
+
288
+ case 'restore create': {
289
+ const backupId = requireFlag(flags, 'backup');
290
+ const data = await api(apiUrl, 'POST', `/api/v1/backups/${backupId}/restore`, {
291
+ targetClusterId: flags.cluster || undefined,
292
+ });
293
+ out(data, { rows: [{ backup: backupId, target: data.targetCluster, message: data.message || 'restore initiated' }] });
294
+ return 0;
295
+ }
296
+
297
+ case 'metrics cluster': {
298
+ const clusterId = requireFlag(flags, 'cluster');
299
+ const data = await api(apiUrl, 'GET', `/api/v1/metrics/cluster/${clusterId}`);
300
+ out(data, { rows: Array.isArray(data) ? data : [data] });
301
+ return 0;
302
+ }
303
+
304
+ case 'benchmark run': {
305
+ const root = findRepoRoot(io.cwd ? io.cwd() : process.cwd());
306
+ if (!root) {
307
+ throw new CliError('benchmark run must be executed inside a PacificDB repository (benchmarks/ not found). Benchmarks execute locally against your own hardware; results are artifact-backed and claim-safe.', 2);
308
+ }
309
+ const { spawnSync } = await import('node:child_process');
310
+ io.stderr.write('running: npm run benchmark:quick (system + storage + security tools + report)\n');
311
+ // shell:true resolves npm to npm.cmd on Windows; args are fixed strings (no injection surface).
312
+ const res = spawnSync('npm', ['run', 'benchmark:quick'], { cwd: root, stdio: 'inherit', shell: process.platform === 'win32' });
313
+ return res.status ?? 1;
314
+ }
315
+
316
+ case 'benchmark report': {
317
+ const root = findRepoRoot(io.cwd ? io.cwd() : process.cwd());
318
+ const reportPath = root && path.join(root, 'benchmarks', 'reports', 'final', 'PACIFICDB_PUBLIC_RESULTS.md');
319
+ if (!reportPath || !fs.existsSync(reportPath)) {
320
+ throw new CliError('No public benchmark report found (benchmarks/reports/final/PACIFICDB_PUBLIC_RESULTS.md). Run `pacificdb benchmark run` first.', 4);
321
+ }
322
+ io.stdout.write(fs.readFileSync(reportPath, 'utf8'));
323
+ return 0;
324
+ }
325
+
326
+ default:
327
+ io.stderr.write(USAGE);
328
+ throw new CliError(`Unknown command: ${positional.join(' ') || '(none)'}`, 2);
329
+ }
330
+ }
package/src/config.js ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * CLI configuration: ~/.pacificdb/config.json (created 0600).
3
+ * Stores the API URL, session token, and default org/project/cluster ids.
4
+ * The token can be overridden with PACIFICDB_TOKEN, the config path with
5
+ * PACIFICDB_CONFIG, and the API URL with PACIFICDB_API_URL.
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import os from 'node:os';
11
+
12
+ export function configPath() {
13
+ return process.env.PACIFICDB_CONFIG || path.join(os.homedir(), '.pacificdb', 'config.json');
14
+ }
15
+
16
+ export function loadConfig() {
17
+ try {
18
+ return JSON.parse(fs.readFileSync(configPath(), 'utf8'));
19
+ } catch {
20
+ return {};
21
+ }
22
+ }
23
+
24
+ export function saveConfig(update) {
25
+ const file = configPath();
26
+ const dir = path.dirname(file);
27
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
28
+ const merged = { ...loadConfig(), ...update };
29
+ fs.writeFileSync(file, JSON.stringify(merged, null, 2) + '\n', { mode: 0o600 });
30
+ try { fs.chmodSync(file, 0o600); } catch { /* best effort on non-POSIX */ }
31
+ return merged;
32
+ }
33
+
34
+ export function clearToken() {
35
+ const cfg = loadConfig();
36
+ delete cfg.token;
37
+ delete cfg.userId;
38
+ const file = configPath();
39
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
40
+ fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + '\n', { mode: 0o600 });
41
+ return cfg;
42
+ }
43
+
44
+ export function resolveToken() {
45
+ return process.env.PACIFICDB_TOKEN || loadConfig().token || '';
46
+ }
47
+
48
+ export function resolveApiUrl(flagValue) {
49
+ return (flagValue || process.env.PACIFICDB_API_URL || loadConfig().apiUrl || 'http://localhost:3000').replace(/\/+$/, '');
50
+ }
package/src/output.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Output helpers: JSON or aligned tables, optional color, secret redaction.
3
+ */
4
+
5
+ const SECRET_KEYS = new Set(['password', 'token', 'secret', 'key', 'sessiontoken', 'connectionstring', 'passwordhash']);
6
+
7
+ export function redact(value, { except = new Set() } = {}) {
8
+ if (!value || typeof value !== 'object') return value;
9
+ if (Array.isArray(value)) return value.map((v) => redact(v, { except }));
10
+ const out = {};
11
+ for (const [k, v] of Object.entries(value)) {
12
+ if (SECRET_KEYS.has(k.toLowerCase()) && !except.has(k)) {
13
+ out[k] = 'REDACTED';
14
+ } else {
15
+ out[k] = redact(v, { except });
16
+ }
17
+ }
18
+ return out;
19
+ }
20
+
21
+ export function color(enabled) {
22
+ const wrap = (code) => (s) => (enabled ? `[${code}m${s}` : String(s));
23
+ return { bold: wrap('1'), green: wrap('32'), red: wrap('31'), yellow: wrap('33'), dim: wrap('2') };
24
+ }
25
+
26
+ export function table(rows, columns) {
27
+ if (!rows.length) return '(no results)';
28
+ const cols = columns || Object.keys(rows[0]);
29
+ const cells = rows.map((r) => cols.map((c) => formatCell(r[c])));
30
+ const widths = cols.map((c, i) => Math.max(c.length, ...cells.map((row) => row[i].length)));
31
+ const line = (parts) => parts.map((p, i) => p.padEnd(widths[i])).join(' ');
32
+ return [line(cols.map((c) => c.toUpperCase())), line(widths.map((w) => '-'.repeat(w))), ...cells.map(line)].join('\n');
33
+ }
34
+
35
+ function formatCell(v) {
36
+ if (v === null || v === undefined) return '';
37
+ if (typeof v === 'object') return JSON.stringify(v);
38
+ return String(v);
39
+ }
40
+
41
+ export function printResult(data, { format, rows, columns, showSecrets = new Set() }) {
42
+ if (format === 'json') {
43
+ process.stdout.write(JSON.stringify(redact(data, { except: showSecrets }), null, 2) + '\n');
44
+ } else {
45
+ const safeRows = redact(rows ?? data, { except: showSecrets });
46
+ process.stdout.write(table(Array.isArray(safeRows) ? safeRows : [safeRows], columns) + '\n');
47
+ }
48
+ }