@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 +249 -12
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/commands/hooks.ts +324 -0
- package/src/index.ts +74 -1
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(
|
|
209
|
+
function header(version12, subtitle) {
|
|
210
210
|
const lines = [
|
|
211
211
|
"",
|
|
212
|
-
orange(" \u21D2\u21D2") + reset + " globio " + dim(
|
|
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(
|
|
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" +
|
|
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:
|
|
647
|
-
const serviceAccount = JSON.parse(
|
|
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
|
|
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(
|
|
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
|
|
1903
|
+
var version11 = getCliVersion();
|
|
1678
1904
|
var program = new Command();
|
|
1679
|
-
program.name("globio").description("The official Globio CLI").version(
|
|
1680
|
-
printBanner(
|
|
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
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@globio/cli",
|
|
3
|
-
"version": "0.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.
|
|
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
|
|
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');
|