@globio/cli 0.1.2 → 0.1.3

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,6 +5,7 @@ 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
@@ -24,14 +25,14 @@ var config = {
24
25
  });
25
26
  },
26
27
  clear: () => store.clear(),
27
- getApiKey: () => store.get("apiKey"),
28
- requireAuth: () => {
29
- const key = store.get("apiKey");
30
- if (!key) {
28
+ getPat: () => store.get("pat"),
29
+ requirePat: () => {
30
+ const pat = store.get("pat");
31
+ if (!pat) {
31
32
  console.error(chalk.red("Not logged in. Run: npx @globio/cli login"));
32
33
  process.exit(1);
33
34
  }
34
- return key;
35
+ return pat;
35
36
  },
36
37
  requireProject: () => {
37
38
  const projectId = store.get("projectId");
@@ -42,9 +43,78 @@ var config = {
42
43
  process.exit(1);
43
44
  }
44
45
  return projectId;
46
+ },
47
+ setProjectAuth: (projectId, apiKey, projectName) => {
48
+ const projectApiKeys = store.get("projectApiKeys") ?? {};
49
+ const projectNames = store.get("projectNames") ?? {};
50
+ projectApiKeys[projectId] = apiKey;
51
+ if (projectName) {
52
+ projectNames[projectId] = projectName;
53
+ }
54
+ store.set("projectApiKeys", projectApiKeys);
55
+ store.set("projectNames", projectNames);
56
+ store.set("projectId", projectId);
57
+ if (projectName) {
58
+ store.set("projectName", projectName);
59
+ }
60
+ },
61
+ getProjectApiKey: (projectId) => {
62
+ const projectApiKeys = store.get("projectApiKeys") ?? {};
63
+ return projectApiKeys[projectId];
64
+ },
65
+ requireProjectApiKey: () => {
66
+ const projectId = store.get("projectId");
67
+ if (!projectId) {
68
+ console.error(
69
+ chalk.red("No active project. Run: npx @globio/cli projects use <projectId>")
70
+ );
71
+ process.exit(1);
72
+ }
73
+ const projectApiKeys = store.get("projectApiKeys") ?? {};
74
+ const apiKey = projectApiKeys[projectId];
75
+ if (!apiKey) {
76
+ console.error(
77
+ chalk.red(
78
+ "No project API key stored for the active project. Run: npx @globio/cli projects use <projectId>"
79
+ )
80
+ );
81
+ process.exit(1);
82
+ }
83
+ return apiKey;
45
84
  }
46
85
  };
47
86
 
87
+ // src/lib/manage.ts
88
+ var API_BASE_URL = "https://api.globio.stanlink.online";
89
+ var CONSOLE_BASE_URL = "https://console.globio.stanlink.online";
90
+ function getAuthToken(explicitToken) {
91
+ if (explicitToken) return explicitToken;
92
+ return config.getPat();
93
+ }
94
+ async function manageRequest(path, options = {}) {
95
+ const headers = new Headers();
96
+ if (!(options.body instanceof FormData)) {
97
+ headers.set("Content-Type", "application/json");
98
+ }
99
+ const token = getAuthToken(options.token);
100
+ if (token) {
101
+ headers.set("Authorization", `Bearer ${token}`);
102
+ }
103
+ const response = await fetch(`${API_BASE_URL}/manage${path}`, {
104
+ method: options.method ?? "GET",
105
+ headers,
106
+ body: options.body ? JSON.stringify(options.body) : void 0
107
+ });
108
+ const payload = await response.json().catch(() => ({}));
109
+ if (!response.ok) {
110
+ throw new Error(payload.error || payload.message || "Management request failed");
111
+ }
112
+ return payload.data ?? payload;
113
+ }
114
+ function getConsoleCliAuthUrl(state) {
115
+ return `${CONSOLE_BASE_URL}/cli-auth?state=${encodeURIComponent(state)}`;
116
+ }
117
+
48
118
  // src/lib/banner.ts
49
119
  import { readFileSync } from "fs";
50
120
  import figlet from "figlet";
@@ -79,54 +149,119 @@ function getCliVersion() {
79
149
  }
80
150
 
81
151
  // src/auth/login.ts
82
- var DEFAULT_BASE_URL = "https://api.globio.stanlink.online";
83
152
  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
- }
153
+ function openBrowser(url) {
154
+ const command = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
155
+ exec(command);
156
+ }
157
+ function sleep(ms) {
158
+ return new Promise((resolve) => setTimeout(resolve, ms));
159
+ }
160
+ async function savePat(token) {
161
+ const account = await manageRequest("/account", { token });
162
+ config.set({
163
+ pat: token,
164
+ accountEmail: account.email,
165
+ accountName: account.display_name ?? account.email
166
+ });
167
+ return account;
168
+ }
169
+ async function runTokenLogin() {
170
+ const token = await p.text({
171
+ message: "Paste your personal access token",
172
+ placeholder: "glo_pat_...",
173
+ validate: (value) => {
174
+ if (!value) return "Personal access token is required";
175
+ if (!value.startsWith("glo_pat_")) return "Token must start with glo_pat_";
176
+ return void 0;
104
177
  }
105
- );
178
+ });
179
+ if (p.isCancel(token)) {
180
+ p.cancel("Login cancelled.");
181
+ process.exit(0);
182
+ }
106
183
  const spinner2 = p.spinner();
