@globio/cli 0.1.2 → 0.1.4

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/dist/index.js CHANGED
@@ -5,48 +5,155 @@ import { Command } from "commander";
5
5
 
6
6
  // src/auth/login.ts
7
7
  import * as p from "@clack/prompts";
8
+ import { exec } from "child_process";
8
9
  import chalk2 from "chalk";
9
10
 
10
11
  // src/lib/config.ts
11
12
  import chalk from "chalk";
12
- import Conf from "conf";
13
- var store = new Conf({
14
- projectName: "globio",
15
- defaults: {}
16
- });
13
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "fs";
14
+ import os from "os";
15
+ import path from "path";
16
+ var baseDir = path.join(os.homedir(), ".globio");
17
+ var profilesDir = path.join(baseDir, "profiles");
18
+ var configPath = path.join(baseDir, "config.json");
19
+ function ensureBaseDir() {
20
+ mkdirSync(baseDir, { recursive: true });
21
+ }
22
+ function ensureProfilesDir() {
23
+ ensureBaseDir();
24
+ mkdirSync(profilesDir, { recursive: true });
25
+ }
26
+ function readGlobalConfig() {
27
+ if (!existsSync(configPath)) {
28
+ return { active_profile: "default" };
29
+ }
30
+ try {
31
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
32
+ return {
33
+ active_profile: raw.active_profile ?? "default"
34
+ };
35
+ } catch {
36
+ return { active_profile: "default" };
37
+ }
38
+ }
39
+ function writeGlobalConfig(data) {
40
+ ensureBaseDir();
41
+ writeFileSync(configPath, JSON.stringify(data, null, 2) + "\n");
42
+ }
43
+ function profilePath(name) {
44
+ return path.join(profilesDir, `${name}.json`);
45
+ }
46
+ function readProfile(name) {
47
+ const file = profilePath(name);
48
+ if (!existsSync(file)) return null;
49
+ try {
50
+ return JSON.parse(readFileSync(file, "utf-8"));
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ function writeProfile(name, data) {
56
+ ensureProfilesDir();
57
+ writeFileSync(profilePath(name), JSON.stringify(data, null, 2) + "\n");
58
+ }
17
59
  var config = {
18
- get: () => store.store,
19
- set: (values) => {
20
- Object.entries(values).forEach(([key, value]) => {
21
- if (value !== void 0) {
22
- store.set(key, value);
23
- }
24
- });
60
+ getBaseDir: () => baseDir,
61
+ getProfilesDir: () => profilesDir,
62
+ getActiveProfile: () => readGlobalConfig().active_profile,
63
+ setActiveProfile: (name) => {
64
+ writeGlobalConfig({ active_profile: name });
65
+ },
66
+ getProfile: (name) => {
67
+ const profileName = name ?? config.getActiveProfile();
68
+ if (!profileName) return null;
69
+ return readProfile(profileName);
70
+ },
71
+ setProfile: (name, data) => {
72
+ const existing = readProfile(name);
73
+ const next = {
74
+ pat: data.pat ?? existing?.pat ?? "",
75
+ account_email: data.account_email ?? existing?.account_email ?? "",
76
+ account_name: data.account_name ?? existing?.account_name ?? "",
77
+ active_project_id: data.active_project_id ?? existing?.active_project_id,
78
+ active_project_name: data.active_project_name ?? existing?.active_project_name,
79
+ project_api_key: data.project_api_key ?? existing?.project_api_key,
80
+ created_at: data.created_at ?? existing?.created_at ?? Date.now()
81
+ };
82
+ writeProfile(name, next);
83
+ },
84
+ deleteProfile: (name) => {
85
+ const file = profilePath(name);
86
+ if (existsSync(file)) {
87
+ rmSync(file);
88
+ }
89
+ },
90
+ listProfiles: () => {
91
+ if (!existsSync(profilesDir)) return [];
92
+ return readdirSync(profilesDir).filter((file) => file.endsWith(".json")).map((file) => file.replace(/\.json$/, "")).sort();
25
93
  },
26
- clear: () => store.clear(),
27
- getApiKey: () => store.get("apiKey"),
28
- requireAuth: () => {
29
- const key = store.get("apiKey");
30
- if (!key) {
94
+ getActiveProfileData: () => {
95
+ const active = config.getActiveProfile();
96
+ if (!active) return null;
97
+ return config.getProfile(active);
98
+ },
99
+ requireAuth: (profileName) => {
100
+ const resolvedProfile = profileName ?? config.getActiveProfile() ?? "default";
101
+ const profile = config.getProfile(resolvedProfile);
102
+ if (!profile?.pat) {
31
103
  console.error(chalk.red("Not logged in. Run: npx @globio/cli login"));
32
104
  process.exit(1);
33
105
  }
34
- return key;
106
+ return { pat: profile.pat, profileName: resolvedProfile };
35
107
  },
36
- requireProject: () => {
37
- const projectId = store.get("projectId");
38
- if (!projectId) {
108
+ requireProject: (profileName) => {
109
+ const resolvedProfile = profileName ?? config.getActiveProfile() ?? "default";
110
+ const profile = config.getProfile(resolvedProfile);
111
+ if (!profile?.active_project_id) {
39
112
  console.error(
40
113
  chalk.red("No active project. Run: npx @globio/cli projects use <projectId>")
41
114
  );
42
115
  process.exit(1);
43
116
  }
44
- return projectId;
117
+ return {
118
+ projectId: profile.active_project_id,
119
+ projectName: profile.active_project_name ?? "unnamed"
120
+ };
45
121
  }
46
122
  };
47
123
 
124
+ // src/lib/manage.ts
125
+ var API_BASE_URL = "https://api.globio.stanlink.online";
126
+ var CONSOLE_BASE_URL = "https://console.globio.stanlink.online";
127
+ function getAuthToken(explicitToken, profileName) {
128
+ if (explicitToken) return explicitToken;
129
+ return config.getProfile(profileName)?.pat;
130
+ }
131
+ async function manageRequest(path2, options = {}) {
132
+ const headers = new Headers();
133
+ if (!(options.body instanceof FormData)) {
134
+ headers.set("Content-Type", "application/json");
135
+ }
136
+ const token = getAuthToken(options.token, options.profileName);
137
+ if (token) {
138
+ headers.set("Authorization", `Bearer ${token}`);
139
+ }
140
+ const response = await fetch(`${API_BASE_URL}/manage${path2}`, {
141
+ method: options.method ?? "GET",
142
+ headers,
143
+ body: options.body ? JSON.stringify(options.body) : void 0
144
+ });
145
+ const payload = await response.json().catch(() => ({}));
146
+ if (!response.ok) {
147
+ throw new Error(payload.error || payload.message || "Management request failed");
148
+ }
149
+ return payload.data ?? payload;
150
+ }
151
+ function getConsoleCliAuthUrl(state) {
152
+ return `${CONSOLE_BASE_URL}/cli-auth?state=${encodeURIComponent(state)}`;
153
+ }
154
+
48
155
  // src/lib/banner.ts
49
- import { readFileSync } from "fs";
156
+ import { readFileSync as readFileSync2 } from "fs";
50
157
  import figlet from "figlet";
51
158
  import gradientString from "gradient-string";
52
159
  var globioGradient = gradientString(
@@ -74,116 +181,245 @@ var orange = (s) => "\x1B[38;2;244;140;6m" + s + "\x1B[0m";
74
181
  var gold = (s) => "\x1B[38;2;255;208;0m" + s + "\x1B[0m";
75
182
  var muted = (s) => "\x1B[2m" + s + "\x1B[0m";
76
183
  function getCliVersion() {
77
- const file = readFileSync(new URL("../package.json", import.meta.url), "utf8");
184
+ const file = readFileSync2(new URL("../package.json", import.meta.url), "utf8");
78
185
  return JSON.parse(file).version;
79
186
  }
80
187
 
81
188
  // src/auth/login.ts
82
- var DEFAULT_BASE_URL = "https://api.globio.stanlink.online";
83
189
  var version = getCliVersion();
84
- async function login() {
85
- printBanner(version);
86
- const values = await p.group(
87
- {
88
- apiKey: () => p.text({
89
- message: "Paste your Globio API key",
90
- placeholder: "gk_live_...",
91
- validate: (value) => !value ? "API key is required" : void 0
92
- }),
93
- projectId: () => p.text({
94
- message: "Paste your Project ID",
95
- placeholder: "proj_...",
96
- validate: (value) => !value ? "Project ID is required" : void 0
97
- })
98
- },
99
- {
100
- onCancel: () => {
101
- p.cancel("Login cancelled.");
102
- process.exit(0);
103
- }
190
+ function openBrowser(url) {
191
+ const command = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
192
+ exec(command);
193
+ }
194
+ function sleep(ms) {
195
+ return new Promise((resolve) => setTimeout(resolve, ms));
196
+ }
197
+ async function savePat(token) {
198
+ const account = await manageRequest("/account", { token });
199
+ return account;
200
+ }
201
+ async function runTokenLogin(profileName) {
202
+ const hadProfiles = config.listProfiles().length > 0;
203
+ const token = await p.text({
204
+ message: "Paste your personal access token",
205
+ placeholder: "glo_pat_...",
206
+ validate: (value) => {
207
+ if (!value) return "Personal access token is required";
208
+ if (!value.startsWith("glo_pat_")) return "Token must start with glo_pat_";
209
+ return void 0;
104
210
  }
105
- );
211
+ });
212
+ if (p.isCancel(token)) {
213
+ p.cancel("Login cancelled.");
214
+ process.exit(0);
215
+ }
106
216
  const spinner2 = p.spinner();
107
- spinner2.start("Validating credentials...");
217
+ spinner2.start("Validating personal access token...");
108
218
  try {
109
- const response = await fetch(`${DEFAULT_BASE_URL}/id/health`, {
110
- headers: {
111
- "X-Globio-Key": values.apiKey
112
- }
219
+ const account = await savePat(token);
220
+ config.setProfile(profileName, {
221
+ pat: token,
222
+ account_email: account.email,
223
+ account_name: account.display_name ?? account.email,
224
+ created_at: Date.now()
113
225
  });
114
- if (!response.ok) {
115
- spinner2.stop("Validation failed.");
116
- p.outro(chalk2.red("Invalid API key or project ID."));
117
- process.exit(1);
226
+ if (profileName === "default" || !hadProfiles) {
227
+ config.setActiveProfile(profileName);
228
+ }
229
+ spinner2.stop("Token validated.");
230
+ p.outro(`Logged in as ${account.email}
231
+ Profile: ${profileName}`);
232
+ } catch (error) {
233
+ spinner2.stop("Validation failed.");
234
+ p.outro(chalk2.red(error instanceof Error ? error.message : "Could not validate token"));
235
+ process.exit(1);
236
+ }
237
+ }
238
+ async function runBrowserLogin(profileName) {
239
+ const state = crypto.randomUUID();
240
+ const spinner2 = p.spinner();
241
+ const hadProfiles = config.listProfiles().length > 0;
242
+ await manageRequest("/cli-auth/request", {
243
+ method: "POST",
244
+ body: { state }
245
+ });
246
+ const url = getConsoleCliAuthUrl(state);
247
+ openBrowser(url);
248
+ console.log(" " + muted("Browser URL: ") + orange(url));
249
+ console.log("");
250
+ spinner2.start("Waiting for browser approval...");
251
+ const deadline = Date.now() + 5 * 60 * 1e3;
252
+ while (Date.now() < deadline) {
253
+ try {
254
+ const status = await manageRequest(
255
+ `/cli-auth/poll?state=${encodeURIComponent(state)}`
256
+ );
257
+ if (status.status === "expired") {
258
+ spinner2.stop("Approval window expired.");
259
+ p.outro(chalk2.red("CLI auth request expired. Try again or use globio login --token."));
260
+ process.exit(1);
261
+ }
262
+ if (status.status === "approved" && status.code) {
263
+ const exchange = await manageRequest("/cli-auth/exchange", {
264
+ method: "POST",
265
+ body: { code: status.code }
266
+ });
267
+ config.setProfile(profileName, {
268
+ pat: exchange.token,
269
+ account_email: exchange.account.email,
270
+ account_name: exchange.account.display_name ?? exchange.account.email,
271
+ created_at: Date.now()
272
+ });
273
+ if (profileName === "default" || !hadProfiles) {
274
+ config.setActiveProfile(profileName);
275
+ }
276
+ spinner2.stop("Browser approval received.");
277
+ p.outro(`Logged in as ${exchange.account.email}
278
+ Profile: ${profileName}`);
279
+ return;
280
+ }
281
+ } catch {
118
282
  }
119
- config.set({
120
- apiKey: values.apiKey,
121
- projectId: values.projectId
283
+ await sleep(2e3);
284
+ }
285
+ spinner2.stop("Approval timed out.");
286
+ p.outro(chalk2.red("Timed out waiting for browser approval. Try again or use globio login --token."));
287
+ process.exit(1);
288
+ }
289
+ async function login(options = {}) {
290
+ printBanner(version);
291
+ const profileName = options.profile ?? "default";
292
+ const existing = config.getProfile(profileName);
293
+ if (existing) {
294
+ const proceed = await p.confirm({
295
+ message: `Already logged in as ${existing.account_email} on profile "${profileName}". Replace?`,
296
+ initialValue: false
122
297
  });
123
- spinner2.stop("Credentials validated.");
124
- p.outro(
125
- " Logged in.\n\n " + muted("API Key: ") + orange(values.apiKey) + "\n " + muted("Project: ") + orange(values.projectId)
126
- );
127
- } catch {
128
- spinner2.stop("");
129
- p.outro(chalk2.red("Could not connect to Globio. Check your credentials."));
298
+ if (p.isCancel(proceed) || !proceed) {
299
+ p.outro("Login cancelled.");
300
+ return;
301
+ }
302
+ }
303
+ if (options.token) {
304
+ await runTokenLogin(profileName);
305
+ return;
306
+ }
307
+ const choice = await p.select({
308
+ message: "Choose a login method",
309
+ options: [
310
+ { value: "browser", label: "Browser", hint: "Open console and approve access" },
311
+ { value: "token", label: "Token", hint: "Paste a personal access token" }
312
+ ]
313
+ });
314
+ if (p.isCancel(choice)) {
315
+ p.cancel("Login cancelled.");
316
+ process.exit(0);
317
+ }
318
+ if (choice === "token") {
319
+ await runTokenLogin(profileName);
320
+ return;
321
+ }
322
+ try {
323
+ await runBrowserLogin(profileName);
324
+ } catch (error) {
325
+ p.outro(chalk2.red(error instanceof Error ? error.message : "Could not connect to Globio."));
130
326
  process.exit(1);
131
327
  }
132
328
  }
133
329
 
134
330
  // src/auth/logout.ts
135
- import * as p2 from "@clack/prompts";
136
331
  import chalk3 from "chalk";
137
- async function logout() {
138
- config.clear();
139
- p2.outro(chalk3.green("Logged out."));
332
+ async function logout(options = {}) {
333
+ const activeProfile = config.getActiveProfile();
334
+ const profileName = options.profile ?? activeProfile;
335
+ const profile = profileName ? config.getProfile(profileName) : null;
336
+ if (!profileName || !profile) {
337
+ console.log(chalk3.yellow(`No active session on profile "${profileName || "default"}".`));
338
+ return;
339
+ }
340
+ config.deleteProfile(profileName);
341
+ if (profileName === activeProfile) {
342
+ const remaining = config.listProfiles();
343
+ if (remaining.length > 0) {
344
+ config.setActiveProfile(remaining[0]);
345
+ console.log(chalk3.green(`Logged out. Switched to profile: ${remaining[0]}`));
346
+ return;
347
+ }
348
+ config.setActiveProfile("");
349
+ console.log(chalk3.green("Logged out."));
350
+ return;
351
+ }
352
+ console.log(chalk3.green(`Logged out profile: ${profileName}`));
140
353
  }
141
354
 
142
- // src/auth/whoami.ts
355
+ // src/auth/useProfile.ts
143
356
  import chalk4 from "chalk";
144
- async function whoami() {
145
- const cfg = config.get();
146
- if (!cfg.apiKey) {
147
- console.log(chalk4.red("Not logged in."));
357
+ async function useProfile(profileName) {
358
+ const profile = config.getProfile(profileName);
359
+ if (!profile) {
360
+ console.log(
361
+ chalk4.red(
362
+ `Profile "${profileName}" not found. Run: globio login --profile ${profileName}`
363
+ )
364
+ );
365
+ process.exit(1);
366
+ }
367
+ config.setActiveProfile(profileName);
368
+ console.log(
369
+ chalk4.green("Switched to profile: ") + orange(profileName) + ` (${profile.account_email})`
370
+ );
371
+ }
372
+
373
+ // src/auth/whoami.ts
374
+ import chalk5 from "chalk";
375
+ async function whoami(options = {}) {
376
+ const profileName = options.profile ?? config.getActiveProfile() ?? "default";
377
+ const profile = config.getProfile(profileName);
378
+ if (!profile) {
379
+ console.log(chalk5.red("Not logged in. Run: globio login"));
148
380
  return;
149
381
  }
382
+ const allProfiles = config.listProfiles();
383
+ const activeProfile = config.getActiveProfile();
150
384
  console.log("");
151
- console.log(chalk4.cyan("API Key: ") + cfg.apiKey);
152
- console.log(chalk4.cyan("Project: ") + (cfg.projectId ?? "none"));
385
+ console.log(
386
+ muted("Profile: ") + orange(profileName) + (profileName === activeProfile ? muted(" (active)") : "")
387
+ );
388
+ console.log(muted("Account: ") + profile.account_email);
389
+ console.log(muted("Name: ") + (profile.account_name || "\u2014"));
390
+ console.log(
391
+ muted("Project: ") + (profile.active_project_id ? orange(profile.active_project_name || "unnamed") + muted(` (${profile.active_project_id})`) : chalk5.gray("none \u2014 run: globio projects use <id>"))
392
+ );
393
+ if (allProfiles.length > 1) {
394
+ console.log("");
395
+ console.log(
396
+ muted("Other profiles: ") + allProfiles.filter((name) => name !== profileName).join(", ")
397
+ );
398
+ }
153
399
  console.log("");
154
400
  }
155
401
 
156
402
  // src/commands/init.ts
157
403
  import * as p5 from "@clack/prompts";
158
- import { existsSync, readFileSync as readFileSync2, writeFileSync } from "fs";
404
+ import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
159
405
 
160
406
  // src/prompts/init.ts
161
- import * as p3 from "@clack/prompts";
407
+ import * as p2 from "@clack/prompts";
162
408
  async function promptInit() {
163
- return p3.group(
409
+ return p2.group(
164
410
  {
165
- apiKey: () => p3.text({
166
- message: "Globio API key",
167
- placeholder: "gk_live_...",
168
- validate: (value) => !value ? "Required" : void 0
169
- }),
170
- projectId: () => p3.text({
171
- message: "Project ID",
172
- placeholder: "proj_...",
173
- validate: (value) => !value ? "Required" : void 0
174
- }),
175
- migrateFromFirebase: () => p3.confirm({
411
+ migrateFromFirebase: () => p2.confirm({
176
412
  message: "Migrating from Firebase?",
177
413
  initialValue: false
178
414
  }),
179
- serviceAccountPath: ({ results }) => results.migrateFromFirebase ? p3.text({
415
+ serviceAccountPath: ({ results }) => results.migrateFromFirebase ? p2.text({
180
416
  message: "Path to Firebase service account JSON",
181
417
  placeholder: "./serviceAccountKey.json"
182
418
  }) : Promise.resolve(void 0)
183
419
  },
184
420
  {
185
421
  onCancel: () => {
186
- p3.cancel("Cancelled.");
422
+ p2.cancel("Cancelled.");
187
423
  process.exit(0);
188
424
  }
189
425
  }
@@ -191,15 +427,15 @@ async function promptInit() {
191
427
  }
192
428
 
193
429
  // src/commands/migrate.ts
194
- import * as p4 from "@clack/prompts";
195
- import chalk6 from "chalk";
430
+ import * as p3 from "@clack/prompts";
431
+ import chalk7 from "chalk";
196
432
  import { basename } from "path";
197
433
 
198
434
  // src/lib/firebase.ts
199
435
  async function initFirebase(serviceAccountPath) {
200
436
  const admin = await import("firebase-admin");
201
- const { readFileSync: readFileSync4 } = await import("fs");
202
- const serviceAccount = JSON.parse(readFileSync4(serviceAccountPath, "utf-8"));
437
+ const { readFileSync: readFileSync5 } = await import("fs");
438
+ const serviceAccount = JSON.parse(readFileSync5(serviceAccountPath, "utf-8"));
203
439
  if (!admin.default.apps.length) {
204
440
  admin.default.initializeApp({
205
441
  credential: admin.default.credential.cert(serviceAccount),
@@ -214,12 +450,12 @@ async function initFirebase(serviceAccountPath) {
214
450
  }
215
451
 
216
452
  // src/lib/progress.ts
217
- import chalk5 from "chalk";
453
+ import chalk6 from "chalk";
218
454
  import cliProgress from "cli-progress";
219
455
  function createProgressBar(label) {
220
456
  const bar = new cliProgress.SingleBar(
221
457
  {
222
- format: chalk5.cyan(label) + " [{bar}] {percentage}% | {value}/{total}",
458
+ format: chalk6.cyan(label) + " [{bar}] {percentage}% | {value}/{total}",
223
459
  barCompleteChar: "\u2588",
224
460
  barIncompleteChar: "\u2591",
225
461
  hideCursor: true
@@ -231,32 +467,38 @@ function createProgressBar(label) {
231
467
 
232
468
  // src/lib/sdk.ts
233
469
  import { Globio } from "@globio/sdk";
234
- function getClient() {
235
- const apiKey = config.requireAuth();
236
- config.requireProject();
470
+ function getClient(profileName) {
471
+ const { pat } = config.requireAuth(profileName);
472
+ const { projectId } = config.requireProject(profileName);
473
+ const profile = config.getProfile(profileName);
474
+ const apiKey = profile?.project_api_key ?? pat;
475
+ void projectId;
237
476
  return new Globio({ apiKey });
238
477
  }
239
478
 
240
479
  // src/commands/migrate.ts
241
480
  var version2 = getCliVersion();
481
+ function resolveProfileName(profile) {
482
+ return profile ?? config.getActiveProfile() ?? "default";
483
+ }
242
484
  async function migrateFirestore(options) {
243
485
  printBanner(version2);
244
- p4.intro(gold("\u21D2\u21D2") + " Firebase \u2192 Globio Migration");
486
+ p3.intro(gold("\u21D2\u21D2") + " Firebase \u2192 Globio Migration");
245
487
  const { firestore } = await initFirebase(options.from);
246
- const client = getClient();
488
+ const client = getClient(resolveProfileName(options.profile));
247
489
  let collections = [];
248
490
  if (options.all) {
249
491
  const snapshot = await firestore.listCollections();
250
492
  collections = snapshot.map((collection) => collection.id);
251
493
  console.log(
252
- chalk6.cyan(
494
+ chalk7.cyan(
253
495
  `Found ${collections.length} collections: ${collections.join(", ")}`
254
496
  )
255
497
  );
256
498
  } else if (options.collection) {
257
499
  collections = [options.collection];
258
500
  } else {
259
- console.log(chalk6.red("Specify --collection <name> or --all"));
501
+ console.log(chalk7.red("Specify --collection <name> or --all"));
260
502
  process.exit(1);
261
503
  }
262
504
  const results = {};
@@ -301,32 +543,32 @@ async function migrateFirestore(options) {
301
543
  }
302
544
  bar.stop();
303
545
  console.log(
304
- chalk6.green(` \u2713 ${results[collectionId].success} documents migrated`)
546
+ chalk7.green(` \u2713 ${results[collectionId].success} documents migrated`)
305
547
  );
306
548
  if (results[collectionId].failed > 0) {
307
- console.log(chalk6.red(` \u2717 ${results[collectionId].failed} failed`));
549
+ console.log(chalk7.red(` \u2717 ${results[collectionId].failed} failed`));
308
550
  console.log(
309
- chalk6.gray(
551
+ chalk7.gray(
310
552
  " Failed IDs: " + results[collectionId].failedIds.slice(0, 10).join(", ") + (results[collectionId].failedIds.length > 10 ? "..." : "")
311
553
  )
312
554
  );
313
555
  }
314
556
  }
315
557
  console.log("");
316
- p4.outro(
558
+ p3.outro(
317
559
  orange("\u2713") + " Migration complete.\n\n " + muted("Your Firebase data is intact.") + "\n " + muted("Delete it manually when ready.")
318
560
  );
319
561
  }
320
562
  async function migrateFirebaseStorage(options) {
321
563
  printBanner(version2);
322
- p4.intro(gold("\u21D2\u21D2") + " Firebase \u2192 Globio Migration");
564
+ p3.intro(gold("\u21D2\u21D2") + " Firebase \u2192 Globio Migration");
323
565
  const { storage } = await initFirebase(options.from);
324
- const client = getClient();
566
+ const client = getClient(resolveProfileName(options.profile));
325
567
  const bucketName = options.bucket.replace(/^gs:\/\//, "");
326
568
  const bucket = storage.bucket(bucketName);
327
569
  const prefix = options.folder ? options.folder.replace(/^\//, "") : "";
328
570
  const [files] = await bucket.getFiles(prefix ? { prefix } : {});
329
- console.log(chalk6.cyan(`Found ${files.length} files to migrate`));
571
+ console.log(chalk7.cyan(`Found ${files.length} files to migrate`));
330
572
  const bar = createProgressBar("Storage");
331
573
  bar.start(files.length, 0);
332
574
  let success = 0;
@@ -354,39 +596,195 @@ async function migrateFirebaseStorage(options) {
354
596
  }
355
597
  bar.stop();
356
598
  console.log("");
357
- console.log(chalk6.green(` \u2713 ${success} files migrated`));
599
+ console.log(chalk7.green(` \u2713 ${success} files migrated`));
358
600
  if (failed > 0) {
359
- console.log(chalk6.red(` \u2717 ${failed} failed`));
601
+ console.log(chalk7.red(` \u2717 ${failed} failed`));
360
602
  }
361
- p4.outro(
603
+ p3.outro(
362
604
  orange("\u2713") + " Migration complete.\n\n " + muted("Your Firebase data is intact.") + "\n " + muted("Delete it manually when ready.")
363
605
  );
364
606
  }
365
607
 
608
+ // src/commands/projects.ts
609
+ import * as p4 from "@clack/prompts";
610
+ import chalk8 from "chalk";
611
+ function slugify(value) {
612
+ return value.toLowerCase().trim().replace(/[^a-z0-9\\s-]/g, "").replace(/\\s+/g, "-").replace(/-+/g, "-");
613
+ }
614
+ function resolveProfileName2(profileName) {
615
+ return profileName ?? config.getActiveProfile() ?? "default";
616
+ }
617
+ async function createServerKey(projectId, profileName) {
618
+ const created = await manageRequest(`/projects/${projectId}/keys`, {
619
+ method: "POST",
620
+ body: {
621
+ name: "CLI server key",
622
+ scope: "server"
623
+ },
624
+ profileName
625
+ });
626
+ if (!created.token) {
627
+ throw new Error("Management API did not return a project API key");
628
+ }
629
+ return created.token;
630
+ }
631
+ async function projectsList(options = {}) {
632
+ const profileName = resolveProfileName2(options.profile);
633
+ config.requireAuth(profileName);
634
+ const projects2 = await manageRequest("/projects", { profileName });
635
+ const activeProjectId = config.getProfile(profileName)?.active_project_id;
636
+ const grouped = /* @__PURE__ */ new Map();
637
+ for (const project of projects2) {
638
+ const list = grouped.get(project.org_name) ?? [];
639
+ list.push(project);
640
+ grouped.set(project.org_name, list);
641
+ }
642
+ console.log("");
643
+ if (!projects2.length) {
644
+ console.log(chalk8.gray("No projects found."));
645
+ console.log("");
646
+ return;
647
+ }
648
+ for (const [orgName, orgProjects] of grouped.entries()) {
649
+ console.log(chalk8.cyan(`org: ${orgName}`));
650
+ for (const project of orgProjects) {
651
+ const marker = project.id === activeProjectId ? chalk8.green("\u25CF") : chalk8.gray("\u25CB");
652
+ const active = project.id === activeProjectId ? chalk8.green(" (active)") : "";
653
+ console.log(` ${marker} ${project.slug.padEnd(22)} ${chalk8.gray(project.id)}${active}`);
654
+ }
655
+ console.log("");
656
+ }
657
+ }
658
+ async function projectsUse(projectId, options = {}) {
659
+ const profileName = resolveProfileName2(options.profile);
660
+ config.requireAuth(profileName);
661
+ const projects2 = await manageRequest("/projects", { profileName });
662
+ const project = projects2.find((item) => item.id === projectId);
663
+ if (!project) {
664
+ console.log(chalk8.red(`Project not found: ${projectId}`));
665
+ process.exit(1);
666
+ }
667
+ await manageRequest(`/projects/${projectId}/keys`, { profileName });
668
+ const apiKey = await createServerKey(projectId, profileName);
669
+ config.setProfile(profileName, {
670
+ active_project_id: project.id,
671
+ active_project_name: project.name,
672
+ project_api_key: apiKey
673
+ });
674
+ config.setActiveProfile(profileName);
675
+ console.log(
676
+ chalk8.green("Active project set to: ") + chalk8.cyan(`${project.name} (${project.id})`)
677
+ );
678
+ }
679
+ async function projectsCreate(options = {}) {
680
+ const profileName = resolveProfileName2(options.profile);
681
+ config.requireAuth(profileName);
682
+ const orgs = await manageRequest("/orgs", { profileName });
683
+ if (!orgs.length) {
684
+ console.log(chalk8.red("No organizations found. Create one in the console first."));
685
+ process.exit(1);
686
+ }
687
+ const orgId = await p4.select({
688
+ message: "Select an organization",
689
+ options: orgs.map((org) => ({
690
+ value: org.id,
691
+ label: org.name,
692
+ hint: org.role
693
+ }))
694
+ });
695
+ if (p4.isCancel(orgId)) {
696
+ p4.cancel("Project creation cancelled.");
697
+ process.exit(0);
698
+ }
699
+ const values = await p4.group(
700
+ {
701
+ name: () => p4.text({
702
+ message: "Project name",
703
+ validate: (value) => !value ? "Project name is required" : void 0
704
+ }),
705
+ slug: ({ results }) => p4.text({
706
+ message: "Project slug",
707
+ initialValue: slugify(String(results.name ?? "")),
708
+ validate: (value) => !value ? "Project slug is required" : void 0
709
+ }),
710
+ environment: () => p4.select({
711
+ message: "Environment",
712
+ options: [
713
+ { value: "development", label: "development" },
714
+ { value: "staging", label: "staging" },
715
+ { value: "production", label: "production" }
716
+ ]
717
+ })
718
+ },
719
+ {
720
+ onCancel: () => {
721
+ p4.cancel("Project creation cancelled.");
722
+ process.exit(0);
723
+ }
724
+ }
725
+ );
726
+ const result = await manageRequest("/projects", {
727
+ method: "POST",
728
+ body: {
729
+ org_id: orgId,
730
+ name: values.name,
731
+ slug: values.slug,
732
+ environment: values.environment
733
+ },
734
+ profileName
735
+ });
736
+ config.setProfile(profileName, {
737
+ active_project_id: result.project.id,
738
+ active_project_name: result.project.name,
739
+ project_api_key: result.keys.server
740
+ });
741
+ config.setActiveProfile(profileName);
742
+ console.log("");
743
+ console.log(chalk8.green("Project created successfully."));
744
+ console.log(chalk8.cyan("Project: ") + `${result.project.name} (${result.project.id})`);
745
+ console.log(chalk8.cyan("Client key: ") + result.keys.client);
746
+ console.log(chalk8.cyan("Server key: ") + result.keys.server);
747
+ console.log("");
748
+ }
749
+
366
750
  // src/commands/init.ts
367
751
  var version3 = getCliVersion();
368
- async function init() {
752
+ async function init(options = {}) {
369
753
  printBanner(version3);
370
754
  p5.intro(orange("\u21D2\u21D2") + " Initialize your Globio project");
755
+ const profileName = options.profile ?? config.getActiveProfile() ?? "default";
756
+ const profile = config.getProfile(profileName);
757
+ if (!profile) {
758
+ console.log("Run: npx @globio/cli login --profile " + profileName);
759
+ process.exit(1);
760
+ }
761
+ if (!profile.active_project_id) {
762
+ await projectsCreate({ profile: profileName });
763
+ } else {
764
+ await projectsUse(profile.active_project_id, { profile: profileName });
765
+ }
371
766
  const values = await promptInit();
372
- config.set({
373
- apiKey: values.apiKey,
374
- projectId: values.projectId
375
- });
376
- if (!existsSync("globio.config.ts")) {
377
- writeFileSync(
767
+ const activeProfile = config.getProfile(profileName);
768
+ const activeProjectKey = activeProfile?.project_api_key;
769
+ const { projectId: activeProjectId } = config.requireProject(profileName);
770
+ if (!activeProjectKey) {
771
+ console.log("No project API key cached. Run: npx @globio/cli projects use " + activeProjectId);
772
+ process.exit(1);
773
+ }
774
+ if (!existsSync2("globio.config.ts")) {
775
+ writeFileSync2(
378
776
  "globio.config.ts",
379
- `import { GlobioClient } from '@globio/sdk';
777
+ `import { Globio } from '@globio/sdk';
380
778
 
381
- export const globio = new GlobioClient({
779
+ export const globio = new Globio({
382
780
  apiKey: process.env.GLOBIO_API_KEY!,
383
781
  });
384
782
  `
385
783
  );
386
784
  printSuccess("Created globio.config.ts");
387
785
  }
388
- if (!existsSync(".env")) {
389
- writeFileSync(".env", `GLOBIO_API_KEY=${values.apiKey}
786
+ if (!existsSync2(".env")) {
787
+ writeFileSync2(".env", `GLOBIO_API_KEY=${activeProjectKey}
390
788
  `);
391
789
  printSuccess("Created .env");
392
790
  }
@@ -395,40 +793,31 @@ export const globio = new GlobioClient({
395
793
  printSuccess("Starting Firebase migration...");
396
794
  await migrateFirestore({
397
795
  from: values.serviceAccountPath,
398
- all: true
796
+ all: true,
797
+ profile: profileName
399
798
  });
400
799
  const serviceAccount = JSON.parse(
401
- readFileSync2(values.serviceAccountPath, "utf-8")
800
+ readFileSync3(values.serviceAccountPath, "utf-8")
402
801
  );
403
802
  await migrateFirebaseStorage({
404
803
  from: values.serviceAccountPath,
405
804
  bucket: `${serviceAccount.project_id}.appspot.com`,
406
- all: true
805
+ all: true,
806
+ profile: profileName
407
807
  });
408
808
  }
409
809
  console.log("");
410
810
  p5.outro(
411
- orange("\u21D2\u21D2") + " Your project is ready.\n\n " + muted("Next steps:") + "\n\n npm install @globio/sdk\n npx @globio/cli functions create my-first-function"
412
- );
413
- }
811
+ orange("\u21D2\u21D2") + " Your project is ready.\n\n " + muted("Next steps:") + `
414
812
 
415
- // src/commands/projects.ts
416
- import chalk7 from "chalk";
417
- async function projectsList() {
418
- const cfg = config.get();
419
- console.log("");
420
- console.log(
421
- chalk7.cyan("Active project: ") + (cfg.projectId ?? chalk7.gray("none"))
813
+ npm install @globio/sdk
814
+ # active project: ${activeProjectId}
815
+ npx @globio/cli functions create my-first-function`
422
816
  );
423
- console.log("");
424
- }
425
- async function projectsUse(projectId) {
426
- config.set({ projectId });
427
- console.log(chalk7.green("Active project set to: ") + chalk7.cyan(projectId));
428
817
  }
429
818
 
430
819
  // src/commands/services.ts
431
- import chalk8 from "chalk";
820
+ import chalk9 from "chalk";
432
821
  var ALL_SERVICES = [
433
822
  "id",
434
823
  "doc",
@@ -441,30 +830,36 @@ var ALL_SERVICES = [
441
830
  "brain",
442
831
  "code"
443
832
  ];
444
- async function servicesList() {
833
+ async function servicesList(options = {}) {
834
+ void options.profile;
835
+ void config;
445
836
  console.log("");
446
- console.log(chalk8.cyan("Available Globio services:"));
837
+ console.log(chalk9.cyan("Available Globio services:"));
447
838
  ALL_SERVICES.forEach((service) => {
448
- console.log(" " + chalk8.white(service));
839
+ console.log(" " + chalk9.white(service));
449
840
  });
450
841
  console.log("");
451
842
  console.log(
452
- chalk8.gray("Manage service access via console.globio.stanlink.online")
843
+ chalk9.gray("Manage service access via console.globio.stanlink.online")
453
844
  );
454
845
  console.log("");
455
846
  }
456
847
 
457
848
  // src/commands/functions.ts
458
- import chalk9 from "chalk";
849
+ import chalk10 from "chalk";
459
850
  import ora from "ora";
460
- import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
461
- async function functionsList() {
462
- const client = getClient();
851
+ import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
852
+ function resolveProfileName3(profile) {
853
+ return profile ?? config.getActiveProfile() ?? "default";
854
+ }
855
+ async function functionsList(options = {}) {
856
+ const profileName = resolveProfileName3(options.profile);
857
+ const client = getClient(profileName);
463
858
  const spinner2 = ora("Fetching functions...").start();
464
859
  const result = await client.code.listFunctions();
465
860
  spinner2.stop();
466
861
  if (!result.success || !result.data.length) {
467
- console.log(chalk9.gray("No functions found."));
862
+ console.log(chalk10.gray("No functions found."));
468
863
  return;
469
864
  }
470
865
  console.log("");
@@ -478,10 +873,10 @@ async function functionsList() {
478
873
  });
479
874
  console.log("");
480
875
  }
481
- async function functionsCreate(slug) {
876
+ async function functionsCreate(slug, _options = {}) {
482
877
  const filename = `${slug}.js`;
483
- if (existsSync2(filename)) {
484
- console.log(chalk9.yellow(`${filename} already exists.`));
878
+ if (existsSync3(filename)) {
879
+ console.log(chalk10.yellow(`${filename} already exists.`));
485
880
  return;
486
881
  }
487
882
  const template = `/**
@@ -499,24 +894,25 @@ async function handler(input, globio) {
499
894
  };
500
895
  }
501
896
  `;
502
- writeFileSync2(filename, template);
503
- console.log(chalk9.green(`Created ${filename}`));
897
+ writeFileSync3(filename, template);
898
+ console.log(chalk10.green(`Created ${filename}`));
504
899
  console.log(
505
- chalk9.gray(`Deploy with: npx @globio/cli functions deploy ${slug}`)
900
+ chalk10.gray(`Deploy with: npx @globio/cli functions deploy ${slug}`)
506
901
  );
507
902
  }
508
903
  async function functionsDeploy(slug, options) {
509
904
  const filename = options.file ?? `${slug}.js`;
510
- if (!existsSync2(filename)) {
905
+ if (!existsSync3(filename)) {
511
906
  console.log(
512
- chalk9.red(
907
+ chalk10.red(
513
908
  `File not found: ${filename}. Create it with: npx @globio/cli functions create ${slug}`
514
909
  )
515
910
  );
516
911
  process.exit(1);
517
912
  }
518
- const code = readFileSync3(filename, "utf-8");
519
- const client = getClient();
913
+ const code = readFileSync4(filename, "utf-8");
914
+ const profileName = resolveProfileName3(options.profile);
915
+ const client = getClient(profileName);
520
916
  const spinner2 = ora(`Deploying ${slug}...`).start();
521
917
  const existing = await client.code.getFunction(slug);
522
918
  let result;
@@ -546,16 +942,17 @@ async function functionsInvoke(slug, options) {
546
942
  try {
547
943
  input = JSON.parse(options.input);
548
944
  } catch {
549
- console.error(chalk9.red("--input must be valid JSON"));
945
+ console.error(chalk10.red("--input must be valid JSON"));
550
946
  process.exit(1);
551
947
  }
552
948
  }
553
- const client = getClient();
949
+ const profileName = resolveProfileName3(options.profile);
950
+ const client = getClient(profileName);
554
951
  const spinner2 = ora(`Invoking ${slug}...`).start();
555
952
  const result = await client.code.invoke(slug, input);
556
953
  spinner2.stop();
557
954
  if (!result.success) {
558
- console.log(chalk9.red("Invocation failed"));
955
+ console.log(chalk10.red("Invocation failed"));
559
956
  console.error(result.error.message);
560
957
  return;
561
958
  }
@@ -567,12 +964,13 @@ Duration: ${result.data.duration_ms}ms`));
567
964
  }
568
965
  async function functionsLogs(slug, options) {
569
966
  const limit = options.limit ? parseInt(options.limit, 10) : 20;
570
- const client = getClient();
967
+ const profileName = resolveProfileName3(options.profile);
968
+ const client = getClient(profileName);
571
969
  const spinner2 = ora("Fetching invocations...").start();
572
970
  const result = await client.code.getInvocations(slug, limit);
573
971
  spinner2.stop();
574
972
  if (!result.success || !result.data.length) {
575
- console.log(chalk9.gray("No invocations yet."));
973
+ console.log(chalk10.gray("No invocations yet."));
576
974
  return;
577
975
  }
578
976
  console.log("");
@@ -580,13 +978,14 @@ async function functionsLogs(slug, options) {
580
978
  const status = inv.success ? "\x1B[38;2;244;140;6m\u2713\x1B[0m" : "\x1B[31m\u2717\x1B[0m";
581
979
  const date = new Date(inv.invoked_at * 1e3).toISOString().replace("T", " ").slice(0, 19);
582
980
  console.log(
583
- ` ${status} ${chalk9.gray(date)} ${inv.duration_ms}ms ${chalk9.gray(`[${inv.trigger_type}]`)}`
981
+ ` ${status} ${chalk10.gray(date)} ${inv.duration_ms}ms ${chalk10.gray(`[${inv.trigger_type}]`)}`
584
982
  );
585
983
  });
586
984
  console.log("");
587
985
  }
588
- async function functionsDelete(slug) {
589
- const client = getClient();
986
+ async function functionsDelete(slug, options = {}) {
987
+ const profileName = resolveProfileName3(options.profile);
988
+ const client = getClient(profileName);
590
989
  const spinner2 = ora(`Deleting ${slug}...`).start();
591
990
  const result = await client.code.deleteFunction(slug);
592
991
  if (!result.success) {
@@ -596,8 +995,9 @@ async function functionsDelete(slug) {
596
995
  }
597
996
  spinner2.succeed(`Deleted ${slug}`);
598
997
  }
599
- async function functionsToggle(slug, active) {
600
- const client = getClient();
998
+ async function functionsToggle(slug, active, options = {}) {
999
+ const profileName = resolveProfileName3(options.profile);
1000
+ const client = getClient(profileName);
601
1001
  const spinner2 = ora(
602
1002
  `${active ? "Enabling" : "Disabling"} ${slug}...`
603
1003
  ).start();
@@ -616,28 +1016,50 @@ var program = new Command();
616
1016
  program.name("globio").description("The official Globio CLI").version(version4).addHelpText("beforeAll", () => {
617
1017
  printBanner(version4);
618
1018
  return "";
619
- });
620
- program.command("login").description("Log in to your Globio account").action(login);
621
- program.command("logout").description("Log out").action(logout);
622
- program.command("whoami").description("Show current account and project").action(whoami);
623
- program.command("init").description("Initialize a Globio project").action(init);
1019
+ }).addHelpText(
1020
+ "after",
1021
+ `
1022
+ Examples:
1023
+ $ globio login
1024
+ $ globio login --profile work
1025
+ $ globio use work
1026
+ $ globio projects list
1027
+ $ globio projects use proj_abc123
1028
+ $ globio functions deploy my-function
1029
+ $ globio migrate firestore --from ./key.json --all
1030
+
1031
+ Credentials are stored in ~/.globio/profiles/
1032
+ `
1033
+ );
1034
+ program.command("login").description("Log in to your Globio account").option("-p, --profile <name>", "Profile name", "default").option("--token", "Use a personal access token").action(login);
1035
+ program.command("logout").description("Log out").option("--profile <name>", "Use a specific profile").action(logout);
1036
+ program.command("whoami").description("Show current account and project").option("--profile <name>", "Use a specific profile").action(whoami);
1037
+ program.command("use <profile>").description("Switch active profile").action(useProfile);
1038
+ program.command("init").description("Initialize a Globio project").option("--profile <name>", "Use a specific profile").action(init);
624
1039
  var projects = program.command("projects").description("Manage projects");
625
- projects.command("list").description("List projects").action(projectsList);
626
- projects.command("use <projectId>").description("Set active project").action(projectsUse);
627
- program.command("services").description("List available Globio services").action(servicesList);
1040
+ projects.command("list").description("List projects").option("--profile <name>", "Use a specific profile").action(projectsList);
1041
+ projects.command("create").description("Create a project").option("--profile <name>", "Use a specific profile").action(projectsCreate);
1042
+ projects.command("use <projectId>").description("Set active project").option("--profile <name>", "Use a specific profile").action(projectsUse);
1043
+ program.command("services").description("List available Globio services").option("--profile <name>", "Use a specific profile").action(servicesList);
628
1044
  var functions = program.command("functions").alias("fn").description("Manage GlobalCode edge functions");
629
- functions.command("list").description("List all functions").action(functionsList);
630
- functions.command("create <slug>").description("Scaffold a new function file locally").action(functionsCreate);
631
- functions.command("deploy <slug>").description("Deploy a function to GlobalCode").option("-f, --file <path>", "Path to function file").option("-n, --name <name>", "Display name").action(functionsDeploy);
632
- functions.command("invoke <slug>").description("Invoke a function").option("-i, --input <json>", "JSON input payload").action(functionsInvoke);
633
- functions.command("logs <slug>").description("Show invocation history").option("-l, --limit <n>", "Number of entries", "20").action(functionsLogs);
634
- functions.command("delete <slug>").description("Delete a function").action(functionsDelete);
635
- functions.command("enable <slug>").description("Enable a function").action((slug) => functionsToggle(slug, true));
636
- functions.command("disable <slug>").description("Disable a function").action((slug) => functionsToggle(slug, false));
1045
+ functions.command("list").description("List all functions").option("--profile <name>", "Use a specific profile").action(functionsList);
1046
+ functions.command("create <slug>").description("Scaffold a new function file locally").option("--profile <name>", "Use a specific profile").action(functionsCreate);
1047
+ functions.command("deploy <slug>").description("Deploy a function to GlobalCode").option("-f, --file <path>", "Path to function file").option("-n, --name <name>", "Display name").option("--profile <name>", "Use a specific profile").action(functionsDeploy);
1048
+ functions.command("invoke <slug>").description("Invoke a function").option("-i, --input <json>", "JSON input payload").option("--profile <name>", "Use a specific profile").action(functionsInvoke);
1049
+ functions.command("logs <slug>").description("Show invocation history").option("-l, --limit <n>", "Number of entries", "20").option("--profile <name>", "Use a specific profile").action(functionsLogs);
1050
+ functions.command("delete <slug>").description("Delete a function").option("--profile <name>", "Use a specific profile").action(functionsDelete);
1051
+ functions.command("enable <slug>").description("Enable a function").option("--profile <name>", "Use a specific profile").action((slug, options) => functionsToggle(slug, true, options));
1052
+ functions.command("disable <slug>").description("Disable a function").option("--profile <name>", "Use a specific profile").action((slug, options) => functionsToggle(slug, false, options));
637
1053
  var migrate = program.command("migrate").description("Migrate from Firebase to Globio");
638
- migrate.command("firestore").description("Migrate Firestore collections to GlobalDoc").requiredOption("--from <path>", "Path to Firebase service account JSON").option("--collection <name>", "Migrate a specific collection").option("--all", "Migrate all collections").action(migrateFirestore);
639
- migrate.command("firebase-storage").description("Migrate Firebase Storage to GlobalVault").requiredOption("--from <path>", "Path to Firebase service account JSON").requiredOption("--bucket <name>", "Firebase Storage bucket").option("--folder <path>", "Migrate a specific folder").option("--all", "Migrate all files").action(migrateFirebaseStorage);
640
- if (process.argv.length <= 2) {
641
- program.help();
1054
+ migrate.command("firestore").description("Migrate Firestore collections to GlobalDoc").requiredOption("--from <path>", "Path to Firebase service account JSON").option("--collection <name>", "Migrate a specific collection").option("--all", "Migrate all collections").option("--profile <name>", "Use a specific profile").action(migrateFirestore);
1055
+ migrate.command("firebase-storage").description("Migrate Firebase Storage to GlobalVault").requiredOption("--from <path>", "Path to Firebase service account JSON").requiredOption("--bucket <name>", "Firebase Storage bucket").option("--folder <path>", "Migrate a specific folder").option("--all", "Migrate all files").option("--profile <name>", "Use a specific profile").action(migrateFirebaseStorage);
1056
+ async function main() {
1057
+ if (process.argv.length <= 2) {
1058
+ program.help();
1059
+ }
1060
+ await program.parseAsync();
642
1061
  }
643
- await program.parseAsync();
1062
+ main().catch((error) => {
1063
+ console.error(error instanceof Error ? error.message : error);
1064
+ process.exit(1);
1065
+ });