@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.
@@ -1,16 +1,181 @@
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
+ function resolveProfileName(profileName?: string) {
21
+ return profileName ?? config.getActiveProfile() ?? 'default';
22
+ }
23
+
24
+ async function createServerKey(projectId: string, profileName: string) {
25
+ const created = await manageRequest<ManageProjectKey>(`/projects/${projectId}/keys`, {
26
+ method: 'POST',
27
+ body: {
28
+ name: 'CLI server key',
29
+ scope: 'server',
30
+ },
31
+ profileName,
32
+ });
33
+
34
+ if (!created.token) {
35
+ throw new Error('Management API did not return a project API key');
36
+ }
37
+
38
+ return created.token;
39
+ }
40
+
41
+ export async function projectsList(options: { profile?: string } = {}) {
42
+ const profileName = resolveProfileName(options.profile);
43
+ config.requireAuth(profileName);
44
+
45
+ const projects = await manageRequest<ManageProject[]>('/projects', { profileName });
46
+ const activeProjectId = config.getProfile(profileName)?.active_project_id;
47
+ const grouped = new Map<string, ManageProject[]>();
48
+
49
+ for (const project of projects) {
50
+ const list = grouped.get(project.org_name) ?? [];
51
+ list.push(project);
52
+ grouped.set(project.org_name, list);
53
+ }
3
54
 
4
- export async function projectsList() {
5
- const cfg = config.get();
6
55
  console.log('');
56
+ if (!projects.length) {
57
+ console.log(chalk.gray('No projects found.'));
58
+ console.log('');
59
+ return;
60
+ }
61
+
62
+ for (const [orgName, orgProjects] of grouped.entries()) {
63
+ console.log(chalk.cyan(`org: ${orgName}`));
64
+ for (const project of orgProjects) {
65
+ const marker = project.id === activeProjectId ? chalk.green('●') : chalk.gray('○');
66
+ const active = project.id === activeProjectId ? chalk.green(' (active)') : '';
67
+ console.log(` ${marker} ${project.slug.padEnd(22)} ${chalk.gray(project.id)}${active}`);
68
+ }
69
+ console.log('');
70
+ }
71
+ }
72
+
73
+ export async function projectsUse(projectId: string, options: { profile?: string } = {}) {
74
+ const profileName = resolveProfileName(options.profile);
75
+ config.requireAuth(profileName);
76
+
77
+ const projects = await manageRequest<ManageProject[]>('/projects', { profileName });
78
+ const project = projects.find((item) => item.id === projectId);
79
+ if (!project) {
80
+ console.log(chalk.red(`Project not found: ${projectId}`));
81
+ process.exit(1);
82
+ }
83
+
84
+ await manageRequest<ManageProjectKey[]>(`/projects/${projectId}/keys`, { profileName });
85
+ const apiKey = await createServerKey(projectId, profileName);
86
+
87
+ config.setProfile(profileName, {
88
+ active_project_id: project.id,
89
+ active_project_name: project.name,
90
+ project_api_key: apiKey,
91
+ });
92
+ config.setActiveProfile(profileName);
93
+
7
94
  console.log(
8
- chalk.cyan('Active project: ') + (cfg.projectId ?? chalk.gray('none'))
95
+ chalk.green('Active project set to: ') + chalk.cyan(`${project.name} (${project.id})`)
9
96
  );
10
- console.log('');
11
97
  }
12
98
 
13
- export async function projectsUse(projectId: string) {
14
- config.set({ projectId });
15
- console.log(chalk.green('Active project set to: ') + chalk.cyan(projectId));
99
+ export async function projectsCreate(options: { profile?: string } = {}) {
100
+ const profileName = resolveProfileName(options.profile);
101
+ config.requireAuth(profileName);
102
+
103
+ const orgs = await manageRequest<ManageOrg[]>('/orgs', { profileName });
104
+ if (!orgs.length) {
105
+ console.log(chalk.red('No organizations found. Create one in the console first.'));
106
+ process.exit(1);
107
+ }
108
+
109
+ const orgId = await p.select({
110
+ message: 'Select an organization',
111
+ options: orgs.map((org) => ({
112
+ value: org.id,
113
+ label: org.name,
114
+ hint: org.role,
115
+ })),
116
+ });
117
+
118
+ if (p.isCancel(orgId)) {
119
+ p.cancel('Project creation cancelled.');
120
+ process.exit(0);
121
+ }
122
+
123
+ const values = await p.group(
124
+ {
125
+ name: () =>
126
+ p.text({
127
+ message: 'Project name',
128
+ validate: (value) => (!value ? 'Project name is required' : undefined),
129
+ }),
130
+ slug: ({ results }) =>
131
+ p.text({
132
+ message: 'Project slug',
133
+ initialValue: slugify(String(results.name ?? '')),
134
+ validate: (value) => (!value ? 'Project slug is required' : undefined),
135
+ }),
136
+ environment: () =>
137
+ p.select({
138
+ message: 'Environment',
139
+ options: [
140
+ { value: 'development', label: 'development' },
141
+ { value: 'staging', label: 'staging' },
142
+ { value: 'production', label: 'production' },
143
+ ],
144
+ }),
145
+ },
146
+ {
147
+ onCancel: () => {
148
+ p.cancel('Project creation cancelled.');
149
+ process.exit(0);
150
+ },
151
+ }
152
+ );
153
+
154
+ const result = await manageRequest<{
155
+ project: { id: string; name: string; slug: string; environment: string; active: boolean };
156
+ keys: { client: string; server: string };
157
+ }>('/projects', {
158
+ method: 'POST',
159
+ body: {
160
+ org_id: orgId,
161
+ name: values.name,
162
+ slug: values.slug,
163
+ environment: values.environment,
164
+ },
165
+ profileName,
166
+ });
167
+
168
+ config.setProfile(profileName, {
169
+ active_project_id: result.project.id,
170
+ active_project_name: result.project.name,
171
+ project_api_key: result.keys.server,
172
+ });
173
+ config.setActiveProfile(profileName);
174
+
175
+ console.log('');
176
+ console.log(chalk.green('Project created successfully.'));
177
+ console.log(chalk.cyan('Project: ') + `${result.project.name} (${result.project.id})`);
178
+ console.log(chalk.cyan('Client key: ') + result.keys.client);
179
+ console.log(chalk.cyan('Server key: ') + result.keys.server);
180
+ console.log('');
16
181
  }
@@ -1,4 +1,5 @@
1
1
  import chalk from 'chalk';
2
+ import { config } from '../lib/config.js';
2
3
 
3
4
  const ALL_SERVICES = [
4
5
  'id',
@@ -13,7 +14,9 @@ const ALL_SERVICES = [
13
14
  'code',
14
15
  ];
15
16
 
16
- export async function servicesList() {
17
+ export async function servicesList(options: { profile?: string } = {}) {
18
+ void options.profile;
19
+ void config;
17
20
  console.log('');
18
21
  console.log(chalk.cyan('Available Globio services:'));
19
22
  ALL_SERVICES.forEach((service) => {
package/src/index.ts CHANGED
@@ -2,9 +2,10 @@
2
2
  import { Command } from 'commander';
3
3
  import { login } from './auth/login.js';
4
4
  import { logout } from './auth/logout.js';
5
+ import { useProfile } from './auth/useProfile.js';
5
6
  import { whoami } from './auth/whoami.js';
6
7
  import { init } from './commands/init.js';
7
- import { projectsList, projectsUse } from './commands/projects.js';
8
+ import { projectsCreate, projectsList, projectsUse } from './commands/projects.js';
8
9
  import { servicesList } from './commands/services.js';
9
10
  import {
10
11
  functionsList,
@@ -32,46 +33,79 @@ program
32
33
  .addHelpText('beforeAll', () => {
33
34
  printBanner(version);
34
35
  return '';
35
- });
36
+ })
37
+ .addHelpText(
38
+ 'after',
39
+ `
40
+ Examples:
41
+ $ globio login
42
+ $ globio login --profile work
43
+ $ globio use work
44
+ $ globio projects list
45
+ $ globio projects use proj_abc123
46
+ $ globio functions deploy my-function
47
+ $ globio migrate firestore --from ./key.json --all
36
48
 
37
- program.command('login').description('Log in to your Globio account').action(login);
38
- program.command('logout').description('Log out').action(logout);
39
- program.command('whoami').description('Show current account and project').action(whoami);
49
+ Credentials are stored in ~/.globio/profiles/
50
+ `
51
+ );
40
52
 
41
- program.command('init').description('Initialize a Globio project').action(init);
53
+ program
54
+ .command('login')
55
+ .description('Log in to your Globio account')
56
+ .option('-p, --profile <name>', 'Profile name', 'default')
57
+ .option('--token', 'Use a personal access token')
58
+ .action(login);
59
+ program.command('logout').description('Log out').option('--profile <name>', 'Use a specific profile').action(logout);
60
+ program.command('whoami').description('Show current account and project').option('--profile <name>', 'Use a specific profile').action(whoami);
61
+ program.command('use <profile>').description('Switch active profile').action(useProfile);
62
+
63
+ program.command('init').description('Initialize a Globio project').option('--profile <name>', 'Use a specific profile').action(init);
42
64
 
43
65
  const projects = program.command('projects').description('Manage projects');
44
- projects.command('list').description('List projects').action(projectsList);
45
- projects.command('use <projectId>').description('Set active project').action(projectsUse);
66
+ projects.command('list').description('List projects').option('--profile <name>', 'Use a specific profile').action(projectsList);
67
+ projects.command('create').description('Create a project').option('--profile <name>', 'Use a specific profile').action(projectsCreate);
68
+ projects.command('use <projectId>').description('Set active project').option('--profile <name>', 'Use a specific profile').action(projectsUse);
46
69
 
47
- program.command('services').description('List available Globio services').action(servicesList);
70
+ program.command('services').description('List available Globio services').option('--profile <name>', 'Use a specific profile').action(servicesList);
48
71
 
49
72
  const functions = program
50
73
  .command('functions')
51
74
  .alias('fn')
52
75
  .description('Manage GlobalCode edge functions');
53
76
 
54
- functions.command('list').description('List all functions').action(functionsList);
55
- functions.command('create <slug>').description('Scaffold a new function file locally').action(functionsCreate);
77
+ functions.command('list').description('List all functions').option('--profile <name>', 'Use a specific profile').action(functionsList);
78
+ functions.command('create <slug>').description('Scaffold a new function file locally').option('--profile <name>', 'Use a specific profile').action(functionsCreate);
56
79
  functions
57
80
  .command('deploy <slug>')
58
81
  .description('Deploy a function to GlobalCode')
59
82
  .option('-f, --file <path>', 'Path to function file')
60
83
  .option('-n, --name <name>', 'Display name')
84
+ .option('--profile <name>', 'Use a specific profile')
61
85
  .action(functionsDeploy);
62
86
  functions
63
87
  .command('invoke <slug>')
64
88
  .description('Invoke a function')
65
89
  .option('-i, --input <json>', 'JSON input payload')
90
+ .option('--profile <name>', 'Use a specific profile')
66
91
  .action(functionsInvoke);
67
92
  functions
68
93
  .command('logs <slug>')
69
94
  .description('Show invocation history')
70
95
  .option('-l, --limit <n>', 'Number of entries', '20')
96
+ .option('--profile <name>', 'Use a specific profile')
71
97
  .action(functionsLogs);
72
- functions.command('delete <slug>').description('Delete a function').action(functionsDelete);
73
- functions.command('enable <slug>').description('Enable a function').action((slug) => functionsToggle(slug, true));
74
- functions.command('disable <slug>').description('Disable a function').action((slug) => functionsToggle(slug, false));
98
+ functions.command('delete <slug>').description('Delete a function').option('--profile <name>', 'Use a specific profile').action(functionsDelete);
99
+ functions
100
+ .command('enable <slug>')
101
+ .description('Enable a function')
102
+ .option('--profile <name>', 'Use a specific profile')
103
+ .action((slug, options) => functionsToggle(slug, true, options));
104
+ functions
105
+ .command('disable <slug>')
106
+ .description('Disable a function')
107
+ .option('--profile <name>', 'Use a specific profile')
108
+ .action((slug, options) => functionsToggle(slug, false, options));
75
109
 
76
110
  const migrate = program
77
111
  .command('migrate')
@@ -83,6 +117,7 @@ migrate
83
117
  .requiredOption('--from <path>', 'Path to Firebase service account JSON')
84
118
  .option('--collection <name>', 'Migrate a specific collection')
85
119
  .option('--all', 'Migrate all collections')
120
+ .option('--profile <name>', 'Use a specific profile')
86
121
  .action(migrateFirestore);
87
122
 
88
123
  migrate
@@ -92,10 +127,18 @@ migrate
92
127
  .requiredOption('--bucket <name>', 'Firebase Storage bucket')
93
128
  .option('--folder <path>', 'Migrate a specific folder')
94
129
  .option('--all', 'Migrate all files')
130
+ .option('--profile <name>', 'Use a specific profile')
95
131
  .action(migrateFirebaseStorage);
96
132
 
97
- if (process.argv.length <= 2) {
98
- program.help();
133
+ async function main() {
134
+ if (process.argv.length <= 2) {
135
+ program.help();
136
+ }
137
+
138
+ await program.parseAsync();
99
139
  }
100
140
 
101
- await program.parseAsync();
141
+ main().catch((error) => {
142
+ console.error(error instanceof Error ? error.message : error);
143
+ process.exit(1);
144
+ });
package/src/lib/config.ts CHANGED
@@ -1,47 +1,139 @@
1
1
  import chalk from 'chalk';
2
- import Conf from 'conf';
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
3
5
 
4
- interface GlobioConfig {
5
- apiKey?: string;
6
- projectId?: string;
7
- projectName?: string;
8
- email?: string;
6
+ interface GlobalConfig {
7
+ active_profile: string;
9
8
  }
10
9
 
11
- const store = new Conf<GlobioConfig>({
12
- projectName: 'globio',
13
- defaults: {},
14
- });
10
+ export interface ProfileData {
11
+ pat: string;
12
+ account_email: string;
13
+ account_name: string;
14
+ active_project_id?: string;
15
+ active_project_name?: string;
16
+ project_api_key?: string;
17
+ created_at: number;
18
+ }
19
+
20
+ const baseDir = path.join(os.homedir(), '.globio');
21
+ const profilesDir = path.join(baseDir, 'profiles');
22
+ const configPath = path.join(baseDir, 'config.json');
23
+
24
+ function ensureBaseDir() {
25
+ mkdirSync(baseDir, { recursive: true });
26
+ }
27
+
28
+ function ensureProfilesDir() {
29
+ ensureBaseDir();
30
+ mkdirSync(profilesDir, { recursive: true });
31
+ }
32
+
33
+ function readGlobalConfig(): GlobalConfig {
34
+ if (!existsSync(configPath)) {
35
+ return { active_profile: 'default' };
36
+ }
37
+
38
+ try {
39
+ const raw = JSON.parse(readFileSync(configPath, 'utf-8')) as Partial<GlobalConfig>;
40
+ return {
41
+ active_profile: raw.active_profile ?? 'default',
42
+ };
43
+ } catch {
44
+ return { active_profile: 'default' };
45
+ }
46
+ }
47
+
48
+ function writeGlobalConfig(data: GlobalConfig) {
49
+ ensureBaseDir();
50
+ writeFileSync(configPath, JSON.stringify(data, null, 2) + '\n');
51
+ }
52
+
53
+ function profilePath(name: string) {
54
+ return path.join(profilesDir, `${name}.json`);
55
+ }
56
+
57
+ function readProfile(name: string): ProfileData | null {
58
+ const file = profilePath(name);
59
+ if (!existsSync(file)) return null;
60
+
61
+ try {
62
+ return JSON.parse(readFileSync(file, 'utf-8')) as ProfileData;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function writeProfile(name: string, data: ProfileData) {
69
+ ensureProfilesDir();
70
+ writeFileSync(profilePath(name), JSON.stringify(data, null, 2) + '\n');
71
+ }
15
72
 
16
73
  export const config = {
17
- get: (): GlobioConfig => store.store,
18
- set: (values: Partial<GlobioConfig>) => {
19
- Object.entries(values).forEach(([key, value]) => {
20
- if (value !== undefined) {
21
- store.set(key as keyof GlobioConfig, value);
22
- }
23
- });
24
- },
25
- clear: () => store.clear(),
26
- getApiKey: () => store.get('apiKey'),
27
- requireAuth: () => {
28
- const key = store.get('apiKey');
29
- if (!key) {
74
+ getBaseDir: () => baseDir,
75
+ getProfilesDir: () => profilesDir,
76
+ getActiveProfile: (): string => readGlobalConfig().active_profile,
77
+ setActiveProfile: (name: string): void => {
78
+ writeGlobalConfig({ active_profile: name });
79
+ },
80
+ getProfile: (name?: string): ProfileData | null => {
81
+ const profileName = name ?? config.getActiveProfile();
82
+ if (!profileName) return null;
83
+ return readProfile(profileName);
84
+ },
85
+ setProfile: (name: string, data: Partial<ProfileData>): void => {
86
+ const existing = readProfile(name);
87
+ const next: ProfileData = {
88
+ pat: data.pat ?? existing?.pat ?? '',
89
+ account_email: data.account_email ?? existing?.account_email ?? '',
90
+ account_name: data.account_name ?? existing?.account_name ?? '',
91
+ active_project_id: data.active_project_id ?? existing?.active_project_id,
92
+ active_project_name: data.active_project_name ?? existing?.active_project_name,
93
+ project_api_key: data.project_api_key ?? existing?.project_api_key,
94
+ created_at: data.created_at ?? existing?.created_at ?? Date.now(),
95
+ };
96
+ writeProfile(name, next);
97
+ },
98
+ deleteProfile: (name: string): void => {
99
+ const file = profilePath(name);
100
+ if (existsSync(file)) {
101
+ rmSync(file);
102
+ }
103
+ },
104
+ listProfiles: (): string[] => {
105
+ if (!existsSync(profilesDir)) return [];
106
+ return readdirSync(profilesDir)
107
+ .filter((file) => file.endsWith('.json'))
108
+ .map((file) => file.replace(/\.json$/, ''))
109
+ .sort();
110
+ },
111
+ getActiveProfileData: (): ProfileData | null => {
112
+ const active = config.getActiveProfile();
113
+ if (!active) return null;
114
+ return config.getProfile(active);
115
+ },
116
+ requireAuth: (profileName?: string): { pat: string; profileName: string } => {
117
+ const resolvedProfile = profileName ?? config.getActiveProfile() ?? 'default';
118
+ const profile = config.getProfile(resolvedProfile);
119
+ if (!profile?.pat) {
30
120
  console.error(chalk.red('Not logged in. Run: npx @globio/cli login'));
31
121
  process.exit(1);
32
122
  }
33
- return key;
123
+ return { pat: profile.pat, profileName: resolvedProfile };
34
124
  },
35
- requireProject: () => {
36
- const projectId = store.get('projectId');
37
- if (!projectId) {
125
+ requireProject: (profileName?: string): { projectId: string; projectName: string } => {
126
+ const resolvedProfile = profileName ?? config.getActiveProfile() ?? 'default';
127
+ const profile = config.getProfile(resolvedProfile);
128
+ if (!profile?.active_project_id) {
38
129
  console.error(
39
130
  chalk.red('No active project. Run: npx @globio/cli projects use <projectId>')
40
131
  );
41
132
  process.exit(1);
42
133
  }
43
- return projectId;
134
+ return {
135
+ projectId: profile.active_project_id,
136
+ projectName: profile.active_project_name ?? 'unnamed',
137
+ };
44
138
  },
45
139
  };
46
-
47
- export type { GlobioConfig };
@@ -0,0 +1,84 @@
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
+ profileName?: string;
11
+ }
12
+
13
+ export interface ManageAccount {
14
+ id: string;
15
+ email: string;
16
+ display_name: string | null;
17
+ avatar_url: string | null;
18
+ created_at: number;
19
+ }
20
+
21
+ export interface ManageOrg {
22
+ id: string;
23
+ name: string;
24
+ slug: string;
25
+ role: 'owner' | 'admin' | 'developer' | 'viewer';
26
+ created_at: number;
27
+ }
28
+
29
+ export interface ManageProject {
30
+ id: string;
31
+ name: string;
32
+ slug: string;
33
+ org_id: string;
34
+ org_name: string;
35
+ environment: string;
36
+ active: boolean;
37
+ }
38
+
39
+ export interface ManageProjectKey {
40
+ id: string;
41
+ name: string;
42
+ key_prefix: string;
43
+ scope: string;
44
+ created_at: number;
45
+ last_used_at: number | null;
46
+ token?: string;
47
+ }
48
+
49
+ function getAuthToken(explicitToken?: string, profileName?: string): string | undefined {
50
+ if (explicitToken) return explicitToken;
51
+ return config.getProfile(profileName)?.pat;
52
+ }
53
+
54
+ export async function manageRequest<T>(
55
+ path: string,
56
+ options: ManageRequestOptions = {}
57
+ ): Promise<T> {
58
+ const headers = new Headers();
59
+ if (!(options.body instanceof FormData)) {
60
+ headers.set('Content-Type', 'application/json');
61
+ }
62
+
63
+ const token = getAuthToken(options.token, options.profileName);
64
+ if (token) {
65
+ headers.set('Authorization', `Bearer ${token}`);
66
+ }
67
+
68
+ const response = await fetch(`${API_BASE_URL}/manage${path}`, {
69
+ method: options.method ?? 'GET',
70
+ headers,
71
+ body: options.body ? JSON.stringify(options.body) : undefined,
72
+ });
73
+
74
+ const payload = await response.json().catch(() => ({}));
75
+ if (!response.ok) {
76
+ throw new Error(payload.error || payload.message || 'Management request failed');
77
+ }
78
+
79
+ return (payload.data ?? payload) as T;
80
+ }
81
+
82
+ export function getConsoleCliAuthUrl(state: string): string {
83
+ return `${CONSOLE_BASE_URL}/cli-auth?state=${encodeURIComponent(state)}`;
84
+ }
package/src/lib/sdk.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  import { Globio } from '@globio/sdk';
2
2
  import { config } from './config.js';
3
3
 
4
- export function getClient(): Globio {
5
- const apiKey = config.requireAuth();
6
- config.requireProject();
4
+ export function getClient(profileName?: string): Globio {
5
+ const { pat } = config.requireAuth(profileName);
6
+ const { projectId } = config.requireProject(profileName);
7
+ const profile = config.getProfile(profileName);
8
+ const apiKey = profile?.project_api_key ?? pat;
9
+ void projectId;
7
10
  return new Globio({ apiKey });
8
11
  }
9
12
 
@@ -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?',