@gurulu/cli 0.4.7 → 1.0.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.
Files changed (190) hide show
  1. package/LICENSE +92 -0
  2. package/README.md +35 -106
  3. package/dist/bin.d.ts +3 -0
  4. package/dist/bin.d.ts.map +1 -0
  5. package/dist/bin.js +25751 -0
  6. package/dist/commands/auth.d.ts +23 -20
  7. package/dist/commands/auth.d.ts.map +1 -0
  8. package/dist/commands/doctor.d.ts +20 -6
  9. package/dist/commands/doctor.d.ts.map +1 -0
  10. package/dist/commands/init.d.ts +33 -11
  11. package/dist/commands/init.d.ts.map +1 -0
  12. package/dist/commands/pull.d.ts +13 -0
  13. package/dist/commands/pull.d.ts.map +1 -0
  14. package/dist/commands/push.d.ts +40 -0
  15. package/dist/commands/push.d.ts.map +1 -0
  16. package/dist/commands/validate.d.ts +36 -0
  17. package/dist/commands/validate.d.ts.map +1 -0
  18. package/dist/index.d.ts +4 -1
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +25326 -876
  21. package/dist/lib/api.d.ts +139 -0
  22. package/dist/lib/api.d.ts.map +1 -0
  23. package/dist/lib/codegen.d.ts +4 -0
  24. package/dist/lib/codegen.d.ts.map +1 -0
  25. package/dist/lib/config.d.ts +43 -0
  26. package/dist/lib/config.d.ts.map +1 -0
  27. package/dist/lib/detect.d.ts +27 -0
  28. package/dist/lib/detect.d.ts.map +1 -0
  29. package/dist/lib/detect.js +106 -0
  30. package/dist/lib/exec-install.d.ts +21 -0
  31. package/dist/lib/exec-install.d.ts.map +1 -0
  32. package/dist/lib/install-plan.d.ts +25 -0
  33. package/dist/lib/install-plan.d.ts.map +1 -0
  34. package/dist/lib/install-plan.js +161 -0
  35. package/package.json +51 -20
  36. package/bin/gurulu.js +0 -2
  37. package/dist/api-client.d.ts +0 -33
  38. package/dist/api-client.js +0 -175
  39. package/dist/commands/add-server.d.ts +0 -9
  40. package/dist/commands/add-server.js +0 -162
  41. package/dist/commands/alerts.d.ts +0 -27
  42. package/dist/commands/alerts.js +0 -309
  43. package/dist/commands/api-keys.d.ts +0 -20
  44. package/dist/commands/api-keys.js +0 -130
  45. package/dist/commands/attribution.d.ts +0 -22
  46. package/dist/commands/attribution.js +0 -111
  47. package/dist/commands/audiences.d.ts +0 -23
  48. package/dist/commands/audiences.js +0 -243
  49. package/dist/commands/audit.d.ts +0 -20
  50. package/dist/commands/audit.js +0 -130
  51. package/dist/commands/auth.js +0 -249
  52. package/dist/commands/chat.d.ts +0 -19
  53. package/dist/commands/chat.js +0 -118
  54. package/dist/commands/config.d.ts +0 -10
  55. package/dist/commands/config.js +0 -92
  56. package/dist/commands/consent.d.ts +0 -27
  57. package/dist/commands/consent.js +0 -233
  58. package/dist/commands/conversion-paths.d.ts +0 -19
  59. package/dist/commands/conversion-paths.js +0 -55
  60. package/dist/commands/db.d.ts +0 -25
  61. package/dist/commands/db.js +0 -330
  62. package/dist/commands/destinations.d.ts +0 -20
  63. package/dist/commands/destinations.js +0 -191
  64. package/dist/commands/doctor.js +0 -360
  65. package/dist/commands/errors.d.ts +0 -27
  66. package/dist/commands/errors.js +0 -121
  67. package/dist/commands/events.d.ts +0 -33
  68. package/dist/commands/events.js +0 -371
  69. package/dist/commands/experiments.d.ts +0 -22
  70. package/dist/commands/experiments.js +0 -264
  71. package/dist/commands/funnels.d.ts +0 -17
  72. package/dist/commands/funnels.js +0 -203
  73. package/dist/commands/goals.d.ts +0 -18
  74. package/dist/commands/goals.js +0 -214
  75. package/dist/commands/heatmap.d.ts +0 -27
  76. package/dist/commands/heatmap.js +0 -112
  77. package/dist/commands/identity.d.ts +0 -29
  78. package/dist/commands/identity.js +0 -328
  79. package/dist/commands/init.js +0 -215
  80. package/dist/commands/insights.d.ts +0 -10
  81. package/dist/commands/insights.js +0 -77
  82. package/dist/commands/install.d.ts +0 -259
  83. package/dist/commands/install.js +0 -1590
  84. package/dist/commands/login.d.ts +0 -20
  85. package/dist/commands/login.js +0 -170
  86. package/dist/commands/logout.d.ts +0 -10
  87. package/dist/commands/logout.js +0 -41
  88. package/dist/commands/playground.d.ts +0 -11
  89. package/dist/commands/playground.js +0 -47
  90. package/dist/commands/releases.d.ts +0 -17
  91. package/dist/commands/releases.js +0 -54
  92. package/dist/commands/replay.d.ts +0 -18
  93. package/dist/commands/replay.js +0 -64
  94. package/dist/commands/secrets.d.ts +0 -19
  95. package/dist/commands/secrets.js +0 -145
  96. package/dist/commands/setup.d.ts +0 -21
  97. package/dist/commands/setup.js +0 -67
  98. package/dist/commands/sites.d.ts +0 -18
  99. package/dist/commands/sites.js +0 -139
  100. package/dist/commands/skad.d.ts +0 -18
  101. package/dist/commands/skad.js +0 -53
  102. package/dist/commands/sourcemap.d.ts +0 -33
  103. package/dist/commands/sourcemap.js +0 -204
  104. package/dist/commands/status.d.ts +0 -7
  105. package/dist/commands/status.js +0 -136
  106. package/dist/commands/upgrade.d.ts +0 -21
  107. package/dist/commands/upgrade.js +0 -183
  108. package/dist/commands/warehouse.d.ts +0 -20
  109. package/dist/commands/warehouse.js +0 -65
  110. package/dist/commands/warehouses.d.ts +0 -17
  111. package/dist/commands/warehouses.js +0 -182
  112. package/dist/commands/watch.d.ts +0 -45
  113. package/dist/commands/watch.js +0 -258
  114. package/dist/commands/whoami.d.ts +0 -9
  115. package/dist/commands/whoami.js +0 -50
  116. package/dist/config.d.ts +0 -75
  117. package/dist/config.js +0 -329
  118. package/dist/frameworks/detect.d.ts +0 -8
  119. package/dist/frameworks/detect.js +0 -458
  120. package/dist/install-intent-proposal.d.ts +0 -99
  121. package/dist/install-intent-proposal.js +0 -202
  122. package/dist/utils/api.d.ts +0 -20
  123. package/dist/utils/api.js +0 -47
  124. package/dist/utils/config.d.ts +0 -13
  125. package/dist/utils/config.js +0 -30
  126. package/dist/utils/confirm.d.ts +0 -17
  127. package/dist/utils/confirm.js +0 -40
  128. package/dist/utils/dry-run.d.ts +0 -20
  129. package/dist/utils/dry-run.js +0 -67
  130. package/dist/utils/from-file.d.ts +0 -9
  131. package/dist/utils/from-file.js +0 -72
  132. package/dist/utils/redact.d.ts +0 -14
  133. package/dist/utils/redact.js +0 -48
  134. package/dist/utils/ui.d.ts +0 -14
  135. package/dist/utils/ui.js +0 -59
  136. package/scripts/.gitkeep +0 -0
  137. package/scripts/README-gurulu-agentic-install.md +0 -114
  138. package/scripts/README-gurulu-scan.md +0 -98
  139. package/scripts/audit-cli-scopes.mjs +0 -204
  140. package/scripts/backfill-tenant-id.mjs +0 -172
  141. package/scripts/backfill-tenant-links.ts +0 -252
  142. package/scripts/backup-clickhouse.sh +0 -27
  143. package/scripts/backup-postgres.sh +0 -19
  144. package/scripts/bootstrap-runtime-schema.mjs +0 -87
  145. package/scripts/bootstrap-stripe.mjs +0 -158
  146. package/scripts/gurulu-agentic-install.lib.cjs +0 -762
  147. package/scripts/gurulu-agentic-install.mjs +0 -623
  148. package/scripts/gurulu-scan.lib.cjs +0 -1509
  149. package/scripts/gurulu-scan.mjs +0 -91
  150. package/scripts/gurulu-verify-install.lib.cjs +0 -334
  151. package/scripts/gurulu-verify-install.mjs +0 -59
  152. package/scripts/init-ssl.sh +0 -26
  153. package/scripts/migrate-flow-graph-enums.sh +0 -86
  154. package/scripts/monitor-disk.sh +0 -24
  155. package/scripts/patches/astro.patch.cjs +0 -74
  156. package/scripts/patches/auto-instrument/ast-helper.cjs +0 -480
  157. package/scripts/patches/auto-instrument/astro.cjs +0 -273
  158. package/scripts/patches/auto-instrument/express.cjs +0 -383
  159. package/scripts/patches/auto-instrument/fastify.cjs +0 -262
  160. package/scripts/patches/auto-instrument/hono.cjs +0 -392
  161. package/scripts/patches/auto-instrument/index.cjs +0 -80
  162. package/scripts/patches/auto-instrument/nestjs.cjs +0 -286
  163. package/scripts/patches/auto-instrument/nextjs-app-router.cjs +0 -345
  164. package/scripts/patches/auto-instrument/nextjs-pages.cjs +0 -361
  165. package/scripts/patches/auto-instrument/remix.cjs +0 -168
  166. package/scripts/patches/auto-instrument/sdk-helper-map.cjs +0 -241
  167. package/scripts/patches/auto-instrument/singleton-helper.cjs +0 -193
  168. package/scripts/patches/auto-instrument/sveltekit.cjs +0 -161
  169. package/scripts/patches/auto-instrument/vite-react.cjs +0 -37
  170. package/scripts/patches/auto-instrument/vue.cjs +0 -196
  171. package/scripts/patches/express.patch.cjs +0 -99
  172. package/scripts/patches/fastify.patch.cjs +0 -108
  173. package/scripts/patches/index.cjs +0 -300
  174. package/scripts/patches/nestjs.patch.cjs +0 -112
  175. package/scripts/patches/nextjs-app-router.patch.cjs +0 -97
  176. package/scripts/patches/nextjs-pages.patch.cjs +0 -97
  177. package/scripts/patches/remix.patch.cjs +0 -75
  178. package/scripts/patches/sveltekit.patch.cjs +0 -72
  179. package/scripts/patches/vite-react.patch.cjs +0 -73
  180. package/scripts/patches/vue.patch.cjs +0 -82
  181. package/scripts/renew-ssl.sh +0 -14
  182. package/scripts/resolve-migration.sh +0 -23
  183. package/scripts/seed-cli-dev-keys.mjs +0 -130
  184. package/scripts/seed-test-data.mjs +0 -391
  185. package/scripts/spike-browserless.ts +0 -65
  186. package/scripts/tenant-pivot-consistency-check.mjs +0 -205
  187. package/scripts/tenant-pivot-phase-3-cleanup.lib.cjs +0 -258
  188. package/scripts/tenant-pivot-phase-3-cleanup.mjs +0 -98
  189. package/scripts/test-identity-resolution.ts +0 -804
  190. package/scripts/validate-gurulu-schemas.mjs +0 -79
