@gurulu/cli 0.1.0 → 0.1.2

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 (54) hide show
  1. package/package.json +7 -3
  2. package/scripts/.gitkeep +0 -0
  3. package/scripts/README-gurulu-agentic-install.md +114 -0
  4. package/scripts/README-gurulu-scan.md +98 -0
  5. package/scripts/audit-cli-scopes.mjs +204 -0
  6. package/scripts/backfill-tenant-id.mjs +172 -0
  7. package/scripts/backfill-tenant-links.ts +252 -0
  8. package/scripts/backup-clickhouse.sh +27 -0
  9. package/scripts/backup-postgres.sh +19 -0
  10. package/scripts/bootstrap-runtime-schema.mjs +105 -0
  11. package/scripts/bootstrap-stripe.mjs +158 -0
  12. package/scripts/gurulu-agentic-install.lib.cjs +734 -0
  13. package/scripts/gurulu-agentic-install.mjs +343 -0
  14. package/scripts/gurulu-scan.lib.cjs +989 -0
  15. package/scripts/gurulu-scan.mjs +91 -0
  16. package/scripts/gurulu-verify-install.lib.cjs +334 -0
  17. package/scripts/gurulu-verify-install.mjs +59 -0
  18. package/scripts/init-ssl.sh +26 -0
  19. package/scripts/migrate-flow-graph-enums.sh +86 -0
  20. package/scripts/monitor-disk.sh +24 -0
  21. package/scripts/patches/astro.patch.cjs +73 -0
  22. package/scripts/patches/auto-instrument/ast-helper.cjs +332 -0
  23. package/scripts/patches/auto-instrument/astro.cjs +267 -0
  24. package/scripts/patches/auto-instrument/express.cjs +368 -0
  25. package/scripts/patches/auto-instrument/fastify.cjs +258 -0
  26. package/scripts/patches/auto-instrument/index.cjs +78 -0
  27. package/scripts/patches/auto-instrument/nestjs.cjs +282 -0
  28. package/scripts/patches/auto-instrument/nextjs-app-router.cjs +318 -0
  29. package/scripts/patches/auto-instrument/nextjs-pages.cjs +348 -0
  30. package/scripts/patches/auto-instrument/remix.cjs +164 -0
  31. package/scripts/patches/auto-instrument/singleton-helper.cjs +193 -0
  32. package/scripts/patches/auto-instrument/sveltekit.cjs +157 -0
  33. package/scripts/patches/auto-instrument/vite-react.cjs +37 -0
  34. package/scripts/patches/auto-instrument/vue.cjs +192 -0
  35. package/scripts/patches/express.patch.cjs +99 -0
  36. package/scripts/patches/fastify.patch.cjs +107 -0
  37. package/scripts/patches/index.cjs +294 -0
  38. package/scripts/patches/nestjs.patch.cjs +111 -0
  39. package/scripts/patches/nextjs-app-router.patch.cjs +95 -0
  40. package/scripts/patches/nextjs-pages.patch.cjs +96 -0
  41. package/scripts/patches/remix.patch.cjs +74 -0
  42. package/scripts/patches/sveltekit.patch.cjs +71 -0
  43. package/scripts/patches/vite-react.patch.cjs +72 -0
  44. package/scripts/patches/vue.patch.cjs +81 -0
  45. package/scripts/renew-ssl.sh +14 -0
  46. package/scripts/resolve-migration.sh +23 -0
  47. package/scripts/seed-cli-dev-keys.mjs +130 -0
  48. package/scripts/seed-test-data.mjs +391 -0
  49. package/scripts/spike-browserless.ts +65 -0
  50. package/scripts/tenant-pivot-consistency-check.mjs +205 -0
  51. package/scripts/tenant-pivot-phase-3-cleanup.lib.cjs +258 -0
  52. package/scripts/tenant-pivot-phase-3-cleanup.mjs +98 -0
  53. package/scripts/test-identity-resolution.ts +804 -0
  54. package/scripts/validate-gurulu-schemas.mjs +79 -0
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env node
2
+ // gurulu-agentic-install.mjs — Phase 14 B3 CLI wrapper.
3
+ //
4
+ // Reads a `gurulu-scan` JSON output and produces the 10 `.gurulu/` artifacts
5
+ // that the runtime loader (Phase 13 B1) consumes. Pairs with:
6
+ // scripts/gurulu-scan.mjs (Phase 13 B2 — scanner)
7
+ // scripts/gurulu-agentic-install.lib.cjs (generator logic)
8
+ //
9
+ // Usage:
10
+ // node scripts/gurulu-agentic-install.mjs <scan-output.json> \
11
+ // --site-id <id> --tenant-id <id> [--output .gurulu/] [--dry-run] [--quiet]
12
+ //
13
+ // Pass `-` as the scan path to read JSON from stdin.
14
+
15
+ import { createRequire } from 'node:module';
16
+ import { readFileSync, statSync, existsSync } from 'node:fs';
17
+ import path from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ const require = createRequire(import.meta.url);
21
+ const lib = require('./gurulu-agentic-install.lib.cjs');
22
+ const patches = require('./patches/index.cjs');
23
+
24
+ export const {
25
+ INSTALL_AGENT_VERSION,
26
+ generateArtifacts,
27
+ generateInstallPlan,
28
+ generateCoreConfig,
29
+ generateWebConfig,
30
+ generateAppConfig,
31
+ generateServerMap,
32
+ generateDbMap,
33
+ generateFlowSeeds,
34
+ generateMilestoneRules,
35
+ generateCorrelationMap,
36
+ generateConnectors,
37
+ deriveBusinessSurfaces,
38
+ deriveMilestones,
39
+ inferEventName,
40
+ validateArtifacts,
41
+ writeArtifacts,
42
+ } = lib;
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // CLI entry
46
+ // ---------------------------------------------------------------------------
47
+
48
+ function parseArgs(argv) {
49
+ const args = { _: [] };
50
+ for (let i = 0; i < argv.length; i++) {
51
+ const a = argv[i];
52
+ if (a === '--help' || a === '-h') {
53
+ args.help = true;
54
+ } else if (a === '--quiet' || a === '-q') {
55
+ args.quiet = true;
56
+ } else if (a === '--dry-run') {
57
+ args.dryRun = true;
58
+ } else if (a === '--apply') {
59
+ args.apply = true;
60
+ } else if (a === '--rollback') {
61
+ args.rollback = true;
62
+ } else if (a === '--auto-instrument') {
63
+ args.autoInstrument = true;
64
+ } else if (a.startsWith('--')) {
65
+ const key = a.slice(2);
66
+ args[key] = argv[++i];
67
+ } else {
68
+ args._.push(a);
69
+ }
70
+ }
71
+ return args;
72
+ }
73
+
74
+ function printHelp(out = process.stdout) {
75
+ out.write(
76
+ [
77
+ 'Usage: node scripts/gurulu-agentic-install.mjs <scan-output.json> \\',
78
+ ' --site-id <id> --tenant-id <id> [--output .gurulu/] [--dry-run] [--quiet]',
79
+ '',
80
+ 'Reads a gurulu-scan JSON document (pass `-` for stdin) and generates the',
81
+ '10 .gurulu/ artifacts ingested by the Phase 13 B1 runtime loader.',
82
+ '',
83
+ 'Options:',
84
+ ' --site-id <id> Stable site identifier (required)',
85
+ ' --tenant-id <id> Tenant identifier (required)',
86
+ ' --publishable-key <key> Stripe-style gpk_live_/gpk_test_ key (optional)',
87
+ ' --output <path> Output directory (default: .gurulu)',
88
+ ' --domains <list> Comma-separated host list for install-plan.domains',
89
+ ' --dry-run Validate + report without writing files (default for patch mode)',
90
+ ' --apply (patch mode) write patches + create backup',
91
+ ' --rollback (patch mode) restore most recent backup',
92
+ ' --framework <f> auto|nextjs-app|nextjs-pages|vite-react|express',
93
+ ' --auto-instrument (Phase 18.7) Also write gurulu.track() calls into route handlers',
94
+ ' --intent-result <path> JSON file with InstallIntent output (feeds --auto-instrument)',
95
+ ' --quiet Suppress non-error output',
96
+ ' -h, --help Show this help',
97
+ '',
98
+ ].join('\n'),
99
+ );
100
+ }
101
+
102
+ async function readScanInput(scanFile) {
103
+ if (scanFile === '-') {
104
+ // stdin
105
+ const chunks = [];
106
+ for await (const chunk of process.stdin) chunks.push(chunk);
107
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
108
+ }
109
+ return JSON.parse(readFileSync(scanFile, 'utf8'));
110
+ }
111
+
112
+ async function runPatchMode(repoRoot, args) {
113
+ const log = args.quiet ? () => {} : (m) => process.stdout.write(m + '\n');
114
+ if (args.rollback) {
115
+ const res = patches.rollback(repoRoot);
116
+ if (!res.rolledBack) {
117
+ process.stderr.write(`Rollback failed: ${res.reason}\n`);
118
+ process.exit(3);
119
+ }
120
+ log(`Rolled back ${res.files.length} files`);
121
+ return;
122
+ }
123
+ if (!args['site-id'] || !args['tenant-id']) {
124
+ process.stderr.write('Error: --site-id and --tenant-id are required for patch mode\n');
125
+ process.exit(1);
126
+ }
127
+ const framework = args.framework || 'auto';
128
+ const { patcher, detection, error } = patches.resolvePatcher(repoRoot, framework);
129
+ if (error) {
130
+ process.stderr.write(`Error: ${error}\n`);
131
+ process.exit(1);
132
+ }
133
+ if (!patcher) {
134
+ process.stderr.write('No supported framework detected in ' + repoRoot + '\n');
135
+ process.exit(4);
136
+ }
137
+ const injection = patches.buildInjection({
138
+ siteId: args['site-id'],
139
+ tenantId: args['tenant-id'],
140
+ publishableKey: args['publishable-key'] || '',
141
+ scriptSrc: args['script-src'] || '/gurulu-tracker.js',
142
+ });
143
+ const plan = patcher.plan({ repoRoot, injection });
144
+ log(`Framework: ${patcher.name}`);
145
+ log(`Detected files: ${(detection?.files || []).join(', ') || '(none)'}`);
146
+ if (plan.changes.length === 0) {
147
+ log('No changes to apply (already installed or nothing to patch)');
148
+ return;
149
+ }
150
+ // Default behavior is dry-run unless --apply is passed.
151
+ const shouldApply = !!args.apply;
152
+ if (!shouldApply) {
153
+ for (const change of plan.changes) {
154
+ process.stdout.write(patches.unifiedDiff(change.relPath, change.before, change.after));
155
+ }
156
+ // Phase 18.7 B6 — include auto-instrument diff in dry-run output so
157
+ // users can eyeball what will be written before --apply.
158
+ if (args.autoInstrument && args['intent-result']) {
159
+ try {
160
+ const intent = JSON.parse(readFileSync(args['intent-result'], 'utf8'));
161
+ const acceptedEvents = (intent && intent.accepted && intent.accepted.events) || [];
162
+ const frameworkName = (patcher && patcher.name) || framework;
163
+ const aiResult = patches.autoInstrumentDispatch(
164
+ frameworkName,
165
+ { repoRoot },
166
+ acceptedEvents,
167
+ );
168
+ for (const change of aiResult.changes || []) {
169
+ process.stdout.write(
170
+ patches.unifiedDiff(change.relPath, change.before, change.after),
171
+ );
172
+ }
173
+ log(
174
+ `Auto-instrument dry-run: ${aiResult.filesModified} file(s) would change, ` +
175
+ `${aiResult.eventsInstrumented} event(s), ${aiResult.eventsSkipped} skipped.`,
176
+ );
177
+ } catch (err) {
178
+ process.stderr.write(
179
+ `Auto-instrument dry-run failed: ${err.message || err}\n`,
180
+ );
181
+ }
182
+ }
183
+ log(`Dry-run: ${plan.changes.length} file(s) would change. Re-run with --apply to write.`);
184
+ return;
185
+ }
186
+ const result = await patches.applyPlan(repoRoot, patcher, plan);
187
+ log(`Applied ${result.files.length} file change(s). Patch log: .gurulu/patch-log.json`);
188
+
189
+ // -------------------------------------------------------------------
190
+ // Phase 18.7 B6 — Auto-instrumentation (opt-in).
191
+ // When --auto-instrument is set AND --intent-result is provided, load
192
+ // the accepted events from the Phase 18.6 intent proposal, dispatch
193
+ // to the matching auto-instrument module, and append the resulting
194
+ // route-handler changes to the existing patch-log. Failures here are
195
+ // reported but do NOT unwind the script-tag patch (separate concerns).
196
+ // -------------------------------------------------------------------
197
+ if (args.autoInstrument) {
198
+ const intentPath = args['intent-result'];
199
+ if (!intentPath) {
200
+ log('Auto-instrument: skipped (no --intent-result provided)');
201
+ return;
202
+ }
203
+ let intent;
204
+ try {
205
+ intent = JSON.parse(readFileSync(intentPath, 'utf8'));
206
+ } catch (err) {
207
+ process.stderr.write(`Auto-instrument: failed to read ${intentPath}: ${err.message}\n`);
208
+ return;
209
+ }
210
+ const acceptedEvents = (intent && intent.accepted && intent.accepted.events) || [];
211
+ if (acceptedEvents.length === 0) {
212
+ log('Auto-instrument: no accepted events to instrument');
213
+ return;
214
+ }
215
+ const frameworkName = (patcher && patcher.name) || framework;
216
+ const aiResult = patches.autoInstrumentDispatch(
217
+ frameworkName,
218
+ { repoRoot },
219
+ acceptedEvents,
220
+ );
221
+ if (aiResult.collision) {
222
+ process.stderr.write(
223
+ `Auto-instrument: singleton helper collision; aborting without changes.\n`,
224
+ );
225
+ return;
226
+ }
227
+ if (!aiResult.changes || aiResult.changes.length === 0) {
228
+ log(`Auto-instrument: 0 file(s) changed (${aiResult.eventsSkipped} event(s) skipped)`);
229
+ for (const note of aiResult.notes || []) log(` note: ${note}`);
230
+ return;
231
+ }
232
+ try {
233
+ const aiApply = await patches.applyAutoInstrumentPlan(repoRoot, {
234
+ framework: frameworkName,
235
+ changes: aiResult.changes,
236
+ });
237
+ log(
238
+ `Auto-instrument: ${aiResult.filesModified} file(s) instrumented, ` +
239
+ `${aiResult.eventsInstrumented} event(s), ${aiResult.eventsSkipped} skipped`,
240
+ );
241
+ log(`Auto-instrument: wrote ${aiApply.files.length} change(s) to patch-log`);
242
+ // Emit a machine-readable summary on stdout so the CLI can capture it.
243
+ process.stdout.write(
244
+ 'AUTO_INSTRUMENT_RESULT ' +
245
+ JSON.stringify({
246
+ filesModified: aiResult.filesModified,
247
+ eventsInstrumented: aiResult.eventsInstrumented,
248
+ eventsSkipped: aiResult.eventsSkipped,
249
+ notes: aiResult.notes || [],
250
+ }) +
251
+ '\n',
252
+ );
253
+ } catch (err) {
254
+ process.stderr.write(`Auto-instrument: apply failed: ${err.stack || err.message || err}\n`);
255
+ }
256
+ }
257
+ }
258
+
259
+ async function main() {
260
+ const args = parseArgs(process.argv.slice(2));
261
+ if (args.help) {
262
+ printHelp();
263
+ process.exit(0);
264
+ }
265
+ const input = args._[0];
266
+ if (!input) {
267
+ process.stderr.write('Error: input path is required (scan json or repo root)\n');
268
+ printHelp(process.stderr);
269
+ process.exit(1);
270
+ }
271
+
272
+ // Patch-mode: when input is a directory we run the code editor pipeline.
273
+ const isDirectory = input !== '-' && existsSync(input) && statSync(input).isDirectory();
274
+ if (isDirectory) {
275
+ await runPatchMode(input, args);
276
+ return;
277
+ }
278
+
279
+ const scanFile = input;
280
+ if (!args['site-id'] || !args['tenant-id']) {
281
+ process.stderr.write('Error: --site-id and --tenant-id are required\n');
282
+ process.exit(1);
283
+ }
284
+
285
+ const outputPath = args.output || '.gurulu';
286
+ const log = args.quiet ? () => {} : (m) => process.stderr.write(m + '\n');
287
+
288
+ let scanOutput;
289
+ try {
290
+ scanOutput = await readScanInput(scanFile);
291
+ } catch (err) {
292
+ process.stderr.write(`Error: failed to read scan input: ${err.message}\n`);
293
+ process.exit(1);
294
+ }
295
+
296
+ const domains = typeof args.domains === 'string'
297
+ ? args.domains.split(',').map((d) => d.trim()).filter(Boolean)
298
+ : undefined;
299
+
300
+ const result = generateArtifacts(scanOutput, {
301
+ siteId: args['site-id'],
302
+ tenantId: args['tenant-id'],
303
+ outputPath,
304
+ ...(domains ? { domains } : {}),
305
+ });
306
+
307
+ const validation = validateArtifacts(result.artifacts);
308
+ if (!validation.ok) {
309
+ process.stderr.write('Validation failed — not writing any files:\n');
310
+ for (const err of validation.errors) {
311
+ process.stderr.write(` ${err.file}: ${err.message}\n`);
312
+ }
313
+ process.exit(2);
314
+ }
315
+
316
+ const filenames = Object.keys(result.artifacts);
317
+ if (args.dryRun) {
318
+ log(`Dry-run — would write ${filenames.length} files to ${outputPath}/:`);
319
+ for (const f of filenames) log(` ${outputPath}/${f}`);
320
+ return;
321
+ }
322
+
323
+ await writeArtifacts(result.artifacts, outputPath);
324
+ log(`Wrote ${filenames.length} artifacts to ${outputPath}/`);
325
+ }
326
+
327
+ const invokedDirectly =
328
+ typeof process !== 'undefined' &&
329
+ process.argv[1] &&
330
+ (() => {
331
+ try {
332
+ return fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
333
+ } catch {
334
+ return false;
335
+ }
336
+ })();
337
+
338
+ if (invokedDirectly) {
339
+ main().catch((err) => {
340
+ process.stderr.write(`[agentic-install] ERROR: ${err.stack || err.message || err}\n`);
341
+ process.exit(1);
342
+ });
343
+ }