@glassanalytics/cli 0.2.0 → 0.2.1
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/LICENSE +21 -0
- package/README.md +2 -1
- package/dist/glass.js +471 -62
- package/dist/worker/migrations/0002_cohorts.sql +11 -0
- package/dist/worker/migrations/0003_insights.sql +14 -0
- package/dist/worker/worker.js +860 -155
- package/package.json +4 -4
package/dist/glass.js
CHANGED
|
@@ -2,7 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
// packages/cli/src/index.ts
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
// packages/core/src/version.ts
|
|
7
|
+
var GLASS_VERSION = "0.2.1";
|
|
8
|
+
var UPGRADE_COMMAND = "npx @glassanalytics/cli@latest upgrade";
|
|
9
|
+
function parseSemver(version) {
|
|
10
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
|
|
11
|
+
if (!m) return null;
|
|
12
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
13
|
+
}
|
|
14
|
+
function isNewerThan(a, b) {
|
|
15
|
+
const pa = parseSemver(a);
|
|
16
|
+
const pb = parseSemver(b);
|
|
17
|
+
if (!pa || !pb) return a !== b && a > b;
|
|
18
|
+
for (let i = 0; i < 3; i++) {
|
|
19
|
+
const d = pa[i] - pb[i];
|
|
20
|
+
if (d) return d > 0;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
6
24
|
|
|
7
25
|
// packages/cli/src/config.ts
|
|
8
26
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -366,16 +384,110 @@ async function experiments(sub, key2, rest, flags2) {
|
|
|
366
384
|
}
|
|
367
385
|
|
|
368
386
|
// packages/cli/src/commands/init.ts
|
|
369
|
-
import { readFileSync as
|
|
370
|
-
import { join as
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
387
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
388
|
+
import { join as join6 } from "node:path";
|
|
389
|
+
|
|
390
|
+
// packages/core/src/ids.ts
|
|
391
|
+
function randomBytes(n) {
|
|
392
|
+
const bytes = new Uint8Array(n);
|
|
393
|
+
crypto.getRandomValues(bytes);
|
|
394
|
+
return bytes;
|
|
395
|
+
}
|
|
396
|
+
function hex(bytes) {
|
|
397
|
+
let out = "";
|
|
398
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
399
|
+
return out;
|
|
400
|
+
}
|
|
401
|
+
function randomToken(bytes = 32) {
|
|
402
|
+
return hex(randomBytes(bytes));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// packages/core/src/instrument.ts
|
|
406
|
+
var scriptTag = (o) => `<script defer src="${o.host}/s.js" data-project="${o.project}"></script>`;
|
|
407
|
+
var browserCapture = (event, props) => `window.glass?.capture(${JSON.stringify(event)}${props && Object.keys(props).length ? `, ${JSON.stringify(props)}` : ""});`;
|
|
408
|
+
var moduleCapture = (event, props) => `import { glass } from '@glassanalytics/web';
|
|
409
|
+
|
|
410
|
+
glass.capture(${JSON.stringify(event)}${props && Object.keys(props).length ? `, ${JSON.stringify(props)}` : ""});`;
|
|
411
|
+
var FRAMEWORKS = {
|
|
412
|
+
html: {
|
|
413
|
+
name: "Plain HTML",
|
|
414
|
+
snippet: (o) => ({ file: "index.html (inside <head>, on every page)", code: scriptTag(o), capture: browserCapture })
|
|
415
|
+
},
|
|
416
|
+
nextjs: {
|
|
417
|
+
name: "Next.js",
|
|
418
|
+
snippet: (o) => ({
|
|
419
|
+
file: "app/layout.tsx (App Router) or pages/_app.tsx",
|
|
420
|
+
code: `import Script from 'next/script';
|
|
421
|
+
|
|
422
|
+
// inside <body>:
|
|
423
|
+
<Script src="${o.host}/s.js" data-project="${o.project}" strategy="afterInteractive" />`,
|
|
424
|
+
capture: browserCapture
|
|
425
|
+
})
|
|
426
|
+
},
|
|
427
|
+
astro: {
|
|
428
|
+
name: "Astro",
|
|
429
|
+
snippet: (o) => ({ file: "src/layouts/Layout.astro (inside <head>)", code: scriptTag(o).replace("<script", "<script is:inline"), capture: browserCapture })
|
|
430
|
+
},
|
|
431
|
+
sveltekit: {
|
|
432
|
+
name: "SvelteKit",
|
|
433
|
+
snippet: (o) => ({ file: "src/app.html (inside <head>)", code: scriptTag(o), capture: browserCapture })
|
|
434
|
+
},
|
|
435
|
+
nuxt: {
|
|
436
|
+
name: "Nuxt",
|
|
437
|
+
snippet: (o) => ({
|
|
438
|
+
file: "nuxt.config.ts",
|
|
439
|
+
code: `export default defineNuxtConfig({
|
|
440
|
+
app: { head: { script: [{ src: '${o.host}/s.js', defer: true, 'data-project': '${o.project}' }] } },
|
|
441
|
+
});`,
|
|
442
|
+
capture: browserCapture
|
|
443
|
+
})
|
|
444
|
+
},
|
|
445
|
+
remix: {
|
|
446
|
+
name: "Remix / React Router",
|
|
447
|
+
snippet: (o) => ({ file: "app/root.tsx (inside <head>)", code: scriptTag(o).replace("defer ", "defer={true} "), capture: browserCapture })
|
|
448
|
+
},
|
|
449
|
+
vite: {
|
|
450
|
+
name: "Vite (React, Vue, Svelte, Solid\u2026)",
|
|
451
|
+
snippet: (o) => ({
|
|
452
|
+
file: "src/main.ts (or main.tsx) \u2014 after `npm install @glassanalytics/web`",
|
|
453
|
+
code: `import { glass } from '@glassanalytics/web';
|
|
454
|
+
|
|
455
|
+
glass.init({ host: '${o.host}', project: '${o.project}' });`,
|
|
456
|
+
capture: moduleCapture
|
|
457
|
+
})
|
|
458
|
+
},
|
|
459
|
+
worker: {
|
|
460
|
+
name: "Cloudflare Worker / server-side",
|
|
461
|
+
snippet: (o) => ({
|
|
462
|
+
file: "any server code",
|
|
463
|
+
code: `// Server-side capture: POST events to Glass.
|
|
464
|
+
await fetch('${o.host}/e', {
|
|
465
|
+
method: 'POST',
|
|
466
|
+
body: JSON.stringify({ project: '${o.project}', events: [{ id: crypto.randomUUID(), event: 'order_paid', ts: Date.now(), distinct_id: userId, user_id: userId, props: { amount: 42 } }] }),
|
|
467
|
+
});`,
|
|
468
|
+
capture: (event, props) => `await fetch(GLASS_HOST + '/e', { method: 'POST', body: JSON.stringify({ project: GLASS_PROJECT, events: [{ id: crypto.randomUUID(), event: ${JSON.stringify(event)}, ts: Date.now(), distinct_id: userId, user_id: userId, props: ${JSON.stringify(props ?? {})} }] }) });`
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
function detectFramework(pkg) {
|
|
473
|
+
if (!pkg) return "html";
|
|
474
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
475
|
+
const has = (name) => Object.hasOwn(deps, name);
|
|
476
|
+
if (has("next")) return "nextjs";
|
|
477
|
+
if (has("astro")) return "astro";
|
|
478
|
+
if (has("@sveltejs/kit")) return "sveltekit";
|
|
479
|
+
if (has("nuxt")) return "nuxt";
|
|
480
|
+
if (has("@remix-run/react") || has("react-router")) return "remix";
|
|
481
|
+
if (has("vite")) return "vite";
|
|
482
|
+
if (has("wrangler") || has("hono")) return "worker";
|
|
483
|
+
return "html";
|
|
484
|
+
}
|
|
485
|
+
var EVENT_NAMING_GUIDE = 'Name events object_action in snake_case and past tense (signup_completed, plan_upgraded, invite_sent). Put variable details in properties ({ plan: "pro" }), never in the event name. Never send emails, names or other personal data as properties.';
|
|
486
|
+
var EVENT_DOCUMENTATION_GUIDE = 'After a custom event is shipping, call define_event (or `glass events <name> --describe "\u2026"`). The description must say how and when the event is fired (which user action or code path). Document every property with what it means and which values to expect (e.g. description: "Fired when the user finishes checkout after payment succeeds."; properties: { plan: "Chosen plan id (free|pro|enterprise)", amount: "Charge in the account currency" }). list_events returns undocumented custom events \u2014 document those before relying on them in funnels or trends.';
|
|
374
487
|
|
|
375
488
|
// packages/cli/src/agent-files.ts
|
|
376
489
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
377
490
|
import { dirname as dirname2, join as join2 } from "node:path";
|
|
378
|
-
import { EVENT_NAMING_GUIDE } from "@glassanalytics/core/instrument";
|
|
379
491
|
var START = "<!-- glass:start -->";
|
|
380
492
|
var END = "<!-- glass:end -->";
|
|
381
493
|
function agentsSection(o) {
|
|
@@ -392,8 +504,16 @@ Tracking (project \`${o.project}\`):
|
|
|
392
504
|
- Install (${o.snippet.file}): \`${o.snippet.code.split("\n").pop()}\`
|
|
393
505
|
- Custom events: \`glass.capture('signup_completed', { plan: 'pro' })\`. After login: \`glass.identify(user.id, { plan })\`.
|
|
394
506
|
- ${EVENT_NAMING_GUIDE}
|
|
395
|
-
- After adding an event and deploying, confirm it arrives: \`npx @glassanalytics/cli events <name
|
|
507
|
+
- After adding an event and deploying, confirm it arrives: \`npx @glassanalytics/cli events <name>\` (or MCP \`describe_event\`).
|
|
508
|
+
- ${EVENT_DOCUMENTATION_GUIDE}
|
|
509
|
+
- Prefer MCP \`define_event\` so you can set both the event description and each property's meaning in one call.
|
|
396
510
|
- Feature flags: \`glass.flag('key')\` returns "on", a variant key, or null. Create with \`npx @glassanalytics/cli flags set <key> --rollout 10\`.
|
|
511
|
+
|
|
512
|
+
Upgrades (the Worker version is pinned in \`.glass/config.json\`):
|
|
513
|
+
- \`.github/workflows/glass.yml\` deploys whatever \`.glass/\` holds when it changes on the default branch. If that file is missing, run \`npx @glassanalytics/cli ci\` and add the \`CLOUDFLARE_API_TOKEN\` repository secret.
|
|
514
|
+
- To upgrade: \`npx @glassanalytics/cli@latest upgrade\` stages the new Worker and migrations into \`.glass/\` without deploying. Commit and push (or open a PR); CI deploys. Ask before pushing if pushes also deploy the site.
|
|
515
|
+
- Do not deploy Glass from a laptop (\`glass upgrade --local\`, wrangler) once the workflow exists, and never commit \`.glass/credentials.json\`.
|
|
516
|
+
- \`glass upgrade\` refuses to replace a newer Worker with an older CLI. If it does, use \`@latest\`; do not pass \`--allow-downgrade\`.
|
|
397
517
|
${END}`;
|
|
398
518
|
}
|
|
399
519
|
function upsertSection(path, section, createIfMissing) {
|
|
@@ -418,7 +538,7 @@ Glass is deployed at ${o.host} (project \`${o.project}\`). Prefer the \`glass\`
|
|
|
418
538
|
|
|
419
539
|
## Answer a question about usage
|
|
420
540
|
|
|
421
|
-
1. \`overview\` for the period (visitors, top pages, errors, events that moved).
|
|
541
|
+
1. \`overview\` for the period (visitors, top pages, errors, events that moved). If it (or MCP init) says an upgrade is available, tell the user; with their go-ahead, follow "Upgrade Glass" below.
|
|
422
542
|
2. \`list_events\` to get exact event names \u2014 never guess them.
|
|
423
543
|
3. The right query: \`query_trends\` (counts over time), \`query_funnel\` (conversion through steps), \`query_retention\` (who comes back), \`sql\` when nothing else fits.
|
|
424
544
|
4. Report numbers with the dashboard link and SQL from the result.
|
|
@@ -428,7 +548,13 @@ Glass is deployed at ${o.host} (project \`${o.project}\`). Prefer the \`glass\`
|
|
|
428
548
|
1. \`instrument\` with the event name returns the exact code for this framework.
|
|
429
549
|
2. ${EVENT_NAMING_GUIDE}
|
|
430
550
|
3. After deploying, \`describe_event\` (or \`npx @glassanalytics/cli events <name>\`) confirms events arrive with the expected properties.
|
|
431
|
-
4. \`define_event\`
|
|
551
|
+
4. Always \`define_event\` next. ${EVENT_DOCUMENTATION_GUIDE}
|
|
552
|
+
|
|
553
|
+
## First session on a project
|
|
554
|
+
|
|
555
|
+
1. \`overview\` then \`list_events\`.
|
|
556
|
+
2. For every name in \`undocumented\`, read the code that fires it (or ask), then \`define_event\` with how/when it fires and property meanings.
|
|
557
|
+
3. Do not invent funnel or trend queries against undocumented custom events until those descriptions exist.
|
|
432
558
|
|
|
433
559
|
## Debug a user problem
|
|
434
560
|
|
|
@@ -436,6 +562,13 @@ Glass is deployed at ${o.host} (project \`${o.project}\`). Prefer the \`glass\`
|
|
|
436
562
|
2. \`list_replays\` with \`with_errors: true\` (or a \`person\`) to find affected sessions.
|
|
437
563
|
3. \`summarise_replay\` to read what happened: pages, labelled clicks, rage clicks, errors.
|
|
438
564
|
|
|
565
|
+
## Upgrade Glass
|
|
566
|
+
|
|
567
|
+
1. \`npx @glassanalytics/cli@latest upgrade\`. With \`.github/workflows/glass.yml\` present this only stages \`.glass/\` (Worker, migrations, pinned version); nothing deploys.
|
|
568
|
+
2. No workflow yet? \`npx @glassanalytics/cli ci\` writes it; the repo needs secret \`CLOUDFLARE_API_TOKEN\` (\`gh secret set CLOUDFLARE_API_TOKEN\`).
|
|
569
|
+
3. Commit \`.glass/\` and push to the default branch (or open a PR). The Glass workflow migrates D1 and deploys.
|
|
570
|
+
4. \`npx @glassanalytics/cli status\` confirms the new version is live.
|
|
571
|
+
|
|
439
572
|
## Run an experiment
|
|
440
573
|
|
|
441
574
|
1. \`create_experiment\` with a goal event (creates a control/test flag).
|
|
@@ -472,10 +605,164 @@ function writeAgentFiles(o) {
|
|
|
472
605
|
return written;
|
|
473
606
|
}
|
|
474
607
|
|
|
608
|
+
// packages/cli/src/ci.ts
|
|
609
|
+
import { execFileSync } from "node:child_process";
|
|
610
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
611
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
612
|
+
var CI_WORKFLOW_PATH = ".github/workflows/glass.yml";
|
|
613
|
+
var CI_SECRET = "CLOUDFLARE_API_TOKEN";
|
|
614
|
+
var MARKER = "# Generated by `glass ci`.";
|
|
615
|
+
function inCi(env = process.env) {
|
|
616
|
+
return env.CI === "true" || env.CI === "1" || env.GITHUB_ACTIONS === "true";
|
|
617
|
+
}
|
|
618
|
+
function hasCiWorkflow(root) {
|
|
619
|
+
return existsSync3(join3(root, CI_WORKFLOW_PATH));
|
|
620
|
+
}
|
|
621
|
+
function defaultBranch(root) {
|
|
622
|
+
const git = (args) => {
|
|
623
|
+
try {
|
|
624
|
+
return execFileSync("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
625
|
+
} catch {
|
|
626
|
+
return "";
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
const head = git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]).replace(/^origin\//, "");
|
|
630
|
+
return head || git(["branch", "--show-current"]) || "main";
|
|
631
|
+
}
|
|
632
|
+
function ciWorkflow(o) {
|
|
633
|
+
return `${MARKER} Deploys the Glass analytics Worker from .glass/.
|
|
634
|
+
#
|
|
635
|
+
# The version is pinned in .glass/config.json. To upgrade:
|
|
636
|
+
# npx @glassanalytics/cli@latest upgrade # stages the new Worker into .glass/
|
|
637
|
+
# git commit .glass && git push # this workflow migrates and deploys it
|
|
638
|
+
#
|
|
639
|
+
# Needs repository secret ${o.secret}: a Cloudflare API token with
|
|
640
|
+
# Workers Scripts Edit, D1 Edit, Workers R2 Storage Edit, Account Settings Read
|
|
641
|
+
# (and Zone Workers Routes Edit if .glass/config.json has a route).
|
|
642
|
+
# Worker secrets (API keys) are set by \`glass init\` and survive deploys.
|
|
643
|
+
name: Glass
|
|
644
|
+
|
|
645
|
+
on:
|
|
646
|
+
push:
|
|
647
|
+
branches: [${o.branch}]
|
|
648
|
+
paths: ['.glass/**', '${CI_WORKFLOW_PATH}']
|
|
649
|
+
workflow_dispatch:
|
|
650
|
+
|
|
651
|
+
concurrency:
|
|
652
|
+
group: glass-deploy
|
|
653
|
+
cancel-in-progress: false
|
|
654
|
+
|
|
655
|
+
jobs:
|
|
656
|
+
deploy:
|
|
657
|
+
runs-on: ubuntu-latest
|
|
658
|
+
permissions:
|
|
659
|
+
contents: read
|
|
660
|
+
steps:
|
|
661
|
+
- uses: actions/checkout@v4
|
|
662
|
+
- uses: actions/setup-node@v4
|
|
663
|
+
with:
|
|
664
|
+
node-version: 22
|
|
665
|
+
- name: Deploy the Glass version pinned in .glass/config.json
|
|
666
|
+
run: |
|
|
667
|
+
version="$(node -p "require('./.glass/config.json').version")"
|
|
668
|
+
npx -y "@glassanalytics/cli@\${version}" upgrade --ci
|
|
669
|
+
env:
|
|
670
|
+
CLOUDFLARE_API_TOKEN: \${{ secrets.${o.secret} }}
|
|
671
|
+
`;
|
|
672
|
+
}
|
|
673
|
+
function writeCiWorkflow(root, options = {}) {
|
|
674
|
+
const branch = options.branch ?? defaultBranch(root);
|
|
675
|
+
const secret = options.secret ?? CI_SECRET;
|
|
676
|
+
const path = join3(root, CI_WORKFLOW_PATH);
|
|
677
|
+
const next = ciWorkflow({ branch, secret });
|
|
678
|
+
if (existsSync3(path)) {
|
|
679
|
+
const current = readFileSync3(path, "utf8");
|
|
680
|
+
if (!current.startsWith(MARKER)) return { path: CI_WORKFLOW_PATH, status: "kept", branch, secret };
|
|
681
|
+
if (current === next) return { path: CI_WORKFLOW_PATH, status: "kept", branch, secret };
|
|
682
|
+
writeFileSync3(path, next);
|
|
683
|
+
return { path: CI_WORKFLOW_PATH, status: "updated", branch, secret };
|
|
684
|
+
}
|
|
685
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
686
|
+
writeFileSync3(path, next);
|
|
687
|
+
return { path: CI_WORKFLOW_PATH, status: "created", branch, secret };
|
|
688
|
+
}
|
|
689
|
+
function ciNextSteps(o) {
|
|
690
|
+
return [
|
|
691
|
+
`Add repository secret ${o.secret} (a Cloudflare API token; same scopes as init): gh secret set ${o.secret}`,
|
|
692
|
+
`Commit .glass/ (not credentials.json, which is git-ignored) and ${CI_WORKFLOW_PATH}, then push to ${o.branch}.`,
|
|
693
|
+
"Upgrade later with `npx @glassanalytics/cli@latest upgrade`: it stages the new version into .glass/ without deploying. Commit and push; the workflow deploys it.",
|
|
694
|
+
"Do not deploy Glass from a laptop once this workflow exists. `glass upgrade --local` overrides that for emergencies only."
|
|
695
|
+
];
|
|
696
|
+
}
|
|
697
|
+
|
|
475
698
|
// packages/cli/src/cloudflare/provision.ts
|
|
476
|
-
import { copyFileSync, mkdirSync as
|
|
477
|
-
import { join as
|
|
478
|
-
|
|
699
|
+
import { copyFileSync, mkdirSync as mkdirSync4, readdirSync, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
700
|
+
import { join as join4 } from "node:path";
|
|
701
|
+
|
|
702
|
+
// packages/core/src/events.ts
|
|
703
|
+
var EVENT_COLUMNS = [
|
|
704
|
+
"id",
|
|
705
|
+
"project",
|
|
706
|
+
"event",
|
|
707
|
+
"ts_ms",
|
|
708
|
+
"distinct_id",
|
|
709
|
+
"user_id",
|
|
710
|
+
"person_id",
|
|
711
|
+
"session_id",
|
|
712
|
+
"url",
|
|
713
|
+
"host",
|
|
714
|
+
"path",
|
|
715
|
+
"referrer",
|
|
716
|
+
"referrer_host",
|
|
717
|
+
"title",
|
|
718
|
+
"utm_source",
|
|
719
|
+
"utm_medium",
|
|
720
|
+
"utm_campaign",
|
|
721
|
+
"utm_term",
|
|
722
|
+
"utm_content",
|
|
723
|
+
"country",
|
|
724
|
+
"region",
|
|
725
|
+
"city",
|
|
726
|
+
"device",
|
|
727
|
+
"browser",
|
|
728
|
+
"os",
|
|
729
|
+
"flags",
|
|
730
|
+
"props",
|
|
731
|
+
"person_props",
|
|
732
|
+
"lib",
|
|
733
|
+
"received_ms"
|
|
734
|
+
];
|
|
735
|
+
|
|
736
|
+
// packages/core/src/lake.ts
|
|
737
|
+
var INTEGER_COLUMNS = /* @__PURE__ */ new Set(["ts_ms", "received_ms"]);
|
|
738
|
+
var OPTIONAL_COLUMNS = /* @__PURE__ */ new Set(["user_id"]);
|
|
739
|
+
var LAKE_TABLES = {
|
|
740
|
+
events: {
|
|
741
|
+
stream: "glass_events",
|
|
742
|
+
fields: [
|
|
743
|
+
...EVENT_COLUMNS.map(
|
|
744
|
+
(name) => ({
|
|
745
|
+
name,
|
|
746
|
+
type: INTEGER_COLUMNS.has(name) ? "int64" : "string",
|
|
747
|
+
required: !OPTIONAL_COLUMNS.has(name)
|
|
748
|
+
})
|
|
749
|
+
),
|
|
750
|
+
{ name: "ts", type: "timestamp", required: true }
|
|
751
|
+
]
|
|
752
|
+
},
|
|
753
|
+
identities: {
|
|
754
|
+
stream: "glass_identities",
|
|
755
|
+
fields: [
|
|
756
|
+
{ name: "project", type: "string", required: true },
|
|
757
|
+
{ name: "distinct_id", type: "string", required: true },
|
|
758
|
+
{ name: "person_id", type: "string", required: true },
|
|
759
|
+
{ name: "created_at", type: "int64", required: true }
|
|
760
|
+
]
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
function streamSchemaFile(table2) {
|
|
764
|
+
return { fields: [...LAKE_TABLES[table2].fields] };
|
|
765
|
+
}
|
|
479
766
|
|
|
480
767
|
// packages/cli/src/cloudflare/wrangler.ts
|
|
481
768
|
import { spawn } from "node:child_process";
|
|
@@ -605,9 +892,9 @@ async function ensureLake(wrangler, options, report) {
|
|
|
605
892
|
const sinkName = `${streamName}_sink`;
|
|
606
893
|
let stream = await findByName(wrangler, "streams", streamName);
|
|
607
894
|
if (!stream) {
|
|
608
|
-
const schemaPath =
|
|
609
|
-
|
|
610
|
-
|
|
895
|
+
const schemaPath = join4(options.dir, "schemas", `${table2}.json`);
|
|
896
|
+
mkdirSync4(join4(options.dir, "schemas"), { recursive: true });
|
|
897
|
+
writeFileSync4(schemaPath, `${JSON.stringify(streamSchemaFile(table2), null, 2)}
|
|
611
898
|
`);
|
|
612
899
|
await must(wrangler, ["pipelines", "streams", "create", streamName, "--schema-file", schemaPath, "--http-enabled", "false"]);
|
|
613
900
|
stream = await findByName(wrangler, "streams", streamName);
|
|
@@ -677,10 +964,10 @@ ${JSON.stringify(wrangler, null, 2)}
|
|
|
677
964
|
`;
|
|
678
965
|
}
|
|
679
966
|
function installWorkerFiles(dir, bundleDir) {
|
|
680
|
-
|
|
681
|
-
copyFileSync(
|
|
682
|
-
for (const file of readdirSync(
|
|
683
|
-
copyFileSync(
|
|
967
|
+
mkdirSync4(join4(dir, "migrations"), { recursive: true });
|
|
968
|
+
copyFileSync(join4(bundleDir, "worker.js"), join4(dir, "worker.js"));
|
|
969
|
+
for (const file of readdirSync(join4(bundleDir, "migrations"))) {
|
|
970
|
+
copyFileSync(join4(bundleDir, "migrations", file), join4(dir, "migrations", file));
|
|
684
971
|
}
|
|
685
972
|
}
|
|
686
973
|
async function deploy(wrangler, configPath) {
|
|
@@ -691,8 +978,8 @@ async function migrate(wrangler, database, configPath) {
|
|
|
691
978
|
await must(wrangler, ["d1", "migrations", "apply", database, "--remote", "--config", configPath]);
|
|
692
979
|
}
|
|
693
980
|
async function putSecrets(wrangler, secrets, configPath, dir) {
|
|
694
|
-
const file =
|
|
695
|
-
|
|
981
|
+
const file = join4(dir, ".secrets.tmp.json");
|
|
982
|
+
writeFileSync4(file, JSON.stringify(secrets), { mode: 384 });
|
|
696
983
|
try {
|
|
697
984
|
await must(wrangler, ["secret", "bulk", file, "--config", configPath]);
|
|
698
985
|
} finally {
|
|
@@ -747,26 +1034,65 @@ async function choose(question, items, label, flag) {
|
|
|
747
1034
|
}
|
|
748
1035
|
|
|
749
1036
|
// packages/cli/src/commands/upgrade.ts
|
|
750
|
-
import { existsSync as
|
|
751
|
-
import { join as
|
|
1037
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1038
|
+
import { join as join5 } from "node:path";
|
|
752
1039
|
import { fileURLToPath } from "node:url";
|
|
753
|
-
import { GLASS_VERSION } from "@glassanalytics/core/version";
|
|
754
1040
|
function workerBundleDir() {
|
|
755
1041
|
const candidates = [new URL("./worker/", import.meta.url), new URL("../../../worker/dist/", import.meta.url)];
|
|
756
1042
|
for (const c of candidates) {
|
|
757
1043
|
const dir = fileURLToPath(c);
|
|
758
|
-
if (
|
|
1044
|
+
if (existsSync4(join5(dir, "worker.js")) && existsSync4(join5(dir, "migrations"))) return dir;
|
|
759
1045
|
}
|
|
760
1046
|
throw new UsageError("The Worker bundle is missing.", "If you are working on Glass itself, run `pnpm build` first.");
|
|
761
1047
|
}
|
|
1048
|
+
async function liveVersion(host, fetcher) {
|
|
1049
|
+
if (!host) return null;
|
|
1050
|
+
try {
|
|
1051
|
+
const res = await fetcher(`${host}/health`, { signal: AbortSignal.timeout(5e3) });
|
|
1052
|
+
if (!res.ok) return null;
|
|
1053
|
+
const body = await res.json();
|
|
1054
|
+
return typeof body.version === "string" ? body.version : null;
|
|
1055
|
+
} catch {
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
762
1059
|
async function upgrade(options, deps = {}) {
|
|
763
|
-
const root = projectRoot();
|
|
1060
|
+
const root = deps.root ?? projectRoot();
|
|
1061
|
+
const env = deps.env ?? process.env;
|
|
764
1062
|
const config = loadConfig(root);
|
|
1063
|
+
if (!config) throw new UsageError("No Glass deployment in this project.", "Run `npx @glassanalytics/cli init` first.");
|
|
1064
|
+
const ci2 = options.ci ?? inCi(env);
|
|
765
1065
|
const credentials = loadCredentials(root);
|
|
766
|
-
if (!
|
|
767
|
-
const token = options.token ?? process.env.CLOUDFLARE_API_TOKEN ?? await ask("Cloudflare API token", { flag: "--token (or CLOUDFLARE_API_TOKEN)", secret: true });
|
|
768
|
-
const wrangler = (deps.wrangler ?? createWrangler)({ token, accountId: config.accountId }, root);
|
|
1066
|
+
if (!credentials && !ci2) throw new UsageError("No .glass/credentials.json in this project.", "Run `npx @glassanalytics/cli init` again to restore keys, or deploy from CI (`glass upgrade --ci`), which leaves Worker secrets as they are.");
|
|
769
1067
|
const dir = glassDir(root);
|
|
1068
|
+
const bundle = deps.bundleDir ?? workerBundleDir();
|
|
1069
|
+
const live = await liveVersion(config.host, deps.fetcher ?? fetch);
|
|
1070
|
+
const deployed = [config.version, live].filter((v) => Boolean(v)).sort((a, b) => isNewerThan(a, b) ? -1 : 1)[0];
|
|
1071
|
+
if (deployed && isNewerThan(deployed, GLASS_VERSION) && !options.allowDowngrade) {
|
|
1072
|
+
throw new UsageError(
|
|
1073
|
+
`This deployment runs Glass ${deployed}, which is newer than this CLI (${GLASS_VERSION}). Nothing was changed.`,
|
|
1074
|
+
`Run \`${UPGRADE_COMMAND}\` (or npx @glassanalytics/cli@${deployed} or later). --allow-downgrade forces the older Worker.`
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
const from = config.version;
|
|
1078
|
+
if (hasCiWorkflow(root) && !ci2 && !options.local) {
|
|
1079
|
+
installWorkerFiles(dir, bundle);
|
|
1080
|
+
config.version = GLASS_VERSION;
|
|
1081
|
+
writeFileSync5(join5(dir, "wrangler.jsonc"), renderWranglerConfig(config));
|
|
1082
|
+
saveConfig(config, root);
|
|
1083
|
+
const branch = defaultBranch(root);
|
|
1084
|
+
if (options.json) {
|
|
1085
|
+
printJson({ from, to: GLASS_VERSION, staged: true, deploy: CI_WORKFLOW_PATH, next: `Commit .glass/ and push to ${branch}` });
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
ok(`Staged Glass ${from === GLASS_VERSION ? GLASS_VERSION : `${from} \u2192 ${GLASS_VERSION}`} in .glass/ (nothing deployed)`);
|
|
1089
|
+
line(` Commit .glass/ and push to ${branch}; ${CI_WORKFLOW_PATH} migrates and deploys it.`);
|
|
1090
|
+
line(color.dim(" Deploying from this machine instead: glass upgrade --local"));
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
const token = options.token ?? env.CLOUDFLARE_API_TOKEN ?? (ci2 ? void 0 : await ask("Cloudflare API token", { flag: "--token (or CLOUDFLARE_API_TOKEN)", secret: true }));
|
|
1094
|
+
if (!token) throw new UsageError("CLOUDFLARE_API_TOKEN is not set.", `In CI, map the repository secret to it (see ${CI_WORKFLOW_PATH}).`);
|
|
1095
|
+
const wrangler = (deps.wrangler ?? createWrangler)({ token, accountId: config.accountId }, root);
|
|
770
1096
|
const report = (status2, label, detail) => {
|
|
771
1097
|
if (options.json) return;
|
|
772
1098
|
(status2 === "skipped" ? skip : ok)(`${label}${detail ? color.dim(` (${detail})`) : ""}`);
|
|
@@ -774,28 +1100,48 @@ async function upgrade(options, deps = {}) {
|
|
|
774
1100
|
if (!config.replay && await ensureBucket(wrangler, `${config.name}-replays`, config.eu, report, "session replay")) {
|
|
775
1101
|
config.replay = { bucket: `${config.name}-replays` };
|
|
776
1102
|
}
|
|
777
|
-
installWorkerFiles(dir,
|
|
778
|
-
const configPath =
|
|
779
|
-
|
|
1103
|
+
installWorkerFiles(dir, bundle);
|
|
1104
|
+
const configPath = join5(dir, "wrangler.jsonc");
|
|
1105
|
+
writeFileSync5(configPath, renderWranglerConfig(config));
|
|
780
1106
|
await migrate(wrangler, config.d1.name, configPath);
|
|
781
1107
|
report("ok", "Migrations applied");
|
|
782
1108
|
await deploy(wrangler, configPath);
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1109
|
+
if (credentials) {
|
|
1110
|
+
await putSecrets(
|
|
1111
|
+
wrangler,
|
|
1112
|
+
{
|
|
1113
|
+
GLASS_ADMIN_KEY: credentials.adminKey,
|
|
1114
|
+
GLASS_READ_KEY: credentials.readKey,
|
|
1115
|
+
GLASS_SALT_SECRET: credentials.saltSecret
|
|
1116
|
+
// CF_API_TOKEN (lake mode) is set by init and left alone here, so a narrower token you set stays.
|
|
1117
|
+
},
|
|
1118
|
+
configPath,
|
|
1119
|
+
dir
|
|
1120
|
+
);
|
|
1121
|
+
} else {
|
|
1122
|
+
report("skipped", "Worker secrets", "no credentials.json here; the keys set by init stay in place");
|
|
1123
|
+
}
|
|
795
1124
|
config.version = GLASS_VERSION;
|
|
796
1125
|
saveConfig(config, root);
|
|
797
|
-
if (options.json) printJson({ from, to: GLASS_VERSION, host: config.host, replay: Boolean(config.replay) });
|
|
1126
|
+
if (options.json) printJson({ from, to: GLASS_VERSION, host: config.host, replay: Boolean(config.replay), staged: false });
|
|
798
1127
|
else ok(`Glass ${from === GLASS_VERSION ? GLASS_VERSION : `${from} upgraded to ${GLASS_VERSION}`} is live at ${config.host}`);
|
|
1128
|
+
if (!ci2 && !hasCiWorkflow(root) && !options.json) {
|
|
1129
|
+
line(color.dim(` Tip: \`glass ci\` writes ${CI_WORKFLOW_PATH} so future upgrades deploy from CI instead of this machine.`));
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
function ci(options, deps = {}) {
|
|
1133
|
+
const root = deps.root ?? projectRoot();
|
|
1134
|
+
if (!loadConfig(root)) throw new UsageError("No Glass deployment in this project.", "Run `npx @glassanalytics/cli init` first.");
|
|
1135
|
+
const result = writeCiWorkflow(root, { branch: options.branch, secret: options.secret });
|
|
1136
|
+
const next = ciNextSteps(result);
|
|
1137
|
+
if (options.json) {
|
|
1138
|
+
printJson({ ...result, next });
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
(result.status === "kept" ? skip : ok)(`${result.path} ${result.status === "kept" ? "already exists (left as is)" : result.status}`);
|
|
1142
|
+
line();
|
|
1143
|
+
line(color.bold("Finish CI setup"));
|
|
1144
|
+
for (const [i, step] of next.entries()) line(` ${i + 1}. ${step}`);
|
|
799
1145
|
}
|
|
800
1146
|
async function status(options) {
|
|
801
1147
|
const conn = connection();
|
|
@@ -804,18 +1150,24 @@ async function status(options) {
|
|
|
804
1150
|
const events2 = await call("GET", `/api/events?days=1${conn.project ? `&project=${conn.project}` : ""}`).catch(() => ({ events: [] }));
|
|
805
1151
|
const total = events2.events.reduce((a, e) => a + e.count, 0);
|
|
806
1152
|
const last = events2.events.map((e) => e.lastSeen).sort().at(-1);
|
|
807
|
-
const mcp =
|
|
1153
|
+
const mcp = existsSync4(join5(projectRoot(), ".mcp.json"));
|
|
1154
|
+
const cliMismatch = health.version !== GLASS_VERSION;
|
|
1155
|
+
const npmNewer = Boolean(health.latest && isNewerThan(health.latest, health.version));
|
|
1156
|
+
const upgradeAvailable = Boolean(health.upgradeAvailable) || cliMismatch || npmNewer;
|
|
1157
|
+
const upgradeCommand = health.upgradeCommand ?? UPGRADE_COMMAND;
|
|
808
1158
|
const result = {
|
|
809
1159
|
host: conn.host,
|
|
810
1160
|
project: conn.project,
|
|
811
1161
|
version: health.version,
|
|
1162
|
+
latest: health.latest ?? null,
|
|
812
1163
|
cliVersion: GLASS_VERSION,
|
|
813
1164
|
store: health.store,
|
|
814
1165
|
replay: health.replay,
|
|
815
1166
|
eventsLast24h: total,
|
|
816
1167
|
lastEvent: last ?? null,
|
|
817
1168
|
mcpConfigured: mcp,
|
|
818
|
-
upgradeAvailable
|
|
1169
|
+
upgradeAvailable,
|
|
1170
|
+
upgradeCommand,
|
|
819
1171
|
account: config?.accountId
|
|
820
1172
|
};
|
|
821
1173
|
if (options.json) return printJson(result);
|
|
@@ -823,7 +1175,13 @@ async function status(options) {
|
|
|
823
1175
|
(total ? ok : skip)(total ? `${total.toLocaleString("en-US")} events in the last 24 hours, latest ${last}` : "No events in the last 24 hours. Is the snippet installed?");
|
|
824
1176
|
(health.replay ? ok : skip)(health.replay ? "Session replay enabled" : "Session replay disabled (no R2 bucket); `glass upgrade` retries");
|
|
825
1177
|
(mcp ? ok : skip)(mcp ? "MCP server configured in .mcp.json" : "No .mcp.json; run `glass init` again to write agent files");
|
|
826
|
-
if (
|
|
1178
|
+
if (health.latest && isNewerThan(health.latest, health.version)) {
|
|
1179
|
+
skip(`Glass ${health.latest} is available (this deployment is ${health.version}). Run: ${upgradeCommand}`);
|
|
1180
|
+
} else if (cliMismatch) {
|
|
1181
|
+
skip(`This CLI is Glass ${GLASS_VERSION}; the Worker runs ${health.version}. Run: ${upgradeCommand}`);
|
|
1182
|
+
} else {
|
|
1183
|
+
ok(`Up to date (Glass ${health.version})`);
|
|
1184
|
+
}
|
|
827
1185
|
}
|
|
828
1186
|
function key(options) {
|
|
829
1187
|
const conn = connection();
|
|
@@ -838,7 +1196,7 @@ function key(options) {
|
|
|
838
1196
|
// packages/cli/src/commands/init.ts
|
|
839
1197
|
function defaultProjectId(root) {
|
|
840
1198
|
try {
|
|
841
|
-
const pkg = JSON.parse(
|
|
1199
|
+
const pkg = JSON.parse(readFileSync4(join6(root, "package.json"), "utf8"));
|
|
842
1200
|
const id = (pkg.name ?? "").replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
843
1201
|
if (id) return id.slice(0, 63);
|
|
844
1202
|
} catch {
|
|
@@ -847,7 +1205,7 @@ function defaultProjectId(root) {
|
|
|
847
1205
|
}
|
|
848
1206
|
function readPackageJson(root) {
|
|
849
1207
|
try {
|
|
850
|
-
return JSON.parse(
|
|
1208
|
+
return JSON.parse(readFileSync4(join6(root, "package.json"), "utf8"));
|
|
851
1209
|
} catch {
|
|
852
1210
|
return null;
|
|
853
1211
|
}
|
|
@@ -910,7 +1268,7 @@ async function init(options, deps = {}) {
|
|
|
910
1268
|
wantReplay ? `R2 bucket "${name}-replays"` : null,
|
|
911
1269
|
lake ? `R2 bucket "${name}-data" with data catalog, Pipelines streams/sinks for events and identities` : null
|
|
912
1270
|
].filter(Boolean),
|
|
913
|
-
files: [".glass/config.json", ".glass/wrangler.jsonc", ".glass/worker.js", ".glass/migrations/", ".glass/credentials.json (git-ignored)", "AGENTS.md", ".claude/skills/glass/SKILL.md", ".mcp.json"]
|
|
1271
|
+
files: [".glass/config.json", ".glass/wrangler.jsonc", ".glass/worker.js", ".glass/migrations/", ".glass/credentials.json (git-ignored)", "AGENTS.md", ".claude/skills/glass/SKILL.md", ".mcp.json", ...options.ci === false ? [] : [CI_WORKFLOW_PATH]]
|
|
914
1272
|
};
|
|
915
1273
|
if (quiet) printJson(plan);
|
|
916
1274
|
else {
|
|
@@ -944,12 +1302,12 @@ async function init(options, deps = {}) {
|
|
|
944
1302
|
lake: lakeConfig,
|
|
945
1303
|
route,
|
|
946
1304
|
eu,
|
|
947
|
-
version:
|
|
1305
|
+
version: GLASS_VERSION
|
|
948
1306
|
};
|
|
949
1307
|
const credentials = loadCredentials(root) ?? { adminKey: randomToken(), readKey: randomToken(), saltSecret: randomToken() };
|
|
950
1308
|
installWorkerFiles(dir, deps.bundleDir ?? workerBundleDir());
|
|
951
|
-
const configPath =
|
|
952
|
-
|
|
1309
|
+
const configPath = join6(dir, "wrangler.jsonc");
|
|
1310
|
+
writeFileSync6(configPath, renderWranglerConfig(config));
|
|
953
1311
|
saveCredentials(credentials, root);
|
|
954
1312
|
await migrate(wrangler, d1.name, configPath);
|
|
955
1313
|
report("ok", "Database migrated");
|
|
@@ -984,8 +1342,11 @@ async function init(options, deps = {}) {
|
|
|
984
1342
|
const framework = detectFramework(readPackageJson(root));
|
|
985
1343
|
const snippet = FRAMEWORKS[framework].snippet({ host: config.host, project });
|
|
986
1344
|
const written = options.agentFiles === false ? [] : writeAgentFiles({ root, host: config.host, project, snippet });
|
|
1345
|
+
const workflow = options.ci === false ? null : writeCiWorkflow(root);
|
|
1346
|
+
if (workflow && workflow.status !== "kept") written.push(workflow.path);
|
|
1347
|
+
const ci2 = workflow ? { workflow: workflow.path, status: workflow.status, branch: workflow.branch, secret: workflow.secret, next: ciNextSteps(workflow) } : null;
|
|
987
1348
|
if (quiet) {
|
|
988
|
-
printJson({ host: config.host, project, dashboard: `${config.host}/app`, framework, snippet, written, steps });
|
|
1349
|
+
printJson({ host: config.host, project, dashboard: `${config.host}/app`, framework, snippet, written, ci: ci2, steps });
|
|
989
1350
|
return;
|
|
990
1351
|
}
|
|
991
1352
|
line();
|
|
@@ -1001,6 +1362,12 @@ async function init(options, deps = {}) {
|
|
|
1001
1362
|
for (const f of written) line(` ${f}`);
|
|
1002
1363
|
line(` .glass/ ${color.dim("(deployment config: commit it; credentials.json is git-ignored)")}`);
|
|
1003
1364
|
}
|
|
1365
|
+
if (ci2) {
|
|
1366
|
+
line();
|
|
1367
|
+
line(color.bold("Upgrades deploy from CI"));
|
|
1368
|
+
line(color.dim(` ${ci2.workflow} deploys the version pinned in .glass/config.json. Agents: finish these steps now.`));
|
|
1369
|
+
for (const [i, step] of ci2.next.entries()) line(` ${i + 1}. ${step}`);
|
|
1370
|
+
}
|
|
1004
1371
|
line();
|
|
1005
1372
|
line(`Dashboard: ${color.cyan(`${config.host}/app`)} ${color.dim("(sign in with the key from `npx @glassanalytics/cli key`)")}`);
|
|
1006
1373
|
} catch (err) {
|
|
@@ -1053,8 +1420,27 @@ async function mcpBridge(options = {}) {
|
|
|
1053
1420
|
await Promise.all(pending);
|
|
1054
1421
|
}
|
|
1055
1422
|
|
|
1423
|
+
// packages/cli/src/upgrade-check.ts
|
|
1424
|
+
var checked = false;
|
|
1425
|
+
async function warnIfOutdated(options = {}) {
|
|
1426
|
+
if (checked || options.json) return;
|
|
1427
|
+
checked = true;
|
|
1428
|
+
try {
|
|
1429
|
+
const health = await call("GET", "/health");
|
|
1430
|
+
const command = health.upgradeCommand ?? UPGRADE_COMMAND;
|
|
1431
|
+
if (health.upgradeAvailable && health.latest) {
|
|
1432
|
+
skip(`Glass ${health.latest} is available (this deployment is ${health.version}). Run: ${command}`);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
if (health.version !== GLASS_VERSION) {
|
|
1436
|
+
skip(`Worker is Glass ${health.version}; this CLI is ${GLASS_VERSION}. Run: ${command}`);
|
|
1437
|
+
}
|
|
1438
|
+
} catch {
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1056
1442
|
// packages/cli/src/index.ts
|
|
1057
|
-
var HELP = `${color.bold("glass")} ${
|
|
1443
|
+
var HELP = `${color.bold("glass")} ${GLASS_VERSION}: agent-first analytics in your own Cloudflare account
|
|
1058
1444
|
|
|
1059
1445
|
${color.bold("Set up")}
|
|
1060
1446
|
glass init Deploy Glass and wire it into this project
|
|
@@ -1067,8 +1453,16 @@ ${color.bold("Set up")}
|
|
|
1067
1453
|
--eu Keep data in the EU jurisdiction
|
|
1068
1454
|
--no-replay Skip session replay storage
|
|
1069
1455
|
--no-agent-files Don't write AGENTS.md, SKILL.md or .mcp.json
|
|
1456
|
+
--no-ci Don't write .github/workflows/glass.yml
|
|
1070
1457
|
--dry-run Show what would be created
|
|
1071
|
-
glass upgrade
|
|
1458
|
+
glass upgrade Move to this CLI's version of Glass and migrate
|
|
1459
|
+
With .github/workflows/glass.yml: stage into .glass/ only;
|
|
1460
|
+
commit and push, and CI deploys it
|
|
1461
|
+
--ci Deploy as CI (no prompts; default when CI=true)
|
|
1462
|
+
--local Deploy from here even though a CI workflow exists
|
|
1463
|
+
--allow-downgrade Replace a newer deployed Worker with this older one
|
|
1464
|
+
glass ci [--branch main] Write the workflow that deploys Glass from CI
|
|
1465
|
+
--secret <name> Repo secret with the Cloudflare token (default CLOUDFLARE_API_TOKEN)
|
|
1072
1466
|
glass status Check the deployment end to end
|
|
1073
1467
|
glass key [--read] Print the dashboard URL and API key
|
|
1074
1468
|
|
|
@@ -1081,7 +1475,7 @@ ${color.bold("Ask questions")}
|
|
|
1081
1475
|
common: --from, --to, --where prop=value[,\u2026], --project, --sql (print SQL)
|
|
1082
1476
|
glass sql "SELECT \u2026" Read-only SQL against your events
|
|
1083
1477
|
glass events [name] Tracked events, or one event's properties
|
|
1084
|
-
--describe "<text>"
|
|
1478
|
+
--describe "<text>" How/when the event fires (document custom events)
|
|
1085
1479
|
glass person <id> [--delete] One person's properties and events (or erase them)
|
|
1086
1480
|
glass replays [--errors] Recorded sessions
|
|
1087
1481
|
glass replays summary <id> A session as a text transcript
|
|
@@ -1107,6 +1501,8 @@ var STRING_OPTIONS = [
|
|
|
1107
1501
|
"project",
|
|
1108
1502
|
"name",
|
|
1109
1503
|
"route",
|
|
1504
|
+
"branch",
|
|
1505
|
+
"secret",
|
|
1110
1506
|
"from",
|
|
1111
1507
|
"to",
|
|
1112
1508
|
"event",
|
|
@@ -1136,7 +1532,7 @@ var STRING_OPTIONS = [
|
|
|
1136
1532
|
"hypothesis",
|
|
1137
1533
|
"variant"
|
|
1138
1534
|
];
|
|
1139
|
-
var BOOLEAN_OPTIONS = ["json", "lake", "eu", "no-replay", "no-agent-files", "dry-run", "read", "sql", "errors", "enable", "disable", "delete"];
|
|
1535
|
+
var BOOLEAN_OPTIONS = ["json", "lake", "eu", "no-replay", "no-agent-files", "no-ci", "ci", "local", "allow-downgrade", "dry-run", "read", "sql", "errors", "enable", "disable", "delete"];
|
|
1140
1536
|
async function main(argv) {
|
|
1141
1537
|
const { values, positionals } = parseArgs({
|
|
1142
1538
|
args: argv,
|
|
@@ -1151,8 +1547,12 @@ async function main(argv) {
|
|
|
1151
1547
|
});
|
|
1152
1548
|
const flags2 = values;
|
|
1153
1549
|
const [command, sub, arg, ...rest] = positionals;
|
|
1154
|
-
if (flags2.version) return line(
|
|
1550
|
+
if (flags2.version) return line(GLASS_VERSION);
|
|
1155
1551
|
if (!command || command === "help" || flags2.help && !command) return line(HELP);
|
|
1552
|
+
const skipUpgradeWarn = /* @__PURE__ */ new Set(["init", "upgrade", "deploy", "ci", "status", "doctor", "mcp", "key"]);
|
|
1553
|
+
if (command && !skipUpgradeWarn.has(command)) {
|
|
1554
|
+
await warnIfOutdated({ json: Boolean(flags2.json) });
|
|
1555
|
+
}
|
|
1156
1556
|
switch (command) {
|
|
1157
1557
|
case "init":
|
|
1158
1558
|
return init({
|
|
@@ -1165,12 +1565,21 @@ async function main(argv) {
|
|
|
1165
1565
|
eu: flags2.eu === true ? true : void 0,
|
|
1166
1566
|
replay: flags2["no-replay"] ? false : void 0,
|
|
1167
1567
|
agentFiles: flags2["no-agent-files"] ? false : void 0,
|
|
1568
|
+
ci: flags2["no-ci"] ? false : void 0,
|
|
1168
1569
|
dryRun: Boolean(flags2["dry-run"]),
|
|
1169
1570
|
json: Boolean(flags2.json)
|
|
1170
1571
|
});
|
|
1171
1572
|
case "upgrade":
|
|
1172
1573
|
case "deploy":
|
|
1173
|
-
return upgrade({
|
|
1574
|
+
return upgrade({
|
|
1575
|
+
token: str3(flags2.token),
|
|
1576
|
+
json: Boolean(flags2.json),
|
|
1577
|
+
ci: flags2.ci === true ? true : void 0,
|
|
1578
|
+
local: Boolean(flags2.local),
|
|
1579
|
+
allowDowngrade: Boolean(flags2["allow-downgrade"])
|
|
1580
|
+
});
|
|
1581
|
+
case "ci":
|
|
1582
|
+
return ci({ branch: str3(flags2.branch), secret: str3(flags2.secret), json: Boolean(flags2.json) });
|
|
1174
1583
|
case "status":
|
|
1175
1584
|
case "doctor":
|
|
1176
1585
|
return status({ json: Boolean(flags2.json) });
|