@globio/cli 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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": "0.2.3",
4
4
  "license": "MIT",
5
5
  "exports": "./src/index.ts"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@globio/cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
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');