package/dist/utils/ui.js DELETED
@@ -1,59 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.dim = exports.bold = exports.cyan = exports.yellow = exports.red = exports.green = void 0;
4
- exports.success = success;
5
- exports.error = error;
6
- exports.warn = warn;
7
- exports.info = info;
8
- exports.step = step;
9
- exports.banner = banner;
10
- exports.prompt = prompt;
11
- exports.promptSelect = promptSelect;
12
- const green = (s) => `\x1b[32m${s}\x1b[0m`;
13
- exports.green = green;
14
- const red = (s) => `\x1b[31m${s}\x1b[0m`;
15
- exports.red = red;
16
- const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
17
- exports.yellow = yellow;
18
- const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
19
- exports.cyan = cyan;
20
- const bold = (s) => `\x1b[1m${s}\x1b[0m`;
21
- exports.bold = bold;
22
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
23
- exports.dim = dim;
24
- function success(msg) { console.log(`${(0, exports.green)('\u2713')} ${msg}`); }
25
- function error(msg) { console.log(`${(0, exports.red)('\u2717')} ${msg}`); }
26
- function warn(msg) { console.log(`${(0, exports.yellow)('\u26A0')} ${msg}`); }
27
- function info(msg) { console.log(`${(0, exports.cyan)('\u2139')} ${msg}`); }
28
- function step(msg) { console.log(` ${(0, exports.dim)('\u2192')} ${msg}`); }
29
- function banner() {
30
- console.log('');
31
- console.log((0, exports.bold)(' Gurulu.io'));
32
- console.log((0, exports.dim)(' Autonomous Analytics Setup'));
33
- console.log('');
34
- }
35
- function prompt(question) {
36
- return new Promise((resolve) => {
37
- const readline = require('readline');
38
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
39
- rl.question(question, (answer) => {
40
- rl.close();
41
- resolve(answer.trim());
42
- });
43
- });
44
- }
45
- function promptSelect(question, options) {
46
- return new Promise((resolve) => {
47
- options.forEach((opt, i) => {
48
- console.log(` ${(0, exports.dim)(`${i + 1}.`)} ${opt}`);
49
- });
50
- console.log('');
51
- const readline = require('readline');
52
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
53
- rl.question(question, (answer) => {
54
- rl.close();
55
- const idx = parseInt(answer.trim(), 10) - 1;
56
- resolve(idx >= 0 && idx < options.length ? idx : 0);
57
- });
58
- });
59
- }
package/scripts/.gitkeep DELETED
File without changes
@@ -1,114 +0,0 @@
1
- # gurulu-agentic-install (Phase 14 B3 alpha)
2
-
3
- Takes a **gurulu-scan** JSON document (Phase 13 B2) and produces the 10
4
- `.gurulu/` artifacts consumed by the Phase 13 B1 runtime loader. This is the
5
- **alpha** — config-only, no source code edits and no SDK injection.
6
-
7
- ## Pipeline
8
-
9
- ```
10
- your repo
11
- |
12
- | node scripts/gurulu-scan.mjs . (Phase 13 B2)
13
- v
14
- scan-output.json
15
- |
16
- | node scripts/gurulu-agentic-install.mjs (Phase 14 B3 — this tool)
17
- v
18
- .gurulu/
19
- install-plan.json
20
- core-config.json
21
- web.config.json
22
- app.config.json
23
- server-map.json
24
- db-map.json
25
- flow-seeds.json
26
- milestone-rules.json
27
- correlation-map.json
28
- connectors.json
29
- |
30
- | src/lib/gurulu-config/loader.ts (Phase 13 B1)
31
- v
32
- Runtime ready
33
- ```
34
-
35
- ## Usage
36
-
37
- ```bash
38
- # 1. Scan the target repo
39
- node scripts/gurulu-scan.mjs /path/to/repo --output scan.json --quiet
40
-
41
- # 2. Generate .gurulu/ artifacts
42
- node scripts/gurulu-agentic-install.mjs scan.json \
43
- --site-id site_acme --tenant-id tnt_acme \
44
- --output .gurulu
45
-
46
- # Optional: preview without writing
47
- node scripts/gurulu-agentic-install.mjs scan.json \
48
- --site-id site_acme --tenant-id tnt_acme --dry-run
49
-
50
- # stdin input
51
- cat scan.json | node scripts/gurulu-agentic-install.mjs - \
52
- --site-id site_acme --tenant-id tnt_acme
53
- ```
54
-
55
- Flags:
56
-
57
- | flag | purpose |
58
- | --------------- | ------------------------------------------------------------- |
59
- | `--site-id` | stable site identifier (required) |
60
- | `--tenant-id` | tenant identifier (required) |
61
- | `--output` | output directory (default `.gurulu/`) |
62
- | `--domains` | comma-separated hosts for `install-plan.domains` |
63
- | `--dry-run` | validate + report without writing |
64
- | `--quiet` | suppress non-error output |
65
-
66
- ## What the alpha does
67
-
68
- - Validates every generated artifact against
69
- `schemas/gurulu/*.schema.json` (Phase 10 W4.1) before writing.
70
- Atomic: on any validation error nothing is written.
71
- - Derives **business surfaces** from the scanner's `routes` + `mutations`
72
- (signup / checkout / subscription / lead / onboarding) via keyword
73
- heuristics.
74
- - Produces **route → flow** suggestions for `web.config.json`.
75
- - Produces **endpoint → event** suggestions for `server-map.json` (POST
76
- verbs only).
77
- - Produces **table → event** suggestions for `db-map.json` from ORM
78
- mutations (Prisma / Mongoose / Drizzle / raw SQL via the scanner).
79
- - Produces **flow seeds** and **milestone rules** per surface.
80
- - Produces a **correlation map** bridging client → server → db events.
81
- - Produces a **connectors** placeholder (empty — populated via dashboard).
82
- - Produces a mobile SDK **skeleton** (`app.config.json` with `ios`
83
- defaults). Alpha does not auto-detect mobile platforms.
84
-
85
- ## What the alpha does NOT do
86
-
87
- - No source code edits. The tool never touches application source.
88
- - No SDK install. It does not add dependencies or wire up imports.
89
- - No CRM / outbound connector setup (`connectors.json` stays empty).
90
- - No mobile platform detection.
91
- - No code patches — that's Phase 15.
92
-
93
- ## Roadmap → full agentic install (Phase 15)
94
-
95
- 1. Auto-add Web / iOS / Android SDK imports to the host repo.
96
- 2. Auto-edit `layout.tsx` / `_app.tsx` / middleware to bootstrap the SDK.
97
- 3. Emit per-route + per-mutation code patches (PR-ready diffs).
98
- 4. Self-healing: re-scan on CI and update `.gurulu/` automatically.
99
- 5. Auto-detect mobile platforms + populate `app.config.json` properly.
100
- 6. Wire detected external services (Stripe / Shopify / Segment / HubSpot)
101
- into `connectors.json`.
102
-
103
- ## Implementation notes
104
-
105
- - Generator logic lives in `scripts/gurulu-agentic-install.lib.cjs` so it
106
- can be `require()`'d from both the ESM CLI wrapper and ts-jest tests,
107
- mirroring the layout of `scripts/gurulu-scan.lib.cjs`.
108
- - Validation uses `ajv@6.14.0` already installed at the repo root.
109
- - Every artifact is generated with sensible defaults when the scanner
110
- supplies nothing (empty routes / mutations still yields a valid
111
- bundle). `db-map.tables`, `flow-seeds.flows`, `milestone-rules.rules`
112
- and `correlation-map.links` all have `minItems: 1` in their schemas;
113
- the generator plants a placeholder entry rather than producing an
114
- invalid document.
@@ -1,98 +0,0 @@
1
- # gurulu-scan — repo scanner spike (Phase 13 B2)
2
-
3
- `scripts/gurulu-scan.mjs` is a standalone Node script that scans any
4
- repository path and emits a JSON document shaped like
5
- [`install-plan.schema.json`](../schemas/gurulu/install-plan.schema.json). It is
6
- the precursor to a real agentic installer: for now it **only gathers signal**
7
- — it never writes code or touches the host repo.
8
-
9
- ## Usage
10
-
11
- ```bash
12
- # Print the plan to stdout
13
- node scripts/gurulu-scan.mjs <repo-path>
14
-
15
- # Write to a file
16
- node scripts/gurulu-scan.mjs <repo-path> --output /tmp/scan.json
17
-
18
- # Suppress progress logs (useful when piping)
19
- node scripts/gurulu-scan.mjs <repo-path> --quiet > /tmp/scan.json
20
- ```
21
-
22
- Self-scan example:
23
-
24
- ```bash
25
- node scripts/gurulu-scan.mjs . --quiet > /tmp/scan-self.json
26
- ```
27
-
28
- ## What it detects
29
-
30
- | Area | Signal | Notes |
31
- | --- | --- | --- |
32
- | Framework | `package.json` deps + devDeps | Next.js, Nest, Remix, SvelteKit, Astro, Fastify, Express, Vite + React, Vue |
33
- | ORM | deps + schema files | Prisma (`prisma/schema.prisma`), Drizzle (`drizzle.config.*`), TypeORM, Mongoose, Sequelize, Kysely |
34
- | Auth | deps | NextAuth, Clerk, Supabase (`+auth-helpers`), Firebase, Lucia, Passport, raw `jsonwebtoken` |
35
- | Routes | filesystem walk | Next.js **App Router** (`**/app/api/**/route.{ts,js}`) and **Pages Router** (`**/pages/api/**`). Methods inferred from exported handlers / `req.method` checks. |
36
- | Mutations | regex grep over `*.ts(x)`, `*.js(x)`, `*.mjs`, `*.cjs` | `prisma.<model>.{create,update,upsert,delete,...}`, `db.{insert,update,delete}(...)`, Mongoose `Model.{create,findOneAnd*,updateOne,...}`, raw `INSERT INTO / UPDATE / DELETE FROM` in template literals. De-duplicated and capped at 200 entries. |
37
-
38
- ## Output shape
39
-
40
- ```jsonc
41
- {
42
- "site_id": null,
43
- "domains": [],
44
- "framework": { "name": "nextjs", "version": "14.2.35" },
45
- "orm": { "name": "prisma", "schemaPath": "prisma/schema.prisma" },
46
- "auth": { "name": "nextauth" },
47
- "routes": [
48
- { "path": "/api/users", "methods": ["GET", "POST"], "file": "app/api/users/route.ts" }
49
- ],
50
- "businessSurfaces": [],
51
- "mutations": [
52
- {
53
- "file": "app/api/users/route.ts",
54
- "line": 12,
55
- "model": "user",
56
- "operation": "create",
57
- "snippet": "const user = await prisma.user.create({..."
58
- }
59
- ],
60
- "scannedAt": "2026-04-13T12:34:56.000Z",
61
- "scanVersion": "0.1.0"
62
- }
63
- ```
64
-
65
- Conceptually mapped to
66
- [`schemas/gurulu/install-plan.schema.json`](../schemas/gurulu/install-plan.schema.json)
67
- — the scanner emits the signal a later stage will transform into the schema's
68
- `stack`, `surfaces` and `suggested_milestones`.
69
-
70
- ## Known limitations (spike scope)
71
-
72
- - **No Express / Fastify / NestJS route detection.** Routes are returned as
73
- `[]` with `routeDetectionNote: "detection_not_implemented_for_<framework>"`.
74
- Handler introspection for imperative frameworks is a follow-up.
75
- - **Mutation regex is best-effort.** It only matches syntactic patterns — it
76
- has no semantic understanding of the code. Renamed clients (e.g.
77
- `const p = prismaClient; p.user.create(...)`) will be missed.
78
- - **No call-graph analysis.** Mutations are not mapped back to routes.
79
- - **No `businessSurfaces` inference.** That lives in a later work unit — the
80
- field is always emitted as `[]` for now.
81
- - **`site_id` / `domains`** are null / empty — they are assigned by the
82
- control plane, not discoverable locally.
83
- - **No bundler / monorepo awareness.** It assumes a single app rooted at the
84
- provided path. `src/` is preferred as the walk root when present, otherwise
85
- the repo root is used.
86
-
87
- ## Implementation notes
88
-
89
- Detection logic lives in `scripts/gurulu-scan.lib.cjs` (CommonJS) so that the
90
- ESM `scripts/gurulu-scan.mjs` CLI wrapper and the ts-jest test suite
91
- (`tests/gurulu-scan.test.ts`) can share a single module. The `.mjs` file
92
- re-exports the library via `createRequire` and adds the CLI entry point.
93
-
94
- Run the test suite with:
95
-
96
- ```bash
97
- npx jest tests/gurulu-scan.test.ts
98
- ```
@@ -1,204 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Phase 20 W3 C2 — Scope audit script.
4
- *
5
- * Scans src/app/api/cli/** /route.ts for `requireCliAuth` calls, extracts
6
- * the scope strings, and validates them against the catalog in
7
- * src/lib/cli/scopes.ts. Fails with exit code 1 if any route uses an
8
- * unknown scope or any scoped surface is missing a required scope.
9
- *
10
- * Flags:
11
- * --json Emit JSON instead of the human table.
12
- * --root <path> Override repo root (default: script parent).
13
- *
14
- * Exit codes:
15
- * 0 — clean
16
- * 1 — unknown scope or missing requireCliAuth
17
- */
18
-
19
- import { promises as fs } from 'node:fs';
20
- import path from 'node:path';
21
- import { fileURLToPath } from 'node:url';
22
-
23
- const __filename = fileURLToPath(import.meta.url);
24
- const __dirname = path.dirname(__filename);
25
-
26
- const argv = process.argv.slice(2);
27
- const jsonMode = argv.includes('--json');
28
- const rootArgIdx = argv.indexOf('--root');
29
- const repoRoot =
30
- rootArgIdx >= 0 && argv[rootArgIdx + 1]
31
- ? path.resolve(argv[rootArgIdx + 1])
32
- : path.resolve(__dirname, '..');
33
-
34
- const SCOPES_FILE = path.join(repoRoot, 'src/lib/cli/scopes.ts');
35
- const ROUTES_ROOT = path.join(repoRoot, 'src/app/api/cli');
36
-
37
- // Routes that intentionally do not require CLI auth (public bootstrap
38
- // endpoints). Anything else missing requireCliAuth fails the audit.
39
- const UNAUTHED_ALLOWLIST = [
40
- 'verify-key/route.ts',
41
- 'device-link/start/route.ts',
42
- 'device-link/approve/route.ts',
43
- 'device-link/deny/route.ts',
44
- 'device-link/poll/route.ts',
45
- ];
46
-
47
- async function loadScopeCatalog(file) {
48
- const content = await fs.readFile(file, 'utf8');
49
- const match = content.match(/CLI_SCOPES\s*=\s*\[([\s\S]*?)\]\s*as const/);
50
- if (!match) {
51
- throw new Error(`Cannot parse CLI_SCOPES from ${file}`);
52
- }
53
- const catalog = new Set();
54
- const re = /'([^']+)'|"([^"]+)"/g;
55
- let m;
56
- while ((m = re.exec(match[1])) !== null) {
57
- catalog.add(m[1] || m[2]);
58
- }
59
- return catalog;
60
- }
61
-
62
- async function walk(dir) {
63
- const out = [];
64
- let entries;
65
- try {
66
- entries = await fs.readdir(dir, { withFileTypes: true });
67
- } catch (err) {
68
- if (err.code === 'ENOENT') return out;
69
- throw err;
70
- }
71
- for (const entry of entries) {
72
- const full = path.join(dir, entry.name);
73
- if (entry.isDirectory()) {
74
- out.push(...(await walk(full)));
75
- } else if (entry.isFile() && entry.name === 'route.ts') {
76
- out.push(full);
77
- }
78
- }
79
- return out;
80
- }
81
-
82
- function extractScopes(source) {
83
- // Matches both:
84
- // requireCliAuth(req, { requiredScopes: ['a', 'b'] })
85
- // requireWriteAuth(req, { requiredScopes: ['a:write'] }) // Phase 20 W2
86
- // Multiline-tolerant via the [\s\S] dotall trick.
87
- const calls = [];
88
- const callRe =
89
- /require(?:CliAuth|WriteAuth)\s*\([\s\S]*?requiredScopes\s*:\s*\[([^\]]*)\][\s\S]*?\)/g;
90
- let m;
91
- while ((m = callRe.exec(source)) !== null) {
92
- const inner = m[1];
93
- const scopes = [];
94
- const strRe = /'([^']+)'|"([^"]+)"/g;
95
- let s;
96
- while ((s = strRe.exec(inner)) !== null) {
97
- scopes.push(s[1] || s[2]);
98
- }
99
- calls.push(scopes);
100
- }
101
- const usesAuth = /require(?:CliAuth|WriteAuth)\s*\(/.test(source);
102
- const hasBareCall = usesAuth && calls.length === 0;
103
- return { scopedCalls: calls, hasBareCall, usesAuth };
104
- }
105
-
106
- function relPath(repoRoot, full) {
107
- return path.relative(repoRoot, full).split(path.sep).join('/');
108
- }
109
-
110
- function allowlistMatch(rel) {
111
- return UNAUTHED_ALLOWLIST.some((suffix) => rel.endsWith(`api/cli/${suffix}`));
112
- }
113
-
114
- async function main() {
115
- let catalog;
116
- try {
117
- catalog = await loadScopeCatalog(SCOPES_FILE);
118
- } catch (err) {
119
- console.error(err.message);
120
- process.exit(1);
121
- }
122
-
123
- const files = await walk(ROUTES_ROOT);
124
- const rows = [];
125
- const errors = [];
126
-
127
- for (const file of files) {
128
- const rel = relPath(repoRoot, file);
129
- const source = await fs.readFile(file, 'utf8');
130
- const { scopedCalls, hasBareCall, usesAuth } = extractScopes(source);
131
- const allScopes = scopedCalls.flat();
132
- const unknown = allScopes.filter((s) => !catalog.has(s));
133
- void hasBareCall;
134
- const routePath = rel
135
- .replace(/^src\/app\/api\//, '/api/')
136
- .replace(/\/route\.ts$/, '')
137
- .replace(/\[([^\]]+)\]/g, ':$1');
138
-
139
- if (allowlistMatch(rel)) {
140
- rows.push({ route: routePath, file: rel, scopes: [], status: 'public' });
141
- continue;
142
- }
143
-
144
- if (scopedCalls.length === 0 && !usesAuth) {
145
- errors.push({ file: rel, error: 'missing_requireCliAuth' });
146
- rows.push({
147
- route: routePath,
148
- file: rel,
149
- scopes: [],
150
- status: 'missing_auth',
151
- });
152
- continue;
153
- }
154
-
155
- if (unknown.length > 0) {
156
- errors.push({ file: rel, error: 'unknown_scope', scopes: unknown });
157
- }
158
- rows.push({
159
- route: routePath,
160
- file: rel,
161
- scopes: allScopes,
162
- status: unknown.length > 0 ? 'unknown_scope' : 'ok',
163
- });
164
- }
165
-
166
- rows.sort((a, b) => a.route.localeCompare(b.route));
167
-
168
- if (jsonMode) {
169
- process.stdout.write(
170
- JSON.stringify(
171
- { routes: rows, errors, catalogSize: catalog.size },
172
- null,
173
- 2,
174
- ) + '\n',
175
- );
176
- } else {
177
- const pad = (s, n) => s + ' '.repeat(Math.max(0, n - s.length));
178
- process.stdout.write(
179
- pad('ROUTE', 55) + pad('SCOPES', 45) + 'STATUS\n',
180
- );
181
- process.stdout.write('-'.repeat(110) + '\n');
182
- for (const r of rows) {
183
- process.stdout.write(
184
- pad(r.route, 55) +
185
- pad(r.scopes.join(',') || '-', 45) +
186
- r.status +
187
- '\n',
188
- );
189
- }
190
- process.stdout.write(
191
- `\n${rows.length} routes scanned, ${errors.length} errors.\n`,
192
- );
193
- for (const e of errors) {
194
- process.stdout.write(` ✗ ${e.file}: ${e.error} ${JSON.stringify(e.scopes || '')}\n`);
195
- }
196
- }
197
-
198
- process.exit(errors.length > 0 ? 1 : 0);
199
- }
200
-
201
- main().catch((err) => {
202
- console.error(err);
203
- process.exit(1);
204
- });
@@ -1,172 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Phase 16 B2 — Backfill `tenant_id` on historical `events_raw` rows.
4
- *
5
- * For every site→tenant mapping in Prisma, issues one `ALTER TABLE ... UPDATE`
6
- * per monthly partition (`toYYYYMM(date)`), chunked so a single mutation never
7
- * has to scan the whole table. Dry-run by default — pass `--apply` to actually
8
- * execute the mutations.
9
- *
10
- * Usage:
11
- * node scripts/backfill-tenant-id.mjs # dry-run
12
- * node scripts/backfill-tenant-id.mjs --apply # real run
13
- * node scripts/backfill-tenant-id.mjs --partition 202604 # single month
14
- *
15
- * Env:
16
- * CLICKHOUSE_URL, CLICKHOUSE_DATABASE
17
- * BACKFILL_FAKE_SITE_MAP=1 — use an injected fake map instead of Prisma
18
- * (used by tests / smoke tests).
19
- *
20
- * NOTE: intentionally NOT run in dev — this exists for ops to run once after
21
- * the tenant_id column was added, against historical rows where tenant_id is
22
- * empty/null. Do not invoke in CI.
23
- *
24
- * Phase 17 B1 update:
25
- * The write path now stamps `tenant_id` natively on every new row via
26
- * `src/lib/ingest/envelope-normalizer.ts` + `tenant-resolver.ts`. That
27
- * means this backfill only needs to cover the historical window BEFORE
28
- * Phase 17 B1 shipped — new ingests already land with tenant_id populated.
29
- * If ops needs to re-run the backfill they can safely `--partition`-scope
30
- * to the pre-cutover months only. See PHASE-17-ROADMAP.md §B1.
31
- */
32
-
33
- function parseArgs(argv) {
34
- const args = { apply: false, partitions: null };
35
- for (let i = 2; i < argv.length; i++) {
36
- const a = argv[i];
37
- if (a === '--apply') args.apply = true;
38
- else if (a === '--partition') args.partitions = [argv[++i]];
39
- else if (a === '--help' || a === '-h') args.help = true;
40
- }
41
- return args;
42
- }
43
-
44
- function printHelp() {
45
- console.log(
46
- `\nUsage: node scripts/backfill-tenant-id.mjs [--apply] [--partition YYYYMM]\n\n` +
47
- ` --apply Actually execute ALTER TABLE UPDATE. Default is dry-run.\n` +
48
- ` --partition Only process the given YYYYMM partition.\n`
49
- );
50
- }
51
-
52
- async function clickhouseQuery(query, params = {}) {
53
- const CLICKHOUSE_URL = process.env.CLICKHOUSE_URL || 'http://localhost:8123';
54
- const CLICKHOUSE_DB = process.env.CLICKHOUSE_DATABASE || 'gurulu';
55
- const url = new URL(CLICKHOUSE_URL);
56
- url.searchParams.set('database', CLICKHOUSE_DB);
57
- url.searchParams.set('default_format', 'JSON');
58
- for (const [k, v] of Object.entries(params)) {
59
- url.searchParams.set(`param_${k}`, String(v));
60
- }
61
- const res = await fetch(url.toString(), {
62
- method: 'POST',
63
- body: query,
64
- headers: { 'Content-Type': 'text/plain' },
65
- });
66
- if (!res.ok) throw new Error(`ClickHouse error ${res.status}: ${await res.text()}`);
67
- return res.json();
68
- }
69
-
70
- function escapeSql(s) {
71
- return String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
72
- }
73
-
74
- /**
75
- * Resolve the site→tenant map.
76
- *
77
- * Exported for tests — tests pass `fakeMap` directly; the real CLI uses
78
- * Prisma unless BACKFILL_FAKE_SITE_MAP=1 (which makes the function return an
79
- * in-memory fixture so scripts/tests can smoke-test the query builder without
80
- * a DB).
81
- */
82
- export async function loadSiteTenantMap(fakeMap) {
83
- if (fakeMap) return fakeMap;
84
- if (process.env.BACKFILL_FAKE_SITE_MAP === '1') {
85
- return {
86
- 'site-fake-1': 'tenant-fake-A',
87
- 'site-fake-2': 'tenant-fake-A',
88
- 'site-fake-3': 'tenant-fake-B',
89
- };
90
- }
91
- const { PrismaClient } = await import('@prisma/client');
92
- const prisma = new PrismaClient();
93
- try {
94
- const sites = await prisma.site.findMany({ select: { id: true, tenantId: true } });
95
- const map = {};
96
- for (const s of sites) map[s.id] = s.tenantId;
97
- return map;
98
- } finally {
99
- await prisma.$disconnect();
100
- }
101
- }
102
-
103
- /**
104
- * Discover the list of monthly partitions currently present in `events_raw`.
105
- *
106
- * Exported for tests — accepts an optional injected query function.
107
- */
108
- export async function discoverPartitions(queryFn = clickhouseQuery) {
109
- const res = await queryFn(
110
- `SELECT DISTINCT partition FROM system.parts WHERE table = 'events_raw' AND active ORDER BY partition`
111
- );
112
- return (res.data || []).map((r) => String(r.partition));
113
- }
114
-
115
- /**
116
- * Build the ALTER TABLE UPDATE SQL for one (partition, siteId→tenantId) chunk.
117
- *
118
- * Exported for tests.
119
- */
120
- export function buildBackfillSql(partition, siteId, tenantId) {
121
- return (
122
- `ALTER TABLE events_raw UPDATE tenant_id = '${escapeSql(tenantId)}' ` +
123
- `WHERE site_id = '${escapeSql(siteId)}' ` +
124
- `AND (tenant_id = '' OR tenant_id IS NULL) ` +
125
- `AND toYYYYMM(date) = ${Number(partition)}`
126
- );
127
- }
128
-
129
- async function main() {
130
- const args = parseArgs(process.argv);
131
- if (args.help) {
132
- printHelp();
133
- return;
134
- }
135
-
136
- const mode = args.apply ? 'APPLY' : 'DRY-RUN';
137
- console.log(`[backfill] mode=${mode}`);
138
-
139
- const siteMap = await loadSiteTenantMap();
140
- const siteIds = Object.keys(siteMap);
141
- console.log(`[backfill] loaded ${siteIds.length} site→tenant mappings`);
142
-
143
- const partitions = args.partitions || (await discoverPartitions());
144
- console.log(`[backfill] ${partitions.length} partitions to process`);
145
-
146
- let totalChunks = 0;
147
- for (const partition of partitions) {
148
- for (const siteId of siteIds) {
149
- const tenantId = siteMap[siteId];
150
- if (!tenantId) continue;
151
- totalChunks++;
152
- const sql = buildBackfillSql(partition, siteId, tenantId);
153
- if (args.apply) {
154
- console.log(`[backfill] APPLY partition=${partition} site=${siteId}`);
155
- await clickhouseQuery(sql);
156
- } else {
157
- console.log(`[backfill] DRY partition=${partition} site=${siteId}`);
158
- console.log(` ${sql}`);
159
- }
160
- }
161
- }
162
-
163
- console.log(`[backfill] done — ${totalChunks} chunks ${args.apply ? 'applied' : '(dry-run)'}`);
164
- }
165
-
166
- const isMain = import.meta.url === `file://${process.argv[1]}`;
167
- if (isMain) {
168
- main().catch((err) => {
169
- console.error('[backfill] fatal:', err);
170
- process.exit(10);
171
- });
172
- }