@globio/cli 0.2.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,12 @@
1
1
  # @globio/cli
2
2
 
3
- The official CLI for [Globio](https://globio.stanlink.online)
4
- game backend as a service built on Cloudflare Workers.
3
+ The official CLI for [Globio](https://globio.stanlink.online)
4
+ game backend as a service built on Cloudflare Workers.
5
+
6
+ ## Requirements
7
+
8
+ - Node.js 18+
9
+ - A Globio account — [console.globio.stanlink.online](https://console.globio.stanlink.online)
5
10
 
6
11
  ## Install
7
12
 
@@ -16,81 +21,280 @@ npm install -g @globio/cli
16
21
  ## Quick Start
17
22
 
18
23
  ```bash
19
- npx @globio/cli login
20
- npx @globio/cli init
24
+ # Log in to your Globio account
25
+ globio login
26
+
27
+ # Initialize a new project
28
+ globio init
29
+
30
+ # Or non-interactively
31
+ globio init --name "My Game" --org org_xxx --json
21
32
  ```
22
33
 
23
- ## Migrate from Firebase
34
+ ---
35
+
36
+ ## Authentication
24
37
 
38
+ Globio CLI authenticates at account level using a Personal
39
+ Access Token (PAT). Two login methods are supported.
40
+
41
+ **Browser flow** — opens the Globio console for one-click approval:
25
42
  ```bash
26
- # Migrate all Firestore collections to GlobalDoc
27
- npx @globio/cli migrate firestore \
28
- --from ./serviceAccountKey.json \
29
- --all
43
+ globio login
44
+ ```
30
45
 
31
- # Migrate Firebase Storage to GlobalVault
32
- npx @globio/cli migrate firebase-storage \
33
- --from ./serviceAccountKey.json \
34
- --bucket gs://my-game.appspot.com \
35
- --all
46
+ **Token flow** paste a PAT from console settings:
47
+ ```bash
48
+ globio login --token glo_pat_xxxxx
36
49
  ```
37
50
 
38
- After migration, GlobalDoc indexes are created automatically for every field in your collections.
39
- Queries using `where()` clauses will work immediately.
51
+ **Named profiles** manage multiple accounts:
52
+ ```bash
53
+ globio login --profile work
54
+ globio login --profile personal
55
+ globio use work
56
+ ```
40
57
 
41
- Note: GlobalDoc requires explicit indexes unlike Firestore's automatic indexing.
42
- The migrate command handles this for you automatically.
58
+ Credentials are stored in `~/.globio/profiles/`
59
+
60
+ ---
43
61
 
44
62
  ## Commands
45
63
 
46
64
  ### Auth
65
+
47
66
  ```bash
48
- globio login # authenticate
67
+ globio login # browser or token flow
68
+ globio login --token <pat> # non-interactive
69
+ globio login --profile <name> # named profile
49
70
  globio logout
50
- globio whoami # show active account and project
71
+ globio logout --profile <name>
72
+ globio whoami
73
+ globio whoami --json
74
+ ```
75
+
76
+ ### Profiles
77
+
78
+ ```bash
79
+ globio profiles list
80
+ globio profiles list --json
81
+ globio use <profile> # switch active profile
51
82
  ```
52
83
 
53
84
  ### Projects
85
+
54
86
  ```bash
55
87
  globio projects list
88
+ globio projects list --json
56
89
  globio projects use <projectId>
90
+ globio projects create # interactive
91
+ globio projects create \
92
+ --name "My Game" \
93
+ --org <orgId> \
94
+ --json # non-interactive
57
95
  ```
58
96
 
59
97
  ### Services
98
+
60
99
  ```bash
61
100
  globio services list
101
+ globio services list --json
62
102
  ```
63
103
 
64
- ### Functions (GlobalCode)
104
+ ### Edge Functions (GlobalCode)
105
+
65
106
  ```bash
66
107
  globio functions list
67
- globio functions create <slug> # scaffold locally
68
- globio functions deploy <slug> # deploy to Globio
108
+ globio functions list --json
109
+ globio functions create <slug> # scaffold locally
110
+ globio functions deploy <slug> # deploy to Globio
69
111
  globio functions invoke <slug> --input '{"key":"value"}'
112
+ globio functions invoke <slug> --input '{"key":"value"}' --json
70
113
  globio functions logs <slug>
114
+ globio functions logs <slug> --json
115
+ globio functions watch <slug> # live log streaming
71
116
  globio functions enable <slug>
72
117
  globio functions disable <slug>
73
118
  globio functions delete <slug>
74
119
  ```
75
120
 
121
+ ### GC Hooks
122
+
123
+ GC Hooks fire automatically when events occur in your
124
+ Globio project. They cannot be invoked manually.
125
+
126
+ ```bash
127
+ globio hooks list
128
+ globio hooks list --json
129
+ globio hooks create <slug> # scaffold locally
130
+ globio hooks deploy <slug> \
131
+ --trigger id.onSignup # deploy with trigger
132
+ globio hooks logs <slug>
133
+ globio hooks watch <slug> # live log streaming
134
+ globio hooks enable <slug>
135
+ globio hooks disable <slug>
136
+ globio hooks delete <slug>
137
+ ```
138
+
139
+ **Available hook triggers:**
140
+
141
+ | Trigger | Fires when |
142
+ |---|---|
143
+ | `id.onSignup` | New user registers |
144
+ | `id.onSignin` | User signs in |
145
+ | `id.onSignout` | User signs out |
146
+ | `id.onPasswordReset` | Password reset completed |
147
+ | `doc.onCreate` | Document created |
148
+ | `doc.onUpdate` | Document updated |
149
+ | `doc.onDelete` | Document deleted |
150
+ | `mart.onPurchase` | In-game currency purchase |
151
+ | `mart.onPayment` | Fiat payment completed |
152
+ | `sync.onRoomCreate` | Game room created |
153
+ | `sync.onRoomClose` | Game room closed |
154
+ | `sync.onPlayerJoin` | Player joins a room |
155
+ | `sync.onPlayerLeave` | Player leaves a room |
156
+ | `vault.onUpload` | File uploaded |
157
+ | `vault.onDelete` | File deleted |
158
+ | `signal.onDeliver` | Notification delivered |
159
+
160
+ **Example hook:**
161
+ ```javascript
162
+ // init-player.hook.js
163
+ async function handler({ userId, email }, globio) {
164
+ await globio.doc.set('players', userId, {
165
+ level: 1, xp: 0, coins: 100
166
+ });
167
+ await globio.signal.sendToUser(userId, {
168
+ title: 'Welcome!',
169
+ body: 'Your adventure begins.',
170
+ priority: 'high'
171
+ });
172
+ }
173
+ ```
174
+
175
+ ```bash
176
+ globio hooks deploy init-player --trigger id.onSignup
177
+ ```
178
+
76
179
  ### Migrate from Firebase
180
+
181
+ Migrate Firestore collections and Firebase Storage
182
+ to Globio in one command. Non-destructive — your
183
+ Firebase data stays intact until you delete it manually.
184
+
185
+ GlobalDoc indexes are created automatically for every
186
+ field during migration. Queries work immediately after.
187
+
77
188
  ```bash
189
+ # Migrate a single Firestore collection
78
190
  globio migrate firestore \
79
191
  --from ./serviceAccountKey.json \
80
192
  --collection players
81
193
 
194
+ # Migrate all Firestore collections
82
195
  globio migrate firestore \
83
196
  --from ./serviceAccountKey.json \
84
197
  --all
85
198
 
199
+ # Migrate Firebase Storage
86
200
  globio migrate firebase-storage \
87
201
  --from ./serviceAccountKey.json \
88
202
  --bucket gs://my-game.appspot.com \
89
203
  --all
204
+
205
+ # Migrate a specific folder
206
+ globio migrate firebase-storage \
207
+ --from ./serviceAccountKey.json \
208
+ --bucket gs://my-game.appspot.com \
209
+ --folder /avatars
90
210
  ```
91
211
 
92
- ## Built with Globio
212
+ ---
213
+
214
+ ## JSON Output and CI/CD
215
+
216
+ Every command supports `--json` for machine-readable output.
217
+ Use this in CI/CD pipelines, scripts, and AI agents.
218
+
219
+ ```bash
220
+ globio whoami --json
221
+ globio projects list --json
222
+ globio functions list --json
223
+ globio functions invoke <slug> --input '{}' --json
224
+ globio hooks list --json
225
+ globio services list --json
226
+ ```
227
+
228
+ Combined with non-interactive flags for full automation:
229
+
230
+ ```bash
231
+ # Full CI/CD setup — no prompts
232
+ globio login --token $GLOBIO_PAT --profile ci --json
233
+ globio projects create \
234
+ --name "My Game" \
235
+ --org $ORG_ID \
236
+ --json
237
+ globio functions deploy my-function --json
238
+ ```
239
+
240
+ ---
241
+
242
+ ## Live Log Streaming
243
+
244
+ Stream real-time function and hook execution logs
245
+ to your terminal — including console.log output,
246
+ inputs, results, and errors.
247
+
248
+ ```bash
249
+ globio functions watch matchmaking
250
+ globio hooks watch init-player
251
+ ```
252
+
253
+ Example output:
254
+ ```
255
+ ⇒⇒ globio 1.0.0
256
+ ──────────────────────────────────────────
257
+ watching matchmaking · press Ctrl+C to stop
258
+
259
+ ● connected — waiting for invocations...
260
+
261
+ ✓ 2026-03-15 06:12:01 [http] 3ms
262
+ input {"userId":"player_001","rating":1450}
263
+ log Querying players with rating 1450
264
+ log Found 3 candidates
265
+ result {"matched":true,"roomId":"room_abc"}
266
+ ```
267
+
268
+ ---
269
+
270
+ ## globio.config.ts
271
+
272
+ Running `globio init` creates a `globio.config.ts` in
273
+ your project root with your project already configured:
274
+
275
+ ```typescript
276
+ import { Globio } from '@globio/sdk';
277
+
278
+ export const globio = new Globio({
279
+ apiKey: process.env.GLOBIO_API_KEY!,
280
+ projectId: 'proj_xxxxx',
281
+ });
282
+ ```
283
+
284
+ Import it anywhere in your project:
285
+
286
+ ```typescript
287
+ import { globio } from './globio.config';
288
+
289
+ const player = await globio.doc.get('players', userId);
290
+ ```
291
+
292
+ ---
293
+
294
+ ## Links
93
295
 
94
- - [SDK](https://npmjs.com/package/@globio/sdk)
296
+ - [SDK](https://npmjs.com/package/@globio/sdk) — `@globio/sdk`
95
297
  - [Console](https://console.globio.stanlink.online)
96
- - [Docs](https://globio.stanlink.online/docs)
298
+ - [Docs](https://docs.globio.stanl.ink)
299
+ - [Discord](https://discord.gg/bNDvAsVkMY)
300
+ - [GitHub](https://github.com/Globio-Technologies/globio-cli)
package/dist/index.js CHANGED
@@ -206,10 +206,10 @@ function renderTable(options) {
206
206
  );
207
207
  return lines.join("\n");
208
208
  }
209
- function header(version11, subtitle) {
209
+ function header(version12, subtitle) {
210
210
  const lines = [
211
211
  "",
212
- orange(" \u21D2\u21D2") + reset + " globio " + dim(version11),
212
+ orange(" \u21D2\u21D2") + reset + " globio " + dim(version12),
213
213
  dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")
214
214
  ];
215
215
  if (subtitle) {
@@ -234,14 +234,14 @@ var globioGradient = gradientString(
234
234
  "#ffba08",
235
235
  "#ffd000"
236
236
  );
237
- function printBanner(version11) {
237
+ function printBanner(version12) {
238
238
  const art = figlet.textSync("Globio", {
239
239
  font: "ANSI Shadow",
240
240
  horizontalLayout: "default"
241
241
  });
242
242
  console.log(globioGradient.multiline(art));
243
243
  console.log(
244
- globioGradient(" \u21D2\u21D2") + " Game Backend as a Service \x1B[2mv" + version11 + "\x1B[0m"
244
+ globioGradient(" \u21D2\u21D2") + " Game Backend as a Service \x1B[2mv" + version12 + "\x1B[0m"
245
245
  );
246
246
  console.log("");
247
247
  }
@@ -643,8 +643,8 @@ async function createIndex(collection, field, fieldType = "string", profile) {
643
643
  // src/lib/firebase.ts
644
644
  async function initFirebase(serviceAccountPath) {
645
645
  const admin = await import("firebase-admin");
646
- const { readFileSync: readFileSync5 } = await import("fs");
647
- const serviceAccount = JSON.parse(readFileSync5(serviceAccountPath, "utf-8"));
646
+ const { readFileSync: readFileSync6 } = await import("fs");
647
+ const serviceAccount = JSON.parse(readFileSync6(serviceAccountPath, "utf-8"));
648
648
  if (!admin.default.apps.length) {
649
649
  admin.default.initializeApp({
650
650
  credential: admin.default.credential.cert(serviceAccount),
@@ -1566,9 +1566,235 @@ async function functionsToggle(slug, active, options = {}) {
1566
1566
  spinner2?.succeed(`${slug} is now ${active ? "active" : "inactive"}`);
1567
1567
  }
1568
1568
 
1569
+ // src/commands/hooks.ts
1570
+ import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
1571
+ var version9 = getCliVersion();
1572
+ function parseJsonField2(value) {
1573
+ if (!value) return null;
1574
+ try {
1575
+ return JSON.parse(value);
1576
+ } catch {
1577
+ return null;
1578
+ }
1579
+ }
1580
+ var HOOK_TRIGGERS = [
1581
+ "id.onSignup",
1582
+ "id.onSignin",
1583
+ "id.onSignout",
1584
+ "id.onPasswordReset",
1585
+ "doc.onCreate",
1586
+ "doc.onUpdate",
1587
+ "doc.onDelete",
1588
+ "mart.onPurchase",
1589
+ "mart.onPayment",
1590
+ "sync.onRoomCreate",
1591
+ "sync.onRoomClose",
1592
+ "sync.onPlayerJoin",
1593
+ "sync.onPlayerLeave",
1594
+ "vault.onUpload",
1595
+ "vault.onDelete",
1596
+ "signal.onDeliver"
1597
+ ];
1598
+ async function hooksList(options = {}) {
1599
+ const client = getClient(options.profile);
1600
+ const result = await client.code.listHooks();
1601
+ if (options.json) {
1602
+ jsonOutput(
1603
+ result.success ? (result.data ?? []).map((hook) => ({
1604
+ slug: hook.slug,
1605
+ type: hook.type,
1606
+ trigger_event: hook.trigger_event,
1607
+ active: hook.active
1608
+ })) : []
1609
+ );
1610
+ }
1611
+ if (!result.success || !result.data?.length) {
1612
+ console.log(header(version9));
1613
+ console.log(" " + muted("No hooks found.") + "\n");
1614
+ return;
1615
+ }
1616
+ const rows = result.data.map((fn) => [
1617
+ gold(fn.slug),
1618
+ gold(fn.trigger_event ?? "\u2014"),
1619
+ fn.active ? green("active") : inactive("inactive")
1620
+ ]);
1621
+ console.log(header(version9));
1622
+ console.log(
1623
+ renderTable({
1624
+ columns: [
1625
+ { header: "Hook", width: 24 },
1626
+ { header: "Trigger", width: 24 },
1627
+ { header: "Status", width: 10 }
1628
+ ],
1629
+ rows
1630
+ })
1631
+ );
1632
+ console.log("");
1633
+ }
1634
+ async function hooksCreate(slug, options = {}) {
1635
+ const filename = `${slug}.hook.js`;
1636
+ if (existsSync4(filename)) {
1637
+ if (options.json) {
1638
+ jsonOutput({ success: false, file: filename, error: "File already exists" });
1639
+ }
1640
+ console.log(gold(filename) + reset + " already exists.");
1641
+ return;
1642
+ }
1643
+ const template = `/**
1644
+ * GC Hook: ${slug}
1645
+ * This hook fires automatically when its trigger event occurs.
1646
+ * You cannot invoke it manually.
1647
+ *
1648
+ * The handler receives the event payload and the injected
1649
+ * globio SDK \u2014 use it to orchestrate any Globio service.
1650
+ */
1651
+ async function handler(payload, globio) {
1652
+ // payload: event data from the trigger
1653
+ // globio: injected SDK \u2014 access all Globio services
1654
+
1655
+ // Example for id.onSignup:
1656
+ // const { userId, email } = payload;
1657
+ // await globio.doc.set('players', userId, {
1658
+ // level: 1, xp: 0, coins: 100
1659
+ // });
1660
+ }
1661
+ `;
1662
+ writeFileSync4(filename, template);
1663
+ if (options.json) {
1664
+ jsonOutput({ success: true, file: filename });
1665
+ }
1666
+ console.log(green("\u2713") + reset + " Created " + gold(filename) + reset);
1667
+ console.log(muted(" Deploy with: globio hooks deploy " + slug));
1668
+ }
1669
+ async function hooksDeploy(slug, options) {
1670
+ const filename = options.file ?? `${slug}.hook.js`;
1671
+ if (!existsSync4(filename)) {
1672
+ if (options.json) {
1673
+ jsonOutput({ success: false, error: `File not found: ${filename}` });
1674
+ }
1675
+ console.log(
1676
+ failure("File not found: " + filename) + reset + " Run: globio hooks create " + slug
1677
+ );
1678
+ process.exit(1);
1679
+ }
1680
+ if (!options.trigger) {
1681
+ if (options.json) {
1682
+ jsonOutput({ success: false, error: "--trigger required for hooks" });
1683
+ }
1684
+ console.log(
1685
+ failure("--trigger required for hooks.") + reset + "\n\n Available triggers:\n" + HOOK_TRIGGERS.map((trigger) => " " + gold(trigger) + reset).join("\n")
1686
+ );
1687
+ process.exit(1);
1688
+ }
1689
+ const code = readFileSync5(filename, "utf-8");
1690
+ const client = getClient(options.profile);
1691
+ const existing = await client.code.getFunction(slug).catch(() => null);
1692
+ let result;
1693
+ if (existing?.success) {
1694
+ result = await client.code.updateHook(slug, {
1695
+ code,
1696
+ trigger: options.trigger
1697
+ });
1698
+ if (options.json) {
1699
+ jsonOutput({ success: result.success, slug, action: "updated" });
1700
+ }
1701
+ if (!result.success) {
1702
+ console.log(failure("Deploy failed"));
1703
+ process.exit(1);
1704
+ }
1705
+ console.log(green("\u2713") + reset + " Updated hook " + gold(slug) + reset);
1706
+ return;
1707
+ }
1708
+ result = await client.code.createHook({
1709
+ name: options.name ?? slug,
1710
+ slug,
1711
+ trigger: options.trigger,
1712
+ code
1713
+ });
1714
+ if (options.json) {
1715
+ jsonOutput({ success: result.success, slug, action: "created" });
1716
+ }
1717
+ if (!result.success) {
1718
+ console.log(failure("Deploy failed"));
1719
+ process.exit(1);
1720
+ }
1721
+ console.log(green("\u2713") + reset + " Deployed hook " + gold(slug) + reset);
1722
+ }
1723
+ async function hooksLogs(slug, options = {}) {
1724
+ const limit = options.limit ? parseInt(options.limit, 10) : 20;
1725
+ const client = getClient(options.profile);
1726
+ const result = await client.code.getHookInvocations(slug, limit);
1727
+ if (options.json) {
1728
+ jsonOutput(
1729
+ result.success ? result.data.map((invocation) => ({
1730
+ id: invocation.id,
1731
+ trigger_type: invocation.trigger_type,
1732
+ duration_ms: invocation.duration_ms,
1733
+ success: invocation.success,
1734
+ invoked_at: invocation.invoked_at,
1735
+ logs: parseJsonField2(invocation.logs) ?? [],
1736
+ error_message: invocation.error_message ?? null,
1737
+ input: parseJsonField2(invocation.input),
1738
+ result: parseJsonField2(invocation.result)
1739
+ })) : []
1740
+ );
1741
+ }
1742
+ if (!result.success || !result.data?.length) {
1743
+ console.log(header(version9));
1744
+ console.log(" " + muted("No invocations yet.") + "\n");
1745
+ return;
1746
+ }
1747
+ const rows = result.data.map((inv) => {
1748
+ const date = new Date(inv.invoked_at * 1e3).toISOString().replace("T", " ").slice(0, 19);
1749
+ return [
1750
+ muted(date),
1751
+ muted(inv.duration_ms + "ms"),
1752
+ inv.success ? green("success") : failure("failed")
1753
+ ];
1754
+ });
1755
+ console.log(header(version9));
1756
+ console.log(
1757
+ renderTable({
1758
+ columns: [
1759
+ { header: "Time", width: 21 },
1760
+ { header: "Duration", width: 10 },
1761
+ { header: "Status", width: 10 }
1762
+ ],
1763
+ rows
1764
+ })
1765
+ );
1766
+ console.log("");
1767
+ }
1768
+ async function hooksToggle(slug, active, options = {}) {
1769
+ const client = getClient(options.profile);
1770
+ const result = await client.code.toggleHook(slug, active);
1771
+ if (options.json) {
1772
+ jsonOutput({ success: result.success, slug, active });
1773
+ }
1774
+ if (!result.success) {
1775
+ console.log(failure("Toggle failed"));
1776
+ process.exit(1);
1777
+ }
1778
+ console.log(
1779
+ green("\u2713") + reset + " " + gold(slug) + reset + " is now " + (active ? green("active") : inactive("inactive")) + reset
1780
+ );
1781
+ }
1782
+ async function hooksDelete(slug, options = {}) {
1783
+ const client = getClient(options.profile);
1784
+ const result = await client.code.deleteHook(slug);
1785
+ if (options.json) {
1786
+ jsonOutput({ success: result.success, slug });
1787
+ }
1788
+ if (!result.success) {
1789
+ console.log(failure("Delete failed"));
1790
+ process.exit(1);
1791
+ }
1792
+ console.log(green("\u2713") + reset + " Deleted hook " + gold(slug) + reset);
1793
+ }
1794
+
1569
1795
  // src/commands/watch.ts
1570
1796
  var BASE_URL2 = "https://api.globio.stanlink.online";
1571
- var version9 = getCliVersion();
1797
+ var version10 = getCliVersion();
1572
1798
  async function functionsWatch(slug, options = {}) {
1573
1799
  const profileName = options.profile ?? config.getActiveProfile();
1574
1800
  const profile = config.getProfile(profileName ?? "default");
@@ -1578,7 +1804,7 @@ async function functionsWatch(slug, options = {}) {
1578
1804
  );
1579
1805
  process.exit(1);
1580
1806
  }
1581
- console.log(header(version9));
1807
+ console.log(header(version10));
1582
1808
  console.log(
1583
1809
  " " + orange("watching") + reset + " " + slug + dim(" \xB7 press Ctrl+C to stop") + "\n"
1584
1810
  );
@@ -1674,10 +1900,10 @@ function renderEvent(event) {
1674
1900
  }
1675
1901
 
1676
1902
  // src/index.ts
1677
- var version10 = getCliVersion();
1903
+ var version11 = getCliVersion();
1678
1904
  var program = new Command();
1679
- program.name("globio").description("The official Globio CLI").version(version10).addHelpText("beforeAll", () => {
1680
- printBanner(version10);
1905
+ program.name("globio").description("The official Globio CLI").version(version11).addHelpText("beforeAll", () => {
1906
+ printBanner(version11);
1681
1907
  return "";
1682
1908
  }).addHelpText(
1683
1909
  "after",
@@ -1689,6 +1915,8 @@ Examples:
1689
1915
  $ globio projects list
1690
1916
  $ globio projects use proj_abc123
1691
1917
  $ globio functions deploy my-function
1918
+ $ globio hooks deploy on-signup --trigger id.onSignup
1919
+ $ globio hooks list
1692
1920
  $ globio migrate firestore --from ./key.json --all
1693
1921
 
1694
1922
  Credentials are stored in ~/.globio/profiles/
@@ -1710,7 +1938,7 @@ projects.command("list").description("List projects").option("--profile <name>",
1710
1938
  projects.command("create").description("Create a project").option("--name <name>", "Project name").option("--org <orgId>", "Organization ID").option("--env <environment>", "Environment", "development").option("-p, --profile <name>", "Profile name").option("--json", "Output as JSON").action(projectsCreate);
1711
1939
  projects.command("use <projectId>").description("Set active project").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action(projectsUse);
1712
1940
  program.command("services").description("List available Globio services").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action(servicesList);
1713
- var functions = program.command("functions").alias("fn").description("Manage GlobalCode edge functions");
1941
+ var functions = program.command("functions").alias("fn").description("Manage edge functions");
1714
1942
  functions.command("list").description("List all functions").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action(functionsList);
1715
1943
  functions.command("create <slug>").description("Scaffold a new function file locally").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action(functionsCreate);
1716
1944
  functions.command("deploy <slug>").description("Deploy a function to GlobalCode").option("-f, --file <path>", "Path to function file").option("-n, --name <name>", "Display name").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action(functionsDeploy);
@@ -1720,6 +1948,15 @@ functions.command("watch <slug>").description("Stream live function execution lo
1720
1948
  functions.command("delete <slug>").description("Delete a function").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action(functionsDelete);
1721
1949
  functions.command("enable <slug>").description("Enable a function").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action((slug, options) => functionsToggle(slug, true, options));
1722
1950
  functions.command("disable <slug>").description("Disable a function").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action((slug, options) => functionsToggle(slug, false, options));
1951
+ var hooks = program.command("hooks").description("Manage GC Hooks").action(hooksList);
1952
+ hooks.command("list").description("List all hooks").option("-p, --profile <name>", "Profile name").option("--json", "Output as JSON").action(hooksList);
1953
+ hooks.command("create <slug>").description("Scaffold a new hook file locally").option("--json", "Output as JSON").action(hooksCreate);
1954
+ hooks.command("deploy <slug>").description("Deploy a hook").option("-f, --file <path>", "Path to hook file").option("-n, --name <name>", "Display name").option("-t, --trigger <event>", "Hook trigger event (e.g. id.onSignup)").option("-p, --profile <name>", "Profile name").option("--json", "Output as JSON").action(hooksDeploy);
1955
+ hooks.command("logs <slug>").description("Show hook invocation history").option("-l, --limit <n>", "Number of entries", "20").option("-p, --profile <name>", "Profile name").option("--json", "Output as JSON").action(hooksLogs);
1956
+ hooks.command("watch <slug>").description("Stream live hook execution logs").option("-p, --profile <name>", "Profile name").action((slug, opts) => functionsWatch(slug, opts));
1957
+ hooks.command("enable <slug>").description("Enable a hook").option("-p, --profile <name>", "Profile name").option("--json", "Output as JSON").action((slug, opts) => hooksToggle(slug, true, opts));
1958
+ hooks.command("disable <slug>").description("Disable a hook").option("-p, --profile <name>", "Profile name").option("--json", "Output as JSON").action((slug, opts) => hooksToggle(slug, false, opts));
1959
+ hooks.command("delete <slug>").description("Delete a hook").option("-p, --profile <name>", "Profile name").option("--json", "Output as JSON").action(hooksDelete);
1723
1960
  var migrate = program.command("migrate").description("Migrate from Firebase to Globio");
1724
1961
  migrate.command("firestore").description("Migrate Firestore collections to GlobalDoc").requiredOption("--from <path>", "Path to Firebase service account JSON").option("--collection <name>", "Migrate a specific collection").option("--all", "Migrate all collections").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action(migrateFirestore);
1725
1962
  migrate.command("firebase-storage").description("Migrate Firebase Storage to GlobalVault").requiredOption("--from <path>", "Path to Firebase service account JSON").requiredOption("--bucket <name>", "Firebase Storage bucket").option("--folder <path>", "Migrate a specific folder").option("--all", "Migrate all files").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action(migrateFirebaseStorage);
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@globio/cli",
3
- "version": "0.2.2",
3
+ "version": "1.0.0",
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.2.2",
3
+ "version": "1.0.0",
4
4
  "description": "The official CLI for Globio — game backend as a service",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@clack/prompts": "^0.9.0",
17
- "@globio/sdk": "^1.0.0",
17
+ "@globio/sdk": "^1.1.0",
18
18
  "chalk": "^5.3.0",
19
19
  "cli-progress": "^3.12.0",
20
20
  "commander": "^12.0.0",
@@ -0,0 +1,324 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
2
+ import { getClient } from '../lib/sdk.js';
3
+ import {
4
+ failure,
5
+ getCliVersion,
6
+ gold,
7
+ green,
8
+ header,
9
+ inactive,
10
+ jsonOutput,
11
+ muted,
12
+ renderTable,
13
+ reset,
14
+ } from '../lib/banner.js';
15
+ import type { CodeInvocation } from '@globio/sdk';
16
+
17
+ const version = getCliVersion();
18
+
19
+ function parseJsonField<T>(value: string | null | undefined): T | null {
20
+ if (!value) return null;
21
+ try {
22
+ return JSON.parse(value) as T;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ const HOOK_TRIGGERS = [
29
+ 'id.onSignup',
30
+ 'id.onSignin',
31
+ 'id.onSignout',
32
+ 'id.onPasswordReset',
33
+ 'doc.onCreate',
34
+ 'doc.onUpdate',
35
+ 'doc.onDelete',
36
+ 'mart.onPurchase',
37
+ 'mart.onPayment',
38
+ 'sync.onRoomCreate',
39
+ 'sync.onRoomClose',
40
+ 'sync.onPlayerJoin',
41
+ 'sync.onPlayerLeave',
42
+ 'vault.onUpload',
43
+ 'vault.onDelete',
44
+ 'signal.onDeliver',
45
+ ] as const;
46
+
47
+ export async function hooksList(
48
+ options: { profile?: string; json?: boolean } = {}
49
+ ) {
50
+ const client = getClient(options.profile);
51
+ const result = await client.code.listHooks();
52
+
53
+ if (options.json) {
54
+ jsonOutput(
55
+ result.success
56
+ ? (result.data ?? []).map((hook) => ({
57
+ slug: hook.slug,
58
+ type: hook.type,
59
+ trigger_event: hook.trigger_event,
60
+ active: hook.active,
61
+ }))
62
+ : []
63
+ );
64
+ }
65
+
66
+ if (!result.success || !result.data?.length) {
67
+ console.log(header(version));
68
+ console.log(' ' + muted('No hooks found.') + '\n');
69
+ return;
70
+ }
71
+
72
+ const rows = result.data.map((fn) => [
73
+ gold(fn.slug),
74
+ gold(fn.trigger_event ?? '—'),
75
+ fn.active ? green('active') : inactive('inactive'),
76
+ ]);
77
+
78
+ console.log(header(version));
79
+ console.log(
80
+ renderTable({
81
+ columns: [
82
+ { header: 'Hook', width: 24 },
83
+ { header: 'Trigger', width: 24 },
84
+ { header: 'Status', width: 10 },
85
+ ],
86
+ rows,
87
+ })
88
+ );
89
+ console.log('');
90
+ }
91
+
92
+ export async function hooksCreate(
93
+ slug: string,
94
+ options: { json?: boolean } = {}
95
+ ) {
96
+ const filename = `${slug}.hook.js`;
97
+ if (existsSync(filename)) {
98
+ if (options.json) {
99
+ jsonOutput({ success: false, file: filename, error: 'File already exists' });
100
+ }
101
+ console.log(gold(filename) + reset + ' already exists.');
102
+ return;
103
+ }
104
+
105
+ const template = `/**
106
+ * GC Hook: ${slug}
107
+ * This hook fires automatically when its trigger event occurs.
108
+ * You cannot invoke it manually.
109
+ *
110
+ * The handler receives the event payload and the injected
111
+ * globio SDK — use it to orchestrate any Globio service.
112
+ */
113
+ async function handler(payload, globio) {
114
+ // payload: event data from the trigger
115
+ // globio: injected SDK — access all Globio services
116
+
117
+ // Example for id.onSignup:
118
+ // const { userId, email } = payload;
119
+ // await globio.doc.set('players', userId, {
120
+ // level: 1, xp: 0, coins: 100
121
+ // });
122
+ }
123
+ `;
124
+
125
+ writeFileSync(filename, template);
126
+
127
+ if (options.json) {
128
+ jsonOutput({ success: true, file: filename });
129
+ }
130
+
131
+ console.log(green('✓') + reset + ' Created ' + gold(filename) + reset);
132
+ console.log(muted(' Deploy with: globio hooks deploy ' + slug));
133
+ }
134
+
135
+ export async function hooksDeploy(
136
+ slug: string,
137
+ options: {
138
+ file?: string;
139
+ name?: string;
140
+ trigger?: string;
141
+ profile?: string;
142
+ json?: boolean;
143
+ }
144
+ ) {
145
+ const filename = options.file ?? `${slug}.hook.js`;
146
+ if (!existsSync(filename)) {
147
+ if (options.json) {
148
+ jsonOutput({ success: false, error: `File not found: ${filename}` });
149
+ }
150
+ console.log(
151
+ failure('File not found: ' + filename) +
152
+ reset +
153
+ ' Run: globio hooks create ' +
154
+ slug
155
+ );
156
+ process.exit(1);
157
+ }
158
+
159
+ if (!options.trigger) {
160
+ if (options.json) {
161
+ jsonOutput({ success: false, error: '--trigger required for hooks' });
162
+ }
163
+ console.log(
164
+ failure('--trigger required for hooks.') +
165
+ reset +
166
+ '\n\n Available triggers:\n' +
167
+ HOOK_TRIGGERS.map((trigger) => ' ' + gold(trigger) + reset).join('\n')
168
+ );
169
+ process.exit(1);
170
+ }
171
+
172
+ const code = readFileSync(filename, 'utf-8');
173
+ const client = getClient(options.profile);
174
+ const existing = await client.code.getFunction(slug).catch(() => null);
175
+
176
+ let result;
177
+ if (existing?.success) {
178
+ result = await client.code.updateHook(slug, {
179
+ code,
180
+ trigger: options.trigger as (typeof HOOK_TRIGGERS)[number],
181
+ });
182
+
183
+ if (options.json) {
184
+ jsonOutput({ success: result.success, slug, action: 'updated' });
185
+ }
186
+
187
+ if (!result.success) {
188
+ console.log(failure('Deploy failed'));
189
+ process.exit(1);
190
+ }
191
+
192
+ console.log(green('✓') + reset + ' Updated hook ' + gold(slug) + reset);
193
+ return;
194
+ }
195
+
196
+ result = await client.code.createHook({
197
+ name: options.name ?? slug,
198
+ slug,
199
+ trigger: options.trigger as (typeof HOOK_TRIGGERS)[number],
200
+ code,
201
+ });
202
+
203
+ if (options.json) {
204
+ jsonOutput({ success: result.success, slug, action: 'created' });
205
+ }
206
+
207
+ if (!result.success) {
208
+ console.log(failure('Deploy failed'));
209
+ process.exit(1);
210
+ }
211
+
212
+ console.log(green('✓') + reset + ' Deployed hook ' + gold(slug) + reset);
213
+ }
214
+
215
+ export async function hooksLogs(
216
+ slug: string,
217
+ options: { limit?: string; profile?: string; json?: boolean } = {}
218
+ ) {
219
+ const limit = options.limit ? parseInt(options.limit, 10) : 20;
220
+ const client = getClient(options.profile);
221
+ const result = await client.code.getHookInvocations(slug, limit);
222
+
223
+ if (options.json) {
224
+ jsonOutput(
225
+ result.success
226
+ ? (result.data as Array<CodeInvocation & {
227
+ logs?: string | null;
228
+ error_message?: string | null;
229
+ input?: string | null;
230
+ result?: string | null;
231
+ }>).map((invocation) => ({
232
+ id: invocation.id,
233
+ trigger_type: invocation.trigger_type,
234
+ duration_ms: invocation.duration_ms,
235
+ success: invocation.success,
236
+ invoked_at: invocation.invoked_at,
237
+ logs: parseJsonField<string[]>(invocation.logs) ?? [],
238
+ error_message: invocation.error_message ?? null,
239
+ input: parseJsonField<Record<string, unknown>>(invocation.input),
240
+ result: parseJsonField<unknown>(invocation.result),
241
+ }))
242
+ : []
243
+ );
244
+ }
245
+
246
+ if (!result.success || !result.data?.length) {
247
+ console.log(header(version));
248
+ console.log(' ' + muted('No invocations yet.') + '\n');
249
+ return;
250
+ }
251
+
252
+ const rows = result.data.map((inv) => {
253
+ const date = new Date(inv.invoked_at * 1000)
254
+ .toISOString()
255
+ .replace('T', ' ')
256
+ .slice(0, 19);
257
+ return [
258
+ muted(date),
259
+ muted(inv.duration_ms + 'ms'),
260
+ inv.success ? green('success') : failure('failed'),
261
+ ];
262
+ });
263
+
264
+ console.log(header(version));
265
+ console.log(
266
+ renderTable({
267
+ columns: [
268
+ { header: 'Time', width: 21 },
269
+ { header: 'Duration', width: 10 },
270
+ { header: 'Status', width: 10 },
271
+ ],
272
+ rows,
273
+ })
274
+ );
275
+ console.log('');
276
+ }
277
+
278
+ export async function hooksToggle(
279
+ slug: string,
280
+ active: boolean,
281
+ options: { profile?: string; json?: boolean } = {}
282
+ ) {
283
+ const client = getClient(options.profile);
284
+ const result = await client.code.toggleHook(slug, active);
285
+
286
+ if (options.json) {
287
+ jsonOutput({ success: result.success, slug, active });
288
+ }
289
+
290
+ if (!result.success) {
291
+ console.log(failure('Toggle failed'));
292
+ process.exit(1);
293
+ }
294
+
295
+ console.log(
296
+ green('✓') +
297
+ reset +
298
+ ' ' +
299
+ gold(slug) +
300
+ reset +
301
+ ' is now ' +
302
+ (active ? green('active') : inactive('inactive')) +
303
+ reset
304
+ );
305
+ }
306
+
307
+ export async function hooksDelete(
308
+ slug: string,
309
+ options: { profile?: string; json?: boolean } = {}
310
+ ) {
311
+ const client = getClient(options.profile);
312
+ const result = await client.code.deleteHook(slug);
313
+
314
+ if (options.json) {
315
+ jsonOutput({ success: result.success, slug });
316
+ }
317
+
318
+ if (!result.success) {
319
+ console.log(failure('Delete failed'));
320
+ process.exit(1);
321
+ }
322
+
323
+ console.log(green('✓') + reset + ' Deleted hook ' + gold(slug) + reset);
324
+ }
package/src/index.ts CHANGED
@@ -17,6 +17,14 @@ import {
17
17
  functionsDelete,
18
18
  functionsToggle,
19
19
  } from './commands/functions.js';
20
+ import {
21
+ hooksList,
22
+ hooksCreate,
23
+ hooksDeploy,
24
+ hooksLogs,
25
+ hooksToggle,
26
+ hooksDelete,
27
+ } from './commands/hooks.js';
20
28
  import { functionsWatch } from './commands/watch.js';
21
29
  import {
22
30
  migrateFirestore,
@@ -46,6 +54,8 @@ Examples:
46
54
  $ globio projects list
47
55
  $ globio projects use proj_abc123
48
56
  $ globio functions deploy my-function
57
+ $ globio hooks deploy on-signup --trigger id.onSignup
58
+ $ globio hooks list
49
59
  $ globio migrate firestore --from ./key.json --all
50
60
 
51
61
  Credentials are stored in ~/.globio/profiles/
@@ -110,7 +120,7 @@ program.command('services').description('List available Globio services').option
110
120
  const functions = program
111
121
  .command('functions')
112
122
  .alias('fn')
113
- .description('Manage GlobalCode edge functions');
123
+ .description('Manage edge functions');
114
124
 
115
125
  functions.command('list').description('List all functions').option('--profile <name>', 'Use a specific profile').option('--json', 'Output as JSON').action(functionsList);
116
126
  functions.command('create <slug>').description('Scaffold a new function file locally').option('--profile <name>', 'Use a specific profile').option('--json', 'Output as JSON').action(functionsCreate);
@@ -155,6 +165,69 @@ functions
155
165
  .option('--json', 'Output as JSON')
156
166
  .action((slug, options) => functionsToggle(slug, false, options));
157
167
 
168
+ const hooks = program
169
+ .command('hooks')
170
+ .description('Manage GC Hooks')
171
+ .action(hooksList);
172
+
173
+ hooks
174
+ .command('list')
175
+ .description('List all hooks')
176
+ .option('-p, --profile <name>', 'Profile name')
177
+ .option('--json', 'Output as JSON')
178
+ .action(hooksList);
179
+
180
+ hooks
181
+ .command('create <slug>')
182
+ .description('Scaffold a new hook file locally')
183
+ .option('--json', 'Output as JSON')
184
+ .action(hooksCreate);
185
+
186
+ hooks
187
+ .command('deploy <slug>')
188
+ .description('Deploy a hook')
189
+ .option('-f, --file <path>', 'Path to hook file')
190
+ .option('-n, --name <name>', 'Display name')
191
+ .option('-t, --trigger <event>', 'Hook trigger event (e.g. id.onSignup)')
192
+ .option('-p, --profile <name>', 'Profile name')
193
+ .option('--json', 'Output as JSON')
194
+ .action(hooksDeploy);
195
+
196
+ hooks
197
+ .command('logs <slug>')
198
+ .description('Show hook invocation history')
199
+ .option('-l, --limit <n>', 'Number of entries', '20')
200
+ .option('-p, --profile <name>', 'Profile name')
201
+ .option('--json', 'Output as JSON')
202
+ .action(hooksLogs);
203
+
204
+ hooks
205
+ .command('watch <slug>')
206
+ .description('Stream live hook execution logs')
207
+ .option('-p, --profile <name>', 'Profile name')
208
+ .action((slug, opts) => functionsWatch(slug, opts));
209
+
210
+ hooks
211
+ .command('enable <slug>')
212
+ .description('Enable a hook')
213
+ .option('-p, --profile <name>', 'Profile name')
214
+ .option('--json', 'Output as JSON')
215
+ .action((slug, opts) => hooksToggle(slug, true, opts));
216
+
217
+ hooks
218
+ .command('disable <slug>')
219
+ .description('Disable a hook')
220
+ .option('-p, --profile <name>', 'Profile name')
221
+ .option('--json', 'Output as JSON')
222
+ .action((slug, opts) => hooksToggle(slug, false, opts));
223
+
224
+ hooks
225
+ .command('delete <slug>')
226
+ .description('Delete a hook')
227
+ .option('-p, --profile <name>', 'Profile name')
228
+ .option('--json', 'Output as JSON')
229
+ .action(hooksDelete);
230
+
158
231
  const migrate = program
159
232
  .command('migrate')
160
233
  .description('Migrate from Firebase to Globio');