@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 +338 -91
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/auth/login.ts +138 -55
- package/src/auth/whoami.ts +7 -3
- package/src/commands/init.ts +14 -8
- package/src/commands/projects.ts +145 -7
- package/src/index.ts +17 -5
- package/src/lib/config.ts +51 -7
- package/src/lib/manage.ts +83 -0
- package/src/lib/sdk.ts +1 -2
- package/src/prompts/init.ts +0 -12
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
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
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
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
184
|
+
spinner2.start("Validating personal access token...");
|
|
108
185
|
try {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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.
|
|
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("
|
|
152
|
-
console.log(chalk4.cyan("
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
373
|
-
|
|
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 {
|
|
630
|
+
`import { Globio } from '@globio/sdk';
|
|
380
631
|
|
|
381
|
-
export const globio = new
|
|
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=${
|
|
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
|
-
|
|
411
|
-
orange("\u21D2\u21D2") + " Your project is ready.\n\n " + muted("Next steps:") +
|
|
412
|
-
);
|
|
413
|
-
}
|
|
661
|
+
p6.outro(
|
|
662
|
+
orange("\u21D2\u21D2") + " Your project is ready.\n\n " + muted("Next steps:") + `
|
|
414
663
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
641
|
-
|
|
881
|
+
async function main() {
|
|
882
|
+
if (process.argv.length <= 2) {
|
|
883
|
+
program.help();
|
|
884
|
+
}
|
|
885
|
+
await program.parseAsync();
|
|
642
886
|
}
|
|
643
|
-
|
|
887
|
+
main().catch((error) => {
|
|
888
|
+
console.error(error instanceof Error ? error.message : error);
|
|
889
|
+
process.exit(1);
|
|
890
|
+
});
|
package/jsr.json
CHANGED
package/package.json
CHANGED
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
}
|
package/src/auth/whoami.ts
CHANGED
|
@@ -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.
|
|
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('
|
|
13
|
-
console.log(chalk.cyan('
|
|
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
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -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
|
|
21
|
+
const cfg = config.get();
|
|
22
|
+
if (!cfg.projectId) {
|
|
23
|
+
await projectsCreate();
|
|
24
|
+
} else {
|
|
25
|
+
await projectsUse(cfg.projectId);
|
|
26
|
+
}
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 {
|
|
35
|
+
`import { Globio } from '@globio/sdk';
|
|
31
36
|
|
|
32
|
-
export const globio = new
|
|
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=${
|
|
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
|
}
|
package/src/commands/projects.ts
CHANGED
|
@@ -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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
102
|
+
async function main() {
|
|
103
|
+
if (process.argv.length <= 2) {
|
|
104
|
+
program.help();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await program.parseAsync();
|
|
99
108
|
}
|
|
100
109
|
|
|
101
|
-
|
|
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
|
-
|
|
5
|
+
pat?: string;
|
|
6
|
+
accountEmail?: string;
|
|
7
|
+
accountName?: string;
|
|
6
8
|
projectId?: string;
|
|
7
9
|
projectName?: string;
|
|
8
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
if (!
|
|
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
|
|
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.
|
|
6
|
-
config.requireProject();
|
|
5
|
+
const apiKey = config.requireProjectApiKey();
|
|
7
6
|
return new Globio({ apiKey });
|
|
8
7
|
}
|
|
9
8
|
|
package/src/prompts/init.ts
CHANGED
|
@@ -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?',
|