@pb-admin/cli 1.0.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/dist/index.js +922 -0
  4. package/package.json +34 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024
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,114 @@
1
+ # @pb-admin/cli
2
+
3
+ CLI to manage PocketBase collections, rules, and schema across multiple instances.
4
+
5
+ ## Features
6
+
7
+ - **Schema Management** - Pull/push collection fields
8
+ - **Rules Management** - Manage list/view/create/update/delete rules
9
+ - **Multiple Instances** - Login to different PocketBase servers
10
+ - **Diff & Apply** - Compare local config vs live, then apply changes
11
+ - **Interactive** - Login, view collections, select options
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ # Use directly with bunx
17
+ bunx @pb-admin/cli init
18
+
19
+ # Or install globally
20
+ bun add -g @pb-admin/cli
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```bash
26
+ # 1. Initialize in your project
27
+ pb-admin init
28
+
29
+ # 2. Login to your PocketBase
30
+ pb-admin login
31
+
32
+ # 3. Pull current schema and rules
33
+ pb-admin pull
34
+
35
+ # 4. Edit the files in pb-admin/
36
+
37
+ # 5. See differences
38
+ pb-admin diff
39
+
40
+ # 6. Apply changes
41
+ pb-admin push
42
+ ```
43
+
44
+ ## Commands
45
+
46
+ | Command | Description |
47
+ |---------|-------------|
48
+ | `pb-admin init` | Initialize project with config file |
49
+ | `pb-admin login` | Login to PocketBase (saves token) |
50
+ | `pb-admin logout` | Clear saved credentials |
51
+ | `pb-admin pull` | Pull schema & rules from PocketBase |
52
+ | `pb-admin push` | Push schema & rules to PocketBase |
53
+ | `pb-admin diff` | Compare local vs live |
54
+ | `pb-admin col [name]` | List/view collections |
55
+
56
+ ## Options
57
+
58
+ - `--schema, -s` - Only schema operations
59
+ - `--rules, -r` - Only rules operations
60
+ - `--dry-run, -n` - Show what would change (no apply)
61
+ - `--debug` - Show error details
62
+
63
+ ## Files
64
+
65
+ After `pb-admin init`, these files are configured:
66
+
67
+ ```
68
+ pb-admin.json # Config (URL, paths)
69
+ pb-admin/
70
+ schema.json # Collection fields
71
+ rules.json # Collection permissions
72
+ ```
73
+
74
+ ## Example Workflow
75
+
76
+ ```bash
77
+ # Start fresh
78
+ pb-admin init
79
+ pb-admin login
80
+
81
+ # Pull current state
82
+ pb-admin pull
83
+
84
+ # Edit rules locally
85
+ # vim pb-admin/rules.json
86
+
87
+ # See what changed
88
+ pb-admin diff
89
+
90
+ # Apply to live
91
+ pb-admin push
92
+ ```
93
+
94
+ ## For CI/CD
95
+
96
+ ```bash
97
+ # Non-interactive mode (set env vars first)
98
+ export PB_URL=https://your-pb.com
99
+ export PB_EMAIL=admin@example.com
100
+ export PB_PASSWORD=your-password
101
+
102
+ pb-admin pull --rules
103
+ # edit files...
104
+ pb-admin push --dry-run
105
+ pb-admin push
106
+ ```
107
+
108
+ ## Credentials
109
+
110
+ Credentials are stored in `~/.config/pb-admin/credentials.json` (cross-platform config dir).
111
+
112
+ ## License
113
+
114
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,922 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import pc6 from "picocolors";
5
+ import inquirer3 from "inquirer";
6
+
7
+ // src/commands/init.ts
8
+ import pc from "picocolors";
9
+ import inquirer from "inquirer";
10
+
11
+ // src/lib/config.ts
12
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
13
+ import { join, dirname } from "path";
14
+ var DEFAULT_CONFIG = {
15
+ url: "",
16
+ schemaPath: "./pb-admin/schema.json",
17
+ rulesPath: "./pb-admin/rules.json"
18
+ };
19
+ function loadConfig(configPath) {
20
+ const path = configPath || findConfigFile();
21
+ if (!path || !existsSync(path)) {
22
+ return { ...DEFAULT_CONFIG };
23
+ }
24
+ try {
25
+ const content = readFileSync(path, "utf-8");
26
+ return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
27
+ } catch {
28
+ return { ...DEFAULT_CONFIG };
29
+ }
30
+ }
31
+ function saveConfig(config, configPath) {
32
+ const path = configPath || findConfigFile();
33
+ if (!path) {
34
+ console.log('❌ No config file found. Run "pb-admin init" first.');
35
+ return;
36
+ }
37
+ const dir = dirname(path);
38
+ if (!existsSync(dir)) {
39
+ mkdirSync(dir, { recursive: true });
40
+ }
41
+ writeFileSync(path, JSON.stringify(config, null, 2), "utf-8");
42
+ }
43
+ function findConfigFile() {
44
+ let dir = process.cwd();
45
+ if (existsSync(join(dir, "pb-admin.json"))) {
46
+ return join(dir, "pb-admin.json");
47
+ }
48
+ while (dir !== "/") {
49
+ const parent = dirname(dir);
50
+ if (parent === dir)
51
+ break;
52
+ dir = parent;
53
+ const configPath = join(dir, "pb-admin.json");
54
+ if (existsSync(configPath)) {
55
+ return configPath;
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ function ensurePBAdminDir(basePath) {
61
+ const dir = basePath || join(process.cwd(), "pb-admin");
62
+ if (!existsSync(dir)) {
63
+ mkdirSync(dir, { recursive: true });
64
+ }
65
+ return dir;
66
+ }
67
+ function getSchemaPath() {
68
+ const config = loadConfig();
69
+ return config.schemaPath || "./pb-admin/schema.json";
70
+ }
71
+ function getRulesPath() {
72
+ const config = loadConfig();
73
+ return config.rulesPath || "./pb-admin/rules.json";
74
+ }
75
+ function loadSchema() {
76
+ const path = getSchemaPath();
77
+ if (!existsSync(path))
78
+ return null;
79
+ try {
80
+ return JSON.parse(readFileSync(path, "utf-8"));
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+ function loadRules() {
86
+ const path = getRulesPath();
87
+ if (!existsSync(path))
88
+ return null;
89
+ try {
90
+ return JSON.parse(readFileSync(path, "utf-8"));
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+ function saveSchema(schema) {
96
+ const path = getSchemaPath();
97
+ const dir = dirname(path);
98
+ if (!existsSync(dir)) {
99
+ mkdirSync(dir, { recursive: true });
100
+ }
101
+ writeFileSync(path, JSON.stringify(schema, null, 2), "utf-8");
102
+ }
103
+ function saveRules(rules) {
104
+ const path = getRulesPath();
105
+ const dir = dirname(path);
106
+ if (!existsSync(dir)) {
107
+ mkdirSync(dir, { recursive: true });
108
+ }
109
+ writeFileSync(path, JSON.stringify(rules, null, 2), "utf-8");
110
+ }
111
+
112
+ // src/lib/client.ts
113
+ import PocketBase from "pocketbase";
114
+
115
+ // src/lib/credentials.ts
116
+ import { homedir } from "os";
117
+ import { join as join2 } from "path";
118
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync } from "fs";
119
+ function getProjectKey() {
120
+ const cwd = process.cwd();
121
+ return cwd.replace(/^\//, "").replace(/[^a-zA-Z0-9]/g, "-").replace(/-+/g, "-").replace(/-$/, "");
122
+ }
123
+ function getProjectDir() {
124
+ const base = join2(homedir(), ".config", "pb-admin", "projects");
125
+ const key = getProjectKey();
126
+ return join2(base, key);
127
+ }
128
+ function ensureProjectDir() {
129
+ const dir = getProjectDir();
130
+ if (!existsSync2(dir)) {
131
+ mkdirSync2(dir, { recursive: true });
132
+ }
133
+ return dir;
134
+ }
135
+ function loadCredentials() {
136
+ try {
137
+ const dir = getProjectDir();
138
+ const credsPath = join2(dir, "credentials.json");
139
+ if (existsSync2(credsPath)) {
140
+ const content = readFileSync2(credsPath, "utf-8");
141
+ return JSON.parse(content);
142
+ }
143
+ } catch {}
144
+ return null;
145
+ }
146
+ function saveCredentials(credentials) {
147
+ const dir = ensureProjectDir();
148
+ const credsPath = join2(dir, "credentials.json");
149
+ if (credentials === null) {
150
+ if (existsSync2(credsPath)) {
151
+ unlinkSync(credsPath);
152
+ }
153
+ return;
154
+ }
155
+ writeFileSync2(credsPath, JSON.stringify(credentials, null, 2), "utf-8");
156
+ }
157
+
158
+ // src/lib/client.ts
159
+ class NotLoggedInError extends Error {
160
+ constructor(message = 'Not logged in. Run "pb-admin login" first.') {
161
+ super(message);
162
+ this.name = "NotLoggedInError";
163
+ }
164
+ }
165
+
166
+ class SessionExpiredError extends Error {
167
+ constructor(message = 'Session expired. Run "pb-admin login" again.') {
168
+ super(message);
169
+ this.name = "SessionExpiredError";
170
+ }
171
+ }
172
+ var cachedClient = null;
173
+ var cachedInstance = null;
174
+ async function getClient() {
175
+ const creds = loadCredentials();
176
+ if (!creds) {
177
+ throw new NotLoggedInError;
178
+ }
179
+ const instanceUrl = creds.url;
180
+ const instanceToken = creds.token;
181
+ if (cachedClient && cachedInstance?.url === instanceUrl) {
182
+ return { pb: cachedClient, instance: cachedInstance };
183
+ }
184
+ const pb = new PocketBase(instanceUrl);
185
+ pb.autoCancellation(false);
186
+ if (instanceToken) {
187
+ pb.authStore.save(instanceToken, null);
188
+ if (pb.authStore.isValid) {
189
+ cachedClient = pb;
190
+ cachedInstance = { url: instanceUrl, token: instanceToken };
191
+ return { pb, instance: cachedInstance };
192
+ }
193
+ }
194
+ throw new SessionExpiredError;
195
+ }
196
+ async function login(url, email, password) {
197
+ const pb = new PocketBase(url);
198
+ pb.autoCancellation(false);
199
+ await pb.collection("_superusers").authWithPassword(email, password);
200
+ if (!pb.authStore.isValid) {
201
+ throw new Error("Login failed - invalid credentials");
202
+ }
203
+ saveCredentials({ url, token: pb.authStore.token, email });
204
+ cachedClient = pb;
205
+ cachedInstance = { url, token: pb.authStore.token };
206
+ return pb;
207
+ }
208
+ function logout() {
209
+ cachedClient = null;
210
+ cachedInstance = null;
211
+ saveCredentials(null);
212
+ }
213
+ function isLoggedIn() {
214
+ const creds = loadCredentials();
215
+ return !!creds?.token;
216
+ }
217
+
218
+ // src/lib/schema.ts
219
+ async function pullSchema(pb) {
220
+ const collections = await pb.collections.getFullList();
221
+ const schema = {};
222
+ for (const col of collections) {
223
+ if (col.name.startsWith("_"))
224
+ continue;
225
+ schema[col.name] = {
226
+ name: col.name,
227
+ type: col.type,
228
+ active: col.active,
229
+ fields: col.fields.filter((f) => !f.system).map((f) => ({
230
+ name: f.name,
231
+ type: f.type,
232
+ required: f.required,
233
+ presentable: f.presentable,
234
+ values: f.values,
235
+ options: f.options
236
+ })),
237
+ listRule: col.listRule,
238
+ viewRule: col.viewRule,
239
+ createRule: col.createRule,
240
+ updateRule: col.updateRule,
241
+ deleteRule: col.deleteRule
242
+ };
243
+ }
244
+ return schema;
245
+ }
246
+ async function getCollection(pb, name) {
247
+ const collections = await pb.collections.getFullList();
248
+ const col = collections.find((c) => c.name === name);
249
+ if (!col)
250
+ return null;
251
+ return {
252
+ name: col.name,
253
+ type: col.type,
254
+ active: col.active,
255
+ fields: col.fields.filter((f) => !f.system).map((f) => ({
256
+ name: f.name,
257
+ type: f.type,
258
+ required: f.required,
259
+ presentable: f.presentable,
260
+ values: f.values,
261
+ options: f.options
262
+ })),
263
+ listRule: col.listRule,
264
+ viewRule: col.viewRule,
265
+ createRule: col.createRule,
266
+ updateRule: col.updateRule,
267
+ deleteRule: col.deleteRule
268
+ };
269
+ }
270
+ function fieldsMatch(desired, current) {
271
+ if (desired.type !== current.type)
272
+ return false;
273
+ if (desired.required !== undefined && desired.required !== (current.required ?? false))
274
+ return false;
275
+ if (desired.presentable !== undefined && desired.presentable !== (current.presentable ?? false))
276
+ return false;
277
+ if (desired.values && JSON.stringify(desired.values) !== JSON.stringify(current.values))
278
+ return false;
279
+ return true;
280
+ }
281
+ async function applySchema(pb, desiredSchema) {
282
+ const collections = await pb.collections.getFullList();
283
+ for (const [colName, colSchema] of Object.entries(desiredSchema)) {
284
+ if (colName.startsWith("$"))
285
+ continue;
286
+ const col = collections.find((c) => c.name === colName);
287
+ if (!col) {
288
+ console.log(`⚠️ ${colName}: collection not found, skipping`);
289
+ continue;
290
+ }
291
+ const currentFields = col.fields;
292
+ const desiredFields = colSchema.fields;
293
+ let hasChanges = false;
294
+ const updatedFields = [...currentFields];
295
+ for (const desired of desiredFields) {
296
+ const currentIndex = updatedFields.findIndex((f) => f.name === desired.name);
297
+ if (currentIndex === -1) {
298
+ const newField = {
299
+ id: `field_${desired.name}_${Date.now()}`,
300
+ name: desired.name,
301
+ type: desired.type,
302
+ required: desired.required ?? false,
303
+ presentable: desired.presentable ?? false,
304
+ system: false
305
+ };
306
+ if (desired.values)
307
+ newField.values = desired.values;
308
+ if (desired.options)
309
+ newField.options = desired.options;
310
+ updatedFields.push(newField);
311
+ hasChanges = true;
312
+ console.log(` + ${colName}.${desired.name}`);
313
+ } else if (!fieldsMatch(desired, updatedFields[currentIndex])) {
314
+ const existing = updatedFields[currentIndex];
315
+ updatedFields[currentIndex] = {
316
+ ...existing,
317
+ type: desired.type,
318
+ required: desired.required ?? existing.required,
319
+ presentable: desired.presentable ?? existing.presentable,
320
+ values: desired.values ?? existing.values,
321
+ options: desired.options ?? existing.options
322
+ };
323
+ hasChanges = true;
324
+ console.log(` ~ ${colName}.${desired.name}`);
325
+ }
326
+ }
327
+ if (hasChanges) {
328
+ await pb.collections.update(col.id, { fields: updatedFields });
329
+ console.log(`✅ ${colName}: schema updated`);
330
+ } else {
331
+ console.log(` ${colName}: no changes`);
332
+ }
333
+ }
334
+ }
335
+ async function diffSchema(pb, desiredSchema) {
336
+ const collections = await pb.collections.getFullList();
337
+ let hasDiff = false;
338
+ for (const [colName, colSchema] of Object.entries(desiredSchema)) {
339
+ if (colName.startsWith("$"))
340
+ continue;
341
+ const col = collections.find((c) => c.name === colName);
342
+ if (!col) {
343
+ console.log(`❌ ${colName}: collection not found in PocketBase`);
344
+ hasDiff = true;
345
+ continue;
346
+ }
347
+ const currentFields = col.fields;
348
+ const desiredFields = colSchema.fields;
349
+ for (const desired of desiredFields) {
350
+ const current = currentFields.find((f) => f.name === desired.name);
351
+ if (!current) {
352
+ hasDiff = true;
353
+ console.log(`${colName}.${desired.name}: ${"MISSING"}`);
354
+ continue;
355
+ }
356
+ if (!fieldsMatch(desired, current)) {
357
+ hasDiff = true;
358
+ console.log(`${colName}.${desired.name}:`);
359
+ console.log(` type: live=${current.type} desired=${desired.type}`);
360
+ console.log(` required: live=${current.required ?? false} desired=${desired.required ?? false}`);
361
+ }
362
+ }
363
+ }
364
+ return hasDiff;
365
+ }
366
+
367
+ // src/lib/rules.ts
368
+ async function pullRules(pb) {
369
+ const collections = await pb.collections.getFullList();
370
+ const rules = {};
371
+ for (const col of collections) {
372
+ if (col.name.startsWith("_"))
373
+ continue;
374
+ rules[col.name] = {
375
+ listRule: col.listRule,
376
+ viewRule: col.viewRule,
377
+ createRule: col.createRule,
378
+ updateRule: col.updateRule,
379
+ deleteRule: col.deleteRule
380
+ };
381
+ }
382
+ return rules;
383
+ }
384
+ async function applyRules(pb, desiredRules) {
385
+ const collections = await pb.collections.getFullList();
386
+ for (const [colName, rules] of Object.entries(desiredRules)) {
387
+ if (colName.startsWith("$"))
388
+ continue;
389
+ const col = collections.find((c) => c.name === colName);
390
+ if (!col) {
391
+ console.log(`⚠️ ${colName}: collection not found, skipping`);
392
+ continue;
393
+ }
394
+ const updates = {};
395
+ let hasChanges = false;
396
+ for (const [ruleKey, desiredValue] of Object.entries(rules)) {
397
+ const currentValue = col[ruleKey];
398
+ if (currentValue !== desiredValue) {
399
+ updates[ruleKey] = desiredValue;
400
+ hasChanges = true;
401
+ }
402
+ }
403
+ if (hasChanges) {
404
+ await pb.collections.update(col.id, updates);
405
+ console.log(`✅ ${colName}: updated`);
406
+ } else {
407
+ console.log(` ${colName}: no changes`);
408
+ }
409
+ }
410
+ }
411
+ async function diffRules(pb, desiredRules) {
412
+ const collections = await pb.collections.getFullList();
413
+ let hasDiff = false;
414
+ for (const [colName, rules] of Object.entries(desiredRules)) {
415
+ if (colName.startsWith("$"))
416
+ continue;
417
+ const col = collections.find((c) => c.name === colName);
418
+ if (!col) {
419
+ console.log(`❌ ${colName}: collection not found in PocketBase`);
420
+ hasDiff = true;
421
+ continue;
422
+ }
423
+ for (const [ruleKey, desiredValue] of Object.entries(rules)) {
424
+ const currentValue = col[ruleKey];
425
+ if (currentValue !== desiredValue) {
426
+ hasDiff = true;
427
+ console.log(`${colName}.${ruleKey}:`);
428
+ console.log(` live: ${currentValue ?? "(admin)"}`);
429
+ console.log(` desired: ${desiredValue ?? "(admin)"}`);
430
+ }
431
+ }
432
+ }
433
+ return hasDiff;
434
+ }
435
+
436
+ // src/commands/init.ts
437
+ var prompt = inquirer.createPromptModule();
438
+ async function initCommand() {
439
+ console.log(pc.cyan(`
440
+ \uD83D\uDEE0️ pb-admin init
441
+ `));
442
+ console.log(`Setting up PocketBase admin CLI for this project.
443
+ `);
444
+ const { url } = await prompt({
445
+ type: "input",
446
+ name: "url",
447
+ message: "PocketBase URL",
448
+ default: "http://localhost:8090",
449
+ validate: (v) => v.startsWith("http") || "Must be a valid URL"
450
+ });
451
+ const { email } = await prompt({
452
+ type: "input",
453
+ name: "email",
454
+ message: "Admin email"
455
+ });
456
+ const { password } = await prompt({
457
+ type: "password",
458
+ name: "password",
459
+ message: "Admin password",
460
+ mask: "*"
461
+ });
462
+ console.log(pc.dim(`
463
+ Connecting...
464
+ `));
465
+ let pb;
466
+ try {
467
+ pb = await login(url, email, password);
468
+ console.log(pc.green(`✅ Connected to PocketBase
469
+ `));
470
+ } catch (err) {
471
+ console.log(pc.red(`
472
+ ❌ Login failed: ${err.message}
473
+ `));
474
+ process.exit(1);
475
+ }
476
+ const schemaPath = "./pb-admin/schema.json";
477
+ const rulesPath = "./pb-admin/rules.json";
478
+ const config = {
479
+ url,
480
+ schemaPath,
481
+ rulesPath
482
+ };
483
+ saveConfig(config, "./pb-admin.json");
484
+ ensurePBAdminDir();
485
+ console.log(pc.cyan(`\uD83D\uDCE5 Pulling schema and rules...
486
+ `));
487
+ try {
488
+ const schema = await pullSchema(pb);
489
+ saveSchema(schema);
490
+ console.log(pc.green(` ✅ Schema saved (${Object.keys(schema).length} collections)`));
491
+ const rules = await pullRules(pb);
492
+ saveRules(rules);
493
+ console.log(pc.green(` ✅ Rules saved (${Object.keys(rules).length} collections)`));
494
+ } catch (err) {
495
+ console.log(pc.red(`
496
+ ❌ Failed to pull: ${err.message}
497
+ `));
498
+ process.exit(1);
499
+ }
500
+ console.log(pc.green(`
501
+ ✅ Setup complete!
502
+ `));
503
+ console.log(pc.bold("Files created:"));
504
+ console.log(` pb-admin.json - Config`);
505
+ console.log(` pb-admin/schema.json - Collection fields`);
506
+ console.log(` pb-admin/rules.json - Collection permissions
507
+ `);
508
+ console.log(pc.bold("Next steps:"));
509
+ console.log(` ${pc.green("pb-admin diff")} - See differences vs live`);
510
+ console.log(` ${pc.green("pb-admin push")} - Apply changes to PocketBase
511
+ `);
512
+ }
513
+
514
+ // src/commands/pull.ts
515
+ import pc2 from "picocolors";
516
+ async function pullCommand(args) {
517
+ const config = loadConfig();
518
+ const { pb } = await getClient();
519
+ console.log(pc2.cyan(`
520
+ \uD83D\uDCE5 Pulling from PocketBase...
521
+ `));
522
+ ensurePBAdminDir();
523
+ const options = args.includes("--schema") || args.includes("-s") ? { schema: true, rules: false } : args.includes("--rules") || args.includes("-r") ? { schema: false, rules: true } : { schema: true, rules: true };
524
+ if (options.schema) {
525
+ console.log(pc2.dim(" Pulling schema..."));
526
+ const schema = await pullSchema(pb);
527
+ saveSchema(schema);
528
+ const schemaPath = config.schemaPath || "./pb-admin/schema.json";
529
+ console.log(pc2.green(` ✅ Schema saved to ${schemaPath}`));
530
+ console.log(pc2.dim(` ${Object.keys(schema).length} collections
531
+ `));
532
+ }
533
+ if (options.rules) {
534
+ console.log(pc2.dim(" Pulling rules..."));
535
+ const rules = await pullRules(pb);
536
+ saveRules(rules);
537
+ const rulesPath = config.rulesPath || "./pb-admin/rules.json";
538
+ console.log(pc2.green(` ✅ Rules saved to ${rulesPath}`));
539
+ console.log(pc2.dim(` ${Object.keys(rules).length} collections
540
+ `));
541
+ }
542
+ console.log(pc2.green(`Done!
543
+ `));
544
+ }
545
+
546
+ // src/commands/push.ts
547
+ import pc3 from "picocolors";
548
+ async function pushCommand(args) {
549
+ const config = loadConfig();
550
+ const { pb } = await getClient();
551
+ console.log(pc3.cyan(`
552
+ \uD83D\uDCE4 Pushing to PocketBase...
553
+ `));
554
+ const dryRun = args.includes("--dry-run") || args.includes("-n");
555
+ const options = args.includes("--schema") || args.includes("-s") ? { schema: true, rules: false } : args.includes("--rules") || args.includes("-r") ? { schema: false, rules: true } : { schema: true, rules: true };
556
+ if (dryRun) {
557
+ console.log(pc3.yellow(` [DRY RUN - no changes will be made]
558
+ `));
559
+ }
560
+ if (options.schema) {
561
+ const schema = loadSchema();
562
+ if (schema) {
563
+ console.log(pc3.dim(" Pushing schema..."));
564
+ if (!dryRun) {
565
+ await applySchema(pb, schema);
566
+ } else {
567
+ console.log(pc3.dim(" (skipped in dry-run)"));
568
+ }
569
+ } else {
570
+ console.log(pc3.yellow(` ⚠️ No schema file found. Run "pb-admin pull" first.
571
+ `));
572
+ }
573
+ }
574
+ if (options.rules) {
575
+ const rules = loadRules();
576
+ if (rules) {
577
+ console.log(pc3.dim(" Pushing rules..."));
578
+ if (!dryRun) {
579
+ await applyRules(pb, rules);
580
+ } else {
581
+ console.log(pc3.dim(" (skipped in dry-run)"));
582
+ }
583
+ } else {
584
+ console.log(pc3.yellow(` ⚠️ No rules file found. Run "pb-admin pull" first.
585
+ `));
586
+ }
587
+ }
588
+ if (dryRun) {
589
+ console.log(pc3.yellow(`
590
+ ⚠️ This was a dry run. Run without --dry-run to apply changes.
591
+ `));
592
+ } else {
593
+ console.log(pc3.green(`
594
+ Done!
595
+ `));
596
+ }
597
+ }
598
+
599
+ // src/commands/diff.ts
600
+ import pc4 from "picocolors";
601
+ async function diffCommand(args) {
602
+ const config = loadConfig();
603
+ const { pb } = await getClient();
604
+ console.log(pc4.cyan(`
605
+ \uD83D\uDD0D Comparing local files vs live PocketBase...
606
+ `));
607
+ const options = args.includes("--schema") || args.includes("-s") ? { schema: true, rules: false } : args.includes("--rules") || args.includes("-r") ? { schema: false, rules: true } : { schema: true, rules: true };
608
+ let hasDiff = false;
609
+ if (options.schema) {
610
+ const schema = loadSchema();
611
+ if (schema) {
612
+ console.log(pc4.bold(`
613
+ Schema:
614
+ `));
615
+ const schemaHasDiff = await diffSchema(pb, schema);
616
+ if (!schemaHasDiff) {
617
+ console.log(pc4.green(` ✅ Schema matches!
618
+ `));
619
+ }
620
+ hasDiff = hasDiff || schemaHasDiff;
621
+ } else {
622
+ console.log(pc4.yellow(` ⚠️ No schema file found. Run "pb-admin pull" first.
623
+ `));
624
+ }
625
+ }
626
+ if (options.rules) {
627
+ const rules = loadRules();
628
+ if (rules) {
629
+ console.log(pc4.bold(`
630
+ Rules:
631
+ `));
632
+ const rulesHasDiff = await diffRules(pb, rules);
633
+ if (!rulesHasDiff) {
634
+ console.log(pc4.green(` ✅ Rules match!
635
+ `));
636
+ }
637
+ hasDiff = hasDiff || rulesHasDiff;
638
+ } else {
639
+ console.log(pc4.yellow(` ⚠️ No rules file found. Run "pb-admin pull" first.
640
+ `));
641
+ }
642
+ }
643
+ if (!hasDiff) {
644
+ console.log(pc4.green(`
645
+ ✅ Everything matches!
646
+ `));
647
+ } else {
648
+ console.log(pc4.red(`
649
+ ⚠️ Differences found. Run "pb-admin push" to apply changes.
650
+ `));
651
+ process.exit(1);
652
+ }
653
+ }
654
+
655
+ // src/commands/collections.ts
656
+ import pc5 from "picocolors";
657
+ import inquirer2 from "inquirer";
658
+ var prompt2 = inquirer2.createPromptModule();
659
+ async function collectionsCommand(args) {
660
+ const { pb } = await getClient();
661
+ if (args[0] === "get" && args[1]) {
662
+ await showCollection(pb, args[1]);
663
+ return;
664
+ }
665
+ await listCollections(pb);
666
+ }
667
+ async function listCollections(pb) {
668
+ const collections = await pb.collections.getFullList();
669
+ console.log(pc5.cyan(`
670
+ \uD83D\uDCE6 Collections (${collections.length})
671
+ `));
672
+ const rows = collections.filter((c) => !c.name.startsWith("_")).map((c) => ({
673
+ name: c.name,
674
+ type: c.type,
675
+ id: c.id.slice(0, 8)
676
+ }));
677
+ console.table(rows);
678
+ const { name } = await prompt2({
679
+ type: "list",
680
+ name: "name",
681
+ message: "Select collection to view details",
682
+ choices: [
683
+ ...collections.filter((c) => !c.name.startsWith("_")).map((c) => ({
684
+ name: c.name,
685
+ value: c.name
686
+ })),
687
+ { name: "Exit", value: "__exit__" }
688
+ ]
689
+ });
690
+ if (name !== "__exit__") {
691
+ await showCollection(pb, name);
692
+ }
693
+ }
694
+ async function showCollection(pb, name) {
695
+ const col = await getCollection(pb, name);
696
+ if (!col) {
697
+ console.log(pc5.red(`
698
+ ❌ Collection "${name}" not found
699
+ `));
700
+ return;
701
+ }
702
+ console.log(pc5.cyan(`
703
+ \uD83D\uDCCB Collection: ${col.name}
704
+ `));
705
+ console.log(pc5.bold(" ID: ") + col.id);
706
+ console.log(pc5.bold(" Type: ") + col.type);
707
+ console.log(pc5.bold(" Schema:"));
708
+ if (col.fields && col.fields.length > 0) {
709
+ for (const field of col.fields) {
710
+ const req = field.required ? pc5.red("*") : pc5.dim(" ");
711
+ console.log(` ${req} ${pc5.green(field.name)} ${pc5.dim(`(${field.type})`)}`);
712
+ }
713
+ } else {
714
+ console.log(pc5.dim(" (no custom fields)"));
715
+ }
716
+ console.log();
717
+ console.log(pc5.bold(" Rules:"));
718
+ console.log(` list: ${col.listRule ?? pc5.dim("(admin)")}`);
719
+ console.log(` view: ${col.viewRule ?? pc5.dim("(admin)")}`);
720
+ console.log(` create: ${col.createRule ?? pc5.dim("(admin)")}`);
721
+ console.log(` update: ${col.updateRule ?? pc5.dim("(admin)")}`);
722
+ console.log(` delete: ${col.deleteRule ?? pc5.dim("(admin)")}`);
723
+ console.log();
724
+ }
725
+
726
+ // src/index.ts
727
+ import { createRequire } from "module";
728
+ var require2 = createRequire(import.meta.url);
729
+ var pkg = require2("../package.json");
730
+ var prompt3 = inquirer3.createPromptModule();
731
+ var args = process.argv.slice(2);
732
+ var cmd = args[0] || "help";
733
+ async function ensureLogin() {
734
+ if (isLoggedIn()) {
735
+ return;
736
+ }
737
+ console.log(pc6.cyan(`
738
+ \uD83D\uDD10 Authentication required
739
+ `));
740
+ console.log(pc6.dim(`No credentials found for this project.
741
+ `));
742
+ const config = loadConfig();
743
+ const { url } = await prompt3({
744
+ type: "input",
745
+ name: "url",
746
+ message: "PocketBase URL",
747
+ default: config.url || "http://localhost:8090",
748
+ validate: (v) => v.startsWith("http") || "Must be a valid URL"
749
+ });
750
+ const { email } = await prompt3({
751
+ type: "input",
752
+ name: "email",
753
+ message: "Admin email"
754
+ });
755
+ const { password } = await prompt3({
756
+ type: "password",
757
+ name: "password",
758
+ message: "Admin password",
759
+ mask: "*"
760
+ });
761
+ console.log(pc6.dim(`
762
+ Connecting...
763
+ `));
764
+ try {
765
+ await login(url, email, password);
766
+ console.log(pc6.green(`✅ Logged in successfully
767
+ `));
768
+ } catch (err) {
769
+ console.log(pc6.red(`
770
+ ❌ Login failed: ${err.message}
771
+ `));
772
+ process.exit(1);
773
+ }
774
+ }
775
+ async function ensureLoginWithRetry() {
776
+ try {
777
+ await ensureLogin();
778
+ } catch (err) {
779
+ if (err instanceof NotLoggedInError || err instanceof SessionExpiredError) {
780
+ await ensureLogin();
781
+ } else {
782
+ throw err;
783
+ }
784
+ }
785
+ }
786
+ async function main() {
787
+ try {
788
+ switch (cmd) {
789
+ case "init":
790
+ await initCommand();
791
+ break;
792
+ case "login": {
793
+ const config = loadConfig();
794
+ const { email } = await prompt3({
795
+ type: "input",
796
+ name: "email",
797
+ message: "Admin email"
798
+ });
799
+ const { password } = await prompt3({
800
+ type: "password",
801
+ name: "password",
802
+ message: "Admin password",
803
+ mask: "*"
804
+ });
805
+ const url = config.url || "http://localhost:8090";
806
+ console.log(pc6.dim(`
807
+ Connecting...
808
+ `));
809
+ try {
810
+ await login(url, email, password);
811
+ console.log(pc6.green(`✅ Logged in successfully
812
+ `));
813
+ } catch (err) {
814
+ console.log(pc6.red(`
815
+ ❌ Login failed: ${err.message}
816
+ `));
817
+ process.exit(1);
818
+ }
819
+ break;
820
+ }
821
+ case "logout":
822
+ logout();
823
+ console.log(pc6.green(`
824
+ ✅ Logged out
825
+ `));
826
+ break;
827
+ case "pull":
828
+ await ensureLoginWithRetry();
829
+ await pullCommand(args.slice(1));
830
+ break;
831
+ case "push":
832
+ await ensureLoginWithRetry();
833
+ await pushCommand(args.slice(1));
834
+ break;
835
+ case "diff":
836
+ await ensureLoginWithRetry();
837
+ await diffCommand(args.slice(1));
838
+ break;
839
+ case "collections":
840
+ case "col":
841
+ await ensureLoginWithRetry();
842
+ await collectionsCommand(args.slice(1));
843
+ break;
844
+ case "status": {
845
+ if (isLoggedIn()) {
846
+ console.log(pc6.green(`
847
+ ✅ Logged in for this project
848
+ `));
849
+ } else {
850
+ console.log(pc6.yellow(`
851
+ ⚠️ Not logged in for this project
852
+ `));
853
+ }
854
+ break;
855
+ }
856
+ case "help":
857
+ case "--help":
858
+ case "-h":
859
+ showHelp();
860
+ break;
861
+ case "--version":
862
+ case "-v":
863
+ console.log(pc6.cyan(`@pb-admin/cli v${pkg.version}
864
+ `));
865
+ break;
866
+ default:
867
+ console.log(pc6.red(`
868
+ ❌ Unknown command: ${cmd}
869
+ `));
870
+ showHelp();
871
+ process.exit(1);
872
+ }
873
+ } catch (err) {
874
+ console.log(pc6.red(`
875
+ ❌ ${err.message}
876
+ `));
877
+ if (args.includes("--debug")) {
878
+ console.error(err);
879
+ }
880
+ process.exit(1);
881
+ }
882
+ }
883
+ function showHelp() {
884
+ console.log(`
885
+ ${pc6.cyan("\uD83D\uDEE0️ @pb-admin/cli")}
886
+
887
+ ${pc6.bold("Usage:")}
888
+ pb-admin <command> [options]
889
+
890
+ ${pc6.bold("Commands:")}
891
+ ${pc6.green("init")} Initialize project with config file
892
+ ${pc6.green("login")} Login to PocketBase
893
+ ${pc6.green("logout")} Clear credentials for this project
894
+ ${pc6.green("pull")} Pull schema & rules from PocketBase
895
+ ${pc6.green("push")} Push schema & rules to PocketBase
896
+ ${pc6.green("diff")} Compare local vs live
897
+ ${pc6.green("col")} List collections
898
+
899
+ ${pc6.bold("Options:")}
900
+ --schema, -s Only schema operations
901
+ --rules, -r Only rules operations
902
+ --dry-run, -n Show what would change (no apply)
903
+ --debug Show error details
904
+ -h, --help Show this help
905
+ -v, --version Show version
906
+
907
+ ${pc6.bold("Quick Start:")}
908
+ pb-admin init # Initialize (asks for URL + credentials)
909
+ pb-admin pull # Pull schema & rules
910
+ # Edit files in pb-admin/
911
+ pb-admin diff # See changes
912
+ pb-admin push # Apply changes
913
+
914
+ ${pc6.bold("Files:")}
915
+ pb-admin.json - Config (URL, paths)
916
+ pb-admin/schema.json - Collection fields
917
+ pb-admin/rules.json - Collection permissions
918
+
919
+ ${pc6.dim("Credentials:")} ~/.config/pb-admin/projects/{project}/credentials.json
920
+ `);
921
+ }
922
+ main();
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@pb-admin/cli",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "private": false,
6
+ "license": "MIT",
7
+ "description": "CLI to manage PocketBase collections, rules, and schema across multiple instances",
8
+ "bin": {
9
+ "pb-admin": "./dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "scripts": {
15
+ "start": "bun run src/index.ts",
16
+ "build": "bun build ./src/index.ts --outdir ./dist --target node --packages external",
17
+ "test": "bun test",
18
+ "prepublishOnly": "bun run build"
19
+ },
20
+ "devDependencies": {
21
+ "@types/bun": "latest",
22
+ "@types/inquirer": "^9.0.9"
23
+ },
24
+ "peerDependencies": {
25
+ "typescript": "^5"
26
+ },
27
+ "dependencies": {
28
+ "conf": "^15.1.0",
29
+ "inquirer": "^13.4.1",
30
+ "picocolors": "^1.1.1",
31
+ "pocketbase": "^0.26.8",
32
+ "yaml": "^2.8.3"
33
+ }
34
+ }