107
- spinner2.start("Validating credentials...");
184
+ spinner2.start("Validating personal access token...");
108
185
  try {
109
- const response = await fetch(`${DEFAULT_BASE_URL}/id/health`, {
110
- headers: {
111
- "X-Globio-Key": values.apiKey
186
+ const account = await savePat(token);
187
+ spinner2.stop("Token validated.");
188
+ p.outro(`Logged in as ${account.email}`);
189
+ } catch (error) {
190
+ spinner2.stop("Validation failed.");
191
+ p.outro(chalk2.red(error instanceof Error ? error.message : "Could not validate token"));
192
+ process.exit(1);
193
+ }
194
+ }
195
+ async function runBrowserLogin() {
196
+ const state = crypto.randomUUID();
197
+ const spinner2 = p.spinner();
198
+ await manageRequest("/cli-auth/request", {
199
+ method: "POST",
200
+ body: { state }
201
+ });
202
+ const url = getConsoleCliAuthUrl(state);
203
+ openBrowser(url);
204
+ console.log(" " + muted("Browser URL: ") + orange(url));
205
+ console.log("");
206
+ spinner2.start("Waiting for browser approval...");
207
+ const deadline = Date.now() + 5 * 60 * 1e3;
208
+ while (Date.now() < deadline) {
209
+ try {
210
+ const status = await manageRequest(
211
+ `/cli-auth/poll?state=${encodeURIComponent(state)}`
212
+ );
213
+ if (status.status === "expired") {
214
+ spinner2.stop("Approval window expired.");
215
+ p.outro(chalk2.red("CLI auth request expired. Try again or use globio login --token."));
216
+ process.exit(1);
112
217
  }
113
- });
114
- if (!response.ok) {
115
- spinner2.stop("Validation failed.");
116
- p.outro(chalk2.red("Invalid API key or project ID."));
117
- process.exit(1);
218
+ if (status.status === "approved" && status.code) {
219
+ const exchange = await manageRequest("/cli-auth/exchange", {
220
+ method: "POST",
221
+ body: { code: status.code }
222
+ });
223
+ config.set({
224
+ pat: exchange.token,
225
+ accountEmail: exchange.account.email,
226
+ accountName: exchange.account.display_name ?? exchange.account.email
227
+ });
228
+ spinner2.stop("Browser approval received.");
229
+ p.outro(`Logged in as ${exchange.account.email}`);
230
+ return;
231
+ }
232
+ } catch {
118
233
  }
119
- config.set({
120
- apiKey: values.apiKey,
121
- projectId: values.projectId
122
- });
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."));
234
+ await sleep(2e3);
235
+ }
236
+ spinner2.stop("Approval timed out.");
237
+ p.outro(chalk2.red("Timed out waiting for browser approval. Try again or use globio login --token."));
238
+ process.exit(1);
239
+ }
240
+ async function login(options = {}) {
241
+ printBanner(version);
242
+ if (options.token) {
243
+ await runTokenLogin();
244
+ return;
245
+ }
246
+ const choice = await p.select({
247
+ message: "Choose a login method",
248
+ options: [
249
+ { value: "browser", label: "Browser", hint: "Open console and approve access" },
250
+ { value: "token", label: "Token", hint: "Paste a personal access token" }
251
+ ]
252
+ });
253
+ if (p.isCancel(choice)) {
254
+ p.cancel("Login cancelled.");
255
+ process.exit(0);
256
+ }
257
+ if (choice === "token") {
258
+ await runTokenLogin();
259
+ return;
260
+ }
261
+ try {
262
+ await runBrowserLogin();
263
+ } catch (error) {
264
+ p.outro(chalk2.red(error instanceof Error ? error.message : "Could not connect to Globio."));
130
265
  process.exit(1);
131
266
  }
132
267
  }
@@ -143,18 +278,21 @@ async function logout() {
143
278
  import chalk4 from "chalk";
144
279
  async function whoami() {
145
280
  const cfg = config.get();
146
- if (!cfg.apiKey) {
281
+ if (!cfg.pat) {
147
282
  console.log(chalk4.red("Not logged in."));
148
283
  return;
149
284
  }
150
285
  console.log("");
151
- console.log(chalk4.cyan("API Key: ") + cfg.apiKey);
152
- console.log(chalk4.cyan("Project: ") + (cfg.projectId ?? "none"));
286
+ console.log(chalk4.cyan("Account: ") + (cfg.accountEmail ?? "unknown"));
287
+ console.log(chalk4.cyan("Name: ") + (cfg.accountName ?? "unknown"));
288
+ console.log(
289
+ chalk4.cyan("Project: ") + (cfg.projectId ? `${cfg.projectName ?? "unnamed"} (${cfg.projectId})` : "none")
290
+ );
153
291
  console.log("");
154
292
  }
155
293
 
156
294
  // src/commands/init.ts
157
- import * as p5 from "@clack/prompts";
295
+ import * as p6 from "@clack/prompts";
158
296
  import { existsSync, readFileSync as readFileSync2, writeFileSync } from "fs";
159
297
 
160
298
  // src/prompts/init.ts
@@ -162,16 +300,6 @@ import * as p3 from "@clack/prompts";
162
300
  async function promptInit() {
163
301
  return p3.group(
164
302
  {
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
303
  migrateFromFirebase: () => p3.confirm({
176
304
  message: "Migrating from Firebase?",
177
305
  initialValue: false
@@ -232,8 +360,7 @@ function createProgressBar(label) {
232
360
  // src/lib/sdk.ts
233
361
  import { Globio } from "@globio/sdk";
234
362
  function getClient() {
235
- const apiKey = config.requireAuth();
236
- config.requireProject();
363
+ const apiKey = config.requireProjectApiKey();
237
364
  return new Globio({ apiKey });
238
365
  }
239
366
 
@@ -363,22 +490,146 @@ async function migrateFirebaseStorage(options) {
363
490
  );
364
491
  }
365
492
 
493
+ // src/commands/projects.ts
494
+ import * as p5 from "@clack/prompts";
495
+ import chalk7 from "chalk";
496
+ function slugify(value) {
497
+ return value.toLowerCase().trim().replace(/[^a-z0-9\\s-]/g, "").replace(/\\s+/g, "-").replace(/-+/g, "-");
498
+ }
499
+ async function ensureProjectKey(projectId) {
500
+ const existingKey = config.getProjectApiKey(projectId);
501
+ if (existingKey) return existingKey;
502
+ const created = await manageRequest(`/projects/${projectId}/keys`, {
503
+ method: "POST",
504
+ body: {
505
+ name: "CLI server key",
506
+ scope: "server"
507
+ }
508
+ });
509
+ if (!created.token) {
510
+ throw new Error("Management API did not return a project API key");
511
+ }
512
+ return created.token;
513
+ }
514
+ async function projectsList() {
515
+ const projects2 = await manageRequest("/projects");
516
+ const activeProjectId = config.get().projectId;
517
+ const grouped = /* @__PURE__ */ new Map();
518
+ for (const project of projects2) {
519
+ const list = grouped.get(project.org_name) ?? [];
520
+ list.push(project);
521
+ grouped.set(project.org_name, list);
522
+ }
523
+ console.log("");
524
+ if (!projects2.length) {
525
+ console.log(chalk7.gray("No projects found."));
526
+ console.log("");
527
+ return;
528
+ }
529
+ for (const [orgName, orgProjects] of grouped.entries()) {
530
+ console.log(chalk7.cyan(`org: ${orgName}`));
531
+ for (const project of orgProjects) {
532
+ const marker = project.id === activeProjectId ? chalk7.green("\u25CF") : chalk7.gray("\u25CB");
533
+ const active = project.id === activeProjectId ? chalk7.green(" (active)") : "";
534
+ console.log(` ${marker} ${project.slug.padEnd(22)} ${chalk7.gray(project.id)}${active}`);
535
+ }
536
+ console.log("");
537
+ }
538
+ }
539
+ async function projectsUse(projectId) {
540
+ const projects2 = await manageRequest("/projects");
541
+ const project = projects2.find((item) => item.id === projectId);
542
+ if (!project) {
543
+ console.log(chalk7.red(`Project not found: ${projectId}`));
544
+ process.exit(1);
545
+ }
546
+ const apiKey = await ensureProjectKey(projectId);
547
+ config.setProjectAuth(projectId, apiKey, project.name);
548
+ console.log(chalk7.green("Active project set to: ") + chalk7.cyan(`${project.name} (${project.id})`));
549
+ }
550
+ async function projectsCreate() {
551
+ const orgs = await manageRequest("/orgs");
552
+ if (!orgs.length) {
553
+ console.log(chalk7.red("No organizations found. Create one in the console first."));
554
+ process.exit(1);
555
+ }
556
+ const orgId = await p5.select({
557
+ message: "Select an organization",
558
+ options: orgs.map((org) => ({
559
+ value: org.id,
560
+ label: org.name,
561
+ hint: org.role
562
+ }))
563
+ });
564
+ if (p5.isCancel(orgId)) {
565
+ p5.cancel("Project creation cancelled.");
566
+ process.exit(0);
567
+ }
568
+ const values = await p5.group(
569
+ {
570
+ name: () => p5.text({
571
+ message: "Project name",
572
+ validate: (value) => !value ? "Project name is required" : void 0
573
+ }),
574
+ slug: ({ results }) => p5.text({
575
+ message: "Project slug",
576
+ initialValue: slugify(String(results.name ?? "")),
577
+ validate: (value) => !value ? "Project slug is required" : void 0
578
+ }),
579
+ environment: () => p5.select({
580
+ message: "Environment",
581
+ options: [
582
+ { value: "development", label: "development" },
583
+ { value: "staging", label: "staging" },
584
+ { value: "production", label: "production" }
585
+ ]
586
+ })
587
+ },
588
+ {
589
+ onCancel: () => {
590
+ p5.cancel("Project creation cancelled.");
591
+ process.exit(0);
592
+ }
593
+ }
594
+ );
595
+ const result = await manageRequest("/projects", {
596
+ method: "POST",
597
+ body: {
598
+ org_id: orgId,
599
+ name: values.name,
600
+ slug: values.slug,
601
+ environment: values.environment
602
+ }
603
+ });
604
+ config.setProjectAuth(result.project.id, result.keys.server, result.project.name);
605
+ console.log("");
606
+ console.log(chalk7.green("Project created successfully."));
607
+ console.log(chalk7.cyan("Project: ") + `${result.project.name} (${result.project.id})`);
608
+ console.log(chalk7.cyan("Client key: ") + result.keys.client);
609
+ console.log(chalk7.cyan("Server key: ") + result.keys.server);
610
+ console.log("");
611
+ }
612
+
366
613
  // src/commands/init.ts
367
614
  var version3 = getCliVersion();
368
615
  async function init() {
369
616
  printBanner(version3);
370
- p5.intro(orange("\u21D2\u21D2") + " Initialize your Globio project");
617
+ p6.intro(orange("\u21D2\u21D2") + " Initialize your Globio project");
618
+ const cfg = config.get();
619
+ if (!cfg.projectId) {
620
+ await projectsCreate();
621
+ } else {
622
+ await projectsUse(cfg.projectId);
623
+ }
371
624
  const values = await promptInit();
372
- config.set({
373
- apiKey: values.apiKey,
374
- projectId: values.projectId
375
- });
625
+ const activeProjectKey = config.requireProjectApiKey();
626
+ const activeProjectId = config.requireProject();
376
627
  if (!existsSync("globio.config.ts")) {
377
628
  writeFileSync(
378
629
  "globio.config.ts",
379
- `import { GlobioClient } from '@globio/sdk';
630
+ `import { Globio } from '@globio/sdk';
380
631
 
381
- export const globio = new GlobioClient({
632
+ export const globio = new Globio({
382
633
  apiKey: process.env.GLOBIO_API_KEY!,
383
634
  });
384
635
  `
@@ -386,7 +637,7 @@ export const globio = new GlobioClient({
386
637
  printSuccess("Created globio.config.ts");
387
638
  }
388
639
  if (!existsSync(".env")) {
389
- writeFileSync(".env", `GLOBIO_API_KEY=${values.apiKey}
640
+ writeFileSync(".env", `GLOBIO_API_KEY=${activeProjectKey}
390
641
  `);
391
642
  printSuccess("Created .env");
392
643
  }
@@ -407,24 +658,13 @@ export const globio = new GlobioClient({
407
658
  });
408
659
  }
409
660
  console.log("");
410
- 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
- }
661
+ p6.outro(
662
+ orange("\u21D2\u21D2") + " Your project is ready.\n\n " + muted("Next steps:") + `
414
663
 
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"))
664
+ npm install @globio/sdk
665
+ # active project: ${activeProjectId}
666
+ npx @globio/cli functions create my-first-function`
422
667
  );
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
668
  }
429
669
 
430
670
  // src/commands/services.ts
@@ -617,12 +857,13 @@ program.name("globio").description("The official Globio CLI").version(version4).
617
857
  printBanner(version4);
618
858
  return "";
619
859
  });
620
- program.command("login").description("Log in to your Globio account").action(login);
860
+ program.command("login").description("Log in to your Globio account").option("--token", "Use a personal access token").action(login);
621
861
  program.command("logout").description("Log out").action(logout);
622
862
  program.command("whoami").description("Show current account and project").action(whoami);
623
863
  program.command("init").description("Initialize a Globio project").action(init);
624
864
  var projects = program.command("projects").description("Manage projects");
625
865
  projects.command("list").description("List projects").action(projectsList);
866
+ projects.command("create").description("Create a project").action(projectsCreate);
626
867
  projects.command("use <projectId>").description("Set active project").action(projectsUse);
627
868
  program.command("services").description("List available Globio services").action(servicesList);
628
869
  var functions = program.command("functions").alias("fn").description("Manage GlobalCode edge functions");
@@ -637,7 +878,13 @@ functions.command("disable <slug>").description("Disable a function").action((sl
637
878
  var migrate = program.command("migrate").description("Migrate from Firebase to Globio");
638
879
  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
880
  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();
881
+ async function main() {
882
+ if (process.argv.length <= 2) {
883
+ program.help();
884
+ }
885
+ await program.parseAsync();
642
886
  }
643
- await program.parseAsync();
887
+ main().catch((error) => {
888
+ console.error(error instanceof Error ? error.message : error);
889
+ process.exit(1);
890
+ });
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@globio/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "license": "MIT",
5
5
  "exports": "./src/index.ts"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@globio/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "The official CLI for Globio — game backend as a service",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/auth/login.ts CHANGED
@@ -1,72 +1,155 @@
1
1
  import * as p from '@clack/prompts';
2
+ import { exec } from 'child_process';
2
3
  import chalk from 'chalk';
3
4
  import { config } from '../lib/config.js';
5
+ import { getConsoleCliAuthUrl, manageRequest, type ManageAccount } from '../lib/manage.js';
4
6
  import { getCliVersion, muted, orange, printBanner } from '../lib/banner.js';
5
7
 
6
- const DEFAULT_BASE_URL = 'https://api.globio.stanlink.online';
7
8
  const version = getCliVersion();
8
9
 
9
- export async function login() {
10
- printBanner(version);
10
+ function openBrowser(url: string) {
11
+ const command = process.platform === 'win32'
12
+ ? `start "" "${url}"`
13
+ : process.platform === 'darwin'
14
+ ? `open "${url}"`
15
+ : `xdg-open "${url}"`;
16
+
17
+ exec(command);
18
+ }
19
+
20
+ function sleep(ms: number) {
21
+ return new Promise((resolve) => setTimeout(resolve, ms));
22
+ }
11
23
 
12
- const values = await p.group(
13
- {
14
- apiKey: () =>
15
- p.text({
16
- message: 'Paste your Globio API key',
17
- placeholder: 'gk_live_...',
18
- validate: (value) => (!value ? 'API key is required' : undefined),
19
- }),
20
- projectId: () =>
21
- p.text({
22
- message: 'Paste your Project ID',
23
- placeholder: 'proj_...',
24
- validate: (value) => (!value ? 'Project ID is required' : undefined),
25
- }),
24
+ async function savePat(token: string) {
25
+ const account = await manageRequest<ManageAccount>('/account', { token });
26
+ config.set({
27
+ pat: token,
28
+ accountEmail: account.email,
29
+ accountName: account.display_name ?? account.email,
30
+ });
31
+ return account;
32
+ }
33
+
34
+ async function runTokenLogin() {
35
+ const token = await p.text({
36
+ message: 'Paste your personal access token',
37
+ placeholder: 'glo_pat_...',
38
+ validate: (value) => {
39
+ if (!value) return 'Personal access token is required';
40
+ if (!value.startsWith('glo_pat_')) return 'Token must start with glo_pat_';
41
+ return undefined;
26
42
  },
27
- {
28
- onCancel: () => {
29
- p.cancel('Login cancelled.');
30
- process.exit(0);
31
- },
32
- }
33
- );
43
+ });
34
44
 
35
- const spinner = p.spinner();
36
- spinner.start('Validating credentials...');
45
+ if (p.isCancel(token)) {
46
+ p.cancel('Login cancelled.');
47
+ process.exit(0);
48
+ }
37
49
 
50
+ const spinner = p.spinner();
51
+ spinner.start('Validating personal access token...');
38
52
  try {
39
- const response = await fetch(`${DEFAULT_BASE_URL}/id/health`, {
40
- headers: {
41
- 'X-Globio-Key': values.apiKey as string,
42
- },
43
- });
44
-
45
- if (!response.ok) {
46
- spinner.stop('Validation failed.');
47
- p.outro(chalk.red('Invalid API key or project ID.'));
48
- process.exit(1);
53
+ const account = await savePat(token);
54
+ spinner.stop('Token validated.');
55
+ p.outro(`Logged in as ${account.email}`);
56
+ } catch (error) {
57
+ spinner.stop('Validation failed.');
58
+ p.outro(chalk.red(error instanceof Error ? error.message : 'Could not validate token'));
59
+ process.exit(1);
60
+ }
61
+ }
62
+
63
+ async function runBrowserLogin() {
64
+ const state = crypto.randomUUID();
65
+ const spinner = p.spinner();
66
+
67
+ await manageRequest('/cli-auth/request', {
68
+ method: 'POST',
69
+ body: { state },
70
+ });
71
+
72
+ const url = getConsoleCliAuthUrl(state);
73
+ openBrowser(url);
74
+ console.log(' ' + muted('Browser URL: ') + orange(url));
75
+ console.log('');
76
+
77
+ spinner.start('Waiting for browser approval...');
78
+ const deadline = Date.now() + 5 * 60 * 1000;
79
+
80
+ while (Date.now() < deadline) {
81
+ try {
82
+ const status = await manageRequest<{ status: 'pending' | 'approved' | 'expired'; code?: string }>(
83
+ `/cli-auth/poll?state=${encodeURIComponent(state)}`
84
+ );
85
+
86
+ if (status.status === 'expired') {
87
+ spinner.stop('Approval window expired.');
88
+ p.outro(chalk.red('CLI auth request expired. Try again or use globio login --token.'));
89
+ process.exit(1);
90
+ }
91
+
92
+ if (status.status === 'approved' && status.code) {
93
+ const exchange = await manageRequest<{
94
+ token: string;
95
+ account: { email: string; display_name: string | null };
96
+ }>('/cli-auth/exchange', {
97
+ method: 'POST',
98
+ body: { code: status.code },
99
+ });
100
+
101
+ config.set({
102
+ pat: exchange.token,
103
+ accountEmail: exchange.account.email,
104
+ accountName: exchange.account.display_name ?? exchange.account.email,
105
+ });
106
+
107
+ spinner.stop('Browser approval received.');
108
+ p.outro(`Logged in as ${exchange.account.email}`);
109
+ return;
110
+ }
111
+ } catch {
112
+ // Keep polling until timeout.
49
113
  }
50
114
 
51
- config.set({
52
- apiKey: values.apiKey as string,
53
- projectId: values.projectId as string,
54
- });
55
-
56
- spinner.stop('Credentials validated.');
57
- p.outro(
58
- ' Logged in.\n\n' +
59
- ' ' +
60
- muted('API Key: ') +
61
- orange(values.apiKey as string) +
62
- '\n' +
63
- ' ' +
64
- muted('Project: ') +
65
- orange(values.projectId as string)
66
- );
67
- } catch {
68
- spinner.stop('');
69
- p.outro(chalk.red('Could not connect to Globio. Check your credentials.'));
115
+ await sleep(2000);
116
+ }
117
+
118
+ spinner.stop('Approval timed out.');
119
+ p.outro(chalk.red('Timed out waiting for browser approval. Try again or use globio login --token.'));
120
+ process.exit(1);
121
+ }
122
+
123
+ export async function login(options: { token?: boolean } = {}) {
124
+ printBanner(version);
125
+
126
+ if (options.token) {
127
+ await runTokenLogin();
128
+ return;
129
+ }
130
+
131
+ const choice = await p.select({
132
+ message: 'Choose a login method',
133
+ options: [
134
+ { value: 'browser', label: 'Browser', hint: 'Open console and approve access' },
135
+ { value: 'token', label: 'Token', hint: 'Paste a personal access token' },
136
+ ],
137
+ });
138
+
139
+ if (p.isCancel(choice)) {
140
+ p.cancel('Login cancelled.');
141
+ process.exit(0);
142
+ }
143
+
144
+ if (choice === 'token') {
145
+ await runTokenLogin();
146
+ return;
147
+ }
148
+
149
+ try {
150
+ await runBrowserLogin();
151
+ } catch (error) {
152
+ p.outro(chalk.red(error instanceof Error ? error.message : 'Could not connect to Globio.'));
70
153
  process.exit(1);
71
154
  }
72
155
  }
@@ -3,13 +3,17 @@ import { config } from '../lib/config.js';
3
3
 
4
4
  export async function whoami() {
5
5
  const cfg = config.get();
6
- if (!cfg.apiKey) {
6
+ if (!cfg.pat) {
7
7
  console.log(chalk.red('Not logged in.'));
8
8
  return;
9
9
  }
10
10
 
11
11
  console.log('');
12
- console.log(chalk.cyan('API Key: ') + cfg.apiKey);
13
- console.log(chalk.cyan('Project: ') + (cfg.projectId ?? 'none'));
12
+ console.log(chalk.cyan('Account: ') + (cfg.accountEmail ?? 'unknown'));
13
+ console.log(chalk.cyan('Name: ') + (cfg.accountName ?? 'unknown'));
14
+ console.log(
15
+ chalk.cyan('Project: ') +
16
+ (cfg.projectId ? `${cfg.projectName ?? 'unnamed'} (${cfg.projectId})` : 'none')
17
+ );
14
18
  console.log('');
15
19
  }
@@ -10,6 +10,7 @@ import {
10
10
  } from '../lib/banner.js';
11
11
  import { promptInit } from '../prompts/init.js';
12
12
  import { migrateFirestore, migrateFirebaseStorage } from './migrate.js';
13
+ import { projectsCreate, projectsUse } from './projects.js';
13
14
 
14
15
  const version = getCliVersion();
15
16
 
@@ -17,19 +18,23 @@ export async function init() {
17
18
  printBanner(version);
18
19
  p.intro(orange('⇒⇒') + ' Initialize your Globio project');
19
20
 
20
- const values = await promptInit();
21
+ const cfg = config.get();
22
+ if (!cfg.projectId) {
23
+ await projectsCreate();
24
+ } else {
25
+ await projectsUse(cfg.projectId);
26
+ }
21
27
 
22
- config.set({
23
- apiKey: values.apiKey as string,
24
- projectId: values.projectId as string,
25
- });
28
+ const values = await promptInit();
29
+ const activeProjectKey = config.requireProjectApiKey();
30
+ const activeProjectId = config.requireProject();
26
31
 
27
32
  if (!existsSync('globio.config.ts')) {
28
33
  writeFileSync(
29
34
  'globio.config.ts',
30
- `import { GlobioClient } from '@globio/sdk';
35
+ `import { Globio } from '@globio/sdk';
31
36
 
32
- export const globio = new GlobioClient({
37
+ export const globio = new Globio({
33
38
  apiKey: process.env.GLOBIO_API_KEY!,
34
39
  });
35
40
  `
@@ -38,7 +43,7 @@ export const globio = new GlobioClient({
38
43
  }
39
44
 
40
45
  if (!existsSync('.env')) {
41
- writeFileSync('.env', `GLOBIO_API_KEY=${values.apiKey}\n`);
46
+ writeFileSync('.env', `GLOBIO_API_KEY=${activeProjectKey}\n`);
42
47
  printSuccess('Created .env');
43
48
  }
44
49
 
@@ -70,6 +75,7 @@ export const globio = new GlobioClient({
70
75
  muted('Next steps:') +
71
76
  '\n\n' +
72
77
  ' npm install @globio/sdk\n' +
78
+ ` # active project: ${activeProjectId}\n` +
73
79
  ' npx @globio/cli functions create my-first-function'
74
80
  );
75
81
  }
@@ -1,16 +1,154 @@
1
+ import * as p from '@clack/prompts';
1
2
  import chalk from 'chalk';
2
3
  import { config } from '../lib/config.js';
4
+ import {
5
+ manageRequest,
6
+ type ManageOrg,
7
+ type ManageProject,
8
+ type ManageProjectKey,
9
+ } from '../lib/manage.js';
10
+
11
+ function slugify(value: string) {
12
+ return value
13
+ .toLowerCase()
14
+ .trim()
15
+ .replace(/[^a-z0-9\\s-]/g, '')
16
+ .replace(/\\s+/g, '-')
17
+ .replace(/-+/g, '-');
18
+ }
19
+
20
+ async function ensureProjectKey(projectId: string) {
21
+ const existingKey = config.getProjectApiKey(projectId);
22
+ if (existingKey) return existingKey;
23
+
24
+ const created = await manageRequest<ManageProjectKey>(`/projects/${projectId}/keys`, {
25
+ method: 'POST',
26
+ body: {
27
+ name: 'CLI server key',
28
+ scope: 'server',
29
+ },
30
+ });
31
+
32
+ if (!created.token) {
33
+ throw new Error('Management API did not return a project API key');
34
+ }
35
+
36
+ return created.token;
37
+ }
3
38
 
4
39
  export async function projectsList() {
5
- const cfg = config.get();
6
- console.log('');
7
- console.log(
8
- chalk.cyan('Active project: ') + (cfg.projectId ?? chalk.gray('none'))
9
- );
40
+ const projects = await manageRequest<ManageProject[]>('/projects');
41
+ const activeProjectId = config.get().projectId;
42
+ const grouped = new Map<string, ManageProject[]>();
43
+
44
+ for (const project of projects) {
45
+ const list = grouped.get(project.org_name) ?? [];
46
+ list.push(project);
47
+ grouped.set(project.org_name, list);
48
+ }
49
+
10
50
  console.log('');
51
+ if (!projects.length) {
52
+ console.log(chalk.gray('No projects found.'));
53
+ console.log('');
54
+ return;
55
+ }
56
+
57
+ for (const [orgName, orgProjects] of grouped.entries()) {
58
+ console.log(chalk.cyan(`org: ${orgName}`));
59
+ for (const project of orgProjects) {
60
+ const marker = project.id === activeProjectId ? chalk.green('●') : chalk.gray('○');
61
+ const active = project.id === activeProjectId ? chalk.green(' (active)') : '';
62
+ console.log(` ${marker} ${project.slug.padEnd(22)} ${chalk.gray(project.id)}${active}`);
63
+ }
64
+ console.log('');
65
+ }
11
66
  }
12
67
 
13
68
  export async function projectsUse(projectId: string) {
14
- config.set({ projectId });
15
- console.log(chalk.green('Active project set to: ') + chalk.cyan(projectId));
69
+ const projects = await manageRequest<ManageProject[]>('/projects');
70
+ const project = projects.find((item) => item.id === projectId);
71
+ if (!project) {
72
+ console.log(chalk.red(`Project not found: ${projectId}`));
73
+ process.exit(1);
74
+ }
75
+
76
+ const apiKey = await ensureProjectKey(projectId);
77
+ config.setProjectAuth(projectId, apiKey, project.name);
78
+ console.log(chalk.green('Active project set to: ') + chalk.cyan(`${project.name} (${project.id})`));
79
+ }
80
+
81
+ export async function projectsCreate() {
82
+ const orgs = await manageRequest<ManageOrg[]>('/orgs');
83
+ if (!orgs.length) {
84
+ console.log(chalk.red('No organizations found. Create one in the console first.'));
85
+ process.exit(1);
86
+ }
87
+
88
+ const orgId = await p.select({
89
+ message: 'Select an organization',
90
+ options: orgs.map((org) => ({
91
+ value: org.id,
92
+ label: org.name,
93
+ hint: org.role,
94
+ })),
95
+ });
96
+
97
+ if (p.isCancel(orgId)) {
98
+ p.cancel('Project creation cancelled.');
99
+ process.exit(0);
100
+ }
101
+
102
+ const values = await p.group(
103
+ {
104
+ name: () =>
105
+ p.text({
106
+ message: 'Project name',
107
+ validate: (value) => (!value ? 'Project name is required' : undefined),
108
+ }),
109
+ slug: ({ results }) =>
110
+ p.text({
111
+ message: 'Project slug',
112
+ initialValue: slugify(String(results.name ?? '')),
113
+ validate: (value) => (!value ? 'Project slug is required' : undefined),
114
+ }),
115
+ environment: () =>
116
+ p.select({
117
+ message: 'Environment',
118
+ options: [
119
+ { value: 'development', label: 'development' },
120
+ { value: 'staging', label: 'staging' },
121
+ { value: 'production', label: 'production' },
122
+ ],
123
+ }),
124
+ },
125
+ {
126
+ onCancel: () => {
127
+ p.cancel('Project creation cancelled.');
128
+ process.exit(0);
129
+ },
130
+ }
131
+ );
132
+
133
+ const result = await manageRequest<{
134
+ project: { id: string; name: string; slug: string; environment: string; active: boolean };
135
+ keys: { client: string; server: string };
136
+ }>('/projects', {
137
+ method: 'POST',
138
+ body: {
139
+ org_id: orgId,
140
+ name: values.name,
141
+ slug: values.slug,
142
+ environment: values.environment,
143
+ },
144
+ });
145
+
146
+ config.setProjectAuth(result.project.id, result.keys.server, result.project.name);
147
+
148
+ console.log('');
149
+ console.log(chalk.green('Project created successfully.'));
150
+ console.log(chalk.cyan('Project: ') + `${result.project.name} (${result.project.id})`);
151
+ console.log(chalk.cyan('Client key: ') + result.keys.client);
152
+ console.log(chalk.cyan('Server key: ') + result.keys.server);
153
+ console.log('');
16
154
  }
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ import { login } from './auth/login.js';
4
4
  import { logout } from './auth/logout.js';
5
5
  import { whoami } from './auth/whoami.js';
6
6
  import { init } from './commands/init.js';
7
- import { projectsList, projectsUse } from './commands/projects.js';
7
+ import { projectsCreate, projectsList, projectsUse } from './commands/projects.js';
8
8
  import { servicesList } from './commands/services.js';
9
9
  import {
10
10
  functionsList,
@@ -34,7 +34,11 @@ program
34
34
  return '';
35
35
  });
36
36
 
37
- program.command('login').description('Log in to your Globio account').action(login);
37
+ program
38
+ .command('login')
39
+ .description('Log in to your Globio account')
40
+ .option('--token', 'Use a personal access token')
41
+ .action(login);
38
42
  program.command('logout').description('Log out').action(logout);
39
43
  program.command('whoami').description('Show current account and project').action(whoami);
40
44
 
@@ -42,6 +46,7 @@ program.command('init').description('Initialize a Globio project').action(init);
42
46
 
43
47
  const projects = program.command('projects').description('Manage projects');
44
48
  projects.command('list').description('List projects').action(projectsList);
49
+ projects.command('create').description('Create a project').action(projectsCreate);
45
50
  projects.command('use <projectId>').description('Set active project').action(projectsUse);
46
51
 
47
52
  program.command('services').description('List available Globio services').action(servicesList);
@@ -94,8 +99,15 @@ migrate
94
99
  .option('--all', 'Migrate all files')
95
100
  .action(migrateFirebaseStorage);
96
101
 
97
- if (process.argv.length <= 2) {
98
- program.help();
102
+ async function main() {
103
+ if (process.argv.length <= 2) {
104
+ program.help();
105
+ }
106
+
107
+ await program.parseAsync();
99
108
  }
100
109
 
101
- await program.parseAsync();
110
+ main().catch((error) => {
111
+ console.error(error instanceof Error ? error.message : error);
112
+ process.exit(1);
113
+ });
package/src/lib/config.ts CHANGED
@@ -2,10 +2,13 @@ import chalk from 'chalk';
2
2
  import Conf from 'conf';
3
3
 
4
4
  interface GlobioConfig {
5
- apiKey?: string;
5
+ pat?: string;
6
+ accountEmail?: string;
7
+ accountName?: string;
6
8
  projectId?: string;
7
9
  projectName?: string;
8
- email?: string;
10
+ projectApiKeys?: Record<string, string>;
11
+ projectNames?: Record<string, string>;
9
12
  }
10
13
 
11
14
  const store = new Conf<GlobioConfig>({
@@ -23,14 +26,14 @@ export const config = {
23
26
  });
24
27
  },
25
28
  clear: () => store.clear(),
26
- getApiKey: () => store.get('apiKey'),
27
- requireAuth: () => {
28
- const key = store.get('apiKey');
29
- if (!key) {
29
+ getPat: () => store.get('pat'),
30
+ requirePat: () => {
31
+ const pat = store.get('pat');
32
+ if (!pat) {
30
33
  console.error(chalk.red('Not logged in. Run: npx @globio/cli login'));
31
34
  process.exit(1);
32
35
  }
33
- return key;
36
+ return pat;
34
37
  },
35
38
  requireProject: () => {
36
39
  const projectId = store.get('projectId');
@@ -42,6 +45,47 @@ export const config = {
42
45
  }
43
46
  return projectId;
44
47
  },
48
+ setProjectAuth: (projectId: string, apiKey: string, projectName?: string) => {
49
+ const projectApiKeys = store.get('projectApiKeys') ?? {};
50
+ const projectNames = store.get('projectNames') ?? {};
51
+ projectApiKeys[projectId] = apiKey;
52
+ if (projectName) {
53
+ projectNames[projectId] = projectName;
54
+ }
55
+
56
+ store.set('projectApiKeys', projectApiKeys);
57
+ store.set('projectNames', projectNames);
58
+ store.set('projectId', projectId);
59
+ if (projectName) {
60
+ store.set('projectName', projectName);
61
+ }
62
+ },
63
+ getProjectApiKey: (projectId: string) => {
64
+ const projectApiKeys = store.get('projectApiKeys') ?? {};
65
+ return projectApiKeys[projectId];
66
+ },
67
+ requireProjectApiKey: () => {
68
+ const projectId = store.get('projectId');
69
+ if (!projectId) {
70
+ console.error(
71
+ chalk.red('No active project. Run: npx @globio/cli projects use <projectId>')
72
+ );
73
+ process.exit(1);
74
+ }
75
+
76
+ const projectApiKeys = store.get('projectApiKeys') ?? {};
77
+ const apiKey = projectApiKeys[projectId];
78
+ if (!apiKey) {
79
+ console.error(
80
+ chalk.red(
81
+ 'No project API key stored for the active project. Run: npx @globio/cli projects use <projectId>'
82
+ )
83
+ );
84
+ process.exit(1);
85
+ }
86
+
87
+ return apiKey;
88
+ },
45
89
  };
46
90
 
47
91
  export type { GlobioConfig };
@@ -0,0 +1,83 @@
1
+ import { config } from './config.js';
2
+
3
+ const API_BASE_URL = 'https://api.globio.stanlink.online';
4
+ const CONSOLE_BASE_URL = 'https://console.globio.stanlink.online';
5
+
6
+ interface ManageRequestOptions {
7
+ method?: string;
8
+ body?: unknown;
9
+ token?: string;
10
+ }
11
+
12
+ export interface ManageAccount {
13
+ id: string;
14
+ email: string;
15
+ display_name: string | null;
16
+ avatar_url: string | null;
17
+ created_at: number;
18
+ }
19
+
20
+ export interface ManageOrg {
21
+ id: string;
22
+ name: string;
23
+ slug: string;
24
+ role: 'owner' | 'admin' | 'developer' | 'viewer';
25
+ created_at: number;
26
+ }
27
+
28
+ export interface ManageProject {
29
+ id: string;
30
+ name: string;
31
+ slug: string;
32
+ org_id: string;
33
+ org_name: string;
34
+ environment: string;
35
+ active: boolean;
36
+ }
37
+
38
+ export interface ManageProjectKey {
39
+ id: string;
40
+ name: string;
41
+ key_prefix: string;
42
+ scope: string;
43
+ created_at: number;
44
+ last_used_at: number | null;
45
+ token?: string;
46
+ }
47
+
48
+ function getAuthToken(explicitToken?: string): string | undefined {
49
+ if (explicitToken) return explicitToken;
50
+ return config.getPat();
51
+ }
52
+
53
+ export async function manageRequest<T>(
54
+ path: string,
55
+ options: ManageRequestOptions = {}
56
+ ): Promise<T> {
57
+ const headers = new Headers();
58
+ if (!(options.body instanceof FormData)) {
59
+ headers.set('Content-Type', 'application/json');
60
+ }
61
+
62
+ const token = getAuthToken(options.token);
63
+ if (token) {
64
+ headers.set('Authorization', `Bearer ${token}`);
65
+ }
66
+
67
+ const response = await fetch(`${API_BASE_URL}/manage${path}`, {
68
+ method: options.method ?? 'GET',
69
+ headers,
70
+ body: options.body ? JSON.stringify(options.body) : undefined,
71
+ });
72
+
73
+ const payload = await response.json().catch(() => ({}));
74
+ if (!response.ok) {
75
+ throw new Error(payload.error || payload.message || 'Management request failed');
76
+ }
77
+
78
+ return (payload.data ?? payload) as T;
79
+ }
80
+
81
+ export function getConsoleCliAuthUrl(state: string): string {
82
+ return `${CONSOLE_BASE_URL}/cli-auth?state=${encodeURIComponent(state)}`;
83
+ }
package/src/lib/sdk.ts CHANGED
@@ -2,8 +2,7 @@ import { Globio } from '@globio/sdk';
2
2
  import { config } from './config.js';
3
3
 
4
4
  export function getClient(): Globio {
5
- const apiKey = config.requireAuth();
6
- config.requireProject();
5
+ const apiKey = config.requireProjectApiKey();
7
6
  return new Globio({ apiKey });
8
7
  }
9
8
 
@@ -3,18 +3,6 @@ import * as p from '@clack/prompts';
3
3
  export async function promptInit() {
4
4
  return p.group(
5
5
  {
6
- apiKey: () =>
7
- p.text({
8
- message: 'Globio API key',
9
- placeholder: 'gk_live_...',
10
- validate: (value) => (!value ? 'Required' : undefined),
11
- }),
12
- projectId: () =>
13
- p.text({
14
- message: 'Project ID',
15
- placeholder: 'proj_...',
16
- validate: (value) => (!value ? 'Required' : undefined),
17
- }),
18
6
  migrateFromFirebase: () =>
19
7
  p.confirm({
20
8
  message: 'Migrating from Firebase?',