@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.
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/dist/index.js +922 -0
- 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
|
+
}
|