@sansavision/aurora 0.1.0-alpha.20260212.4

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 (150) hide show
  1. package/README.md +4 -0
  2. package/package.json +17 -0
  3. package/src/ai-diagnostics.ts +156 -0
  4. package/src/ai.ts +574 -0
  5. package/src/analyze.ts +669 -0
  6. package/src/bin/aurora.ts +15 -0
  7. package/src/build.ts +431 -0
  8. package/src/bun-test-shims.d.ts +17 -0
  9. package/src/create-feature.ts +419 -0
  10. package/src/create-route.ts +581 -0
  11. package/src/create.ts +425 -0
  12. package/src/dev.ts +126 -0
  13. package/src/devtools.ts +1143 -0
  14. package/src/doctor.ts +611 -0
  15. package/src/explain.ts +855 -0
  16. package/src/help.ts +39 -0
  17. package/src/index.ts +34 -0
  18. package/src/init.ts +1011 -0
  19. package/src/inspect-cache.ts +464 -0
  20. package/src/lsp-inline-hints.ts +254 -0
  21. package/src/node-shims.d.ts +26 -0
  22. package/src/process.d.ts +11 -0
  23. package/src/query-profiler.ts +520 -0
  24. package/src/realtime-monitor.ts +389 -0
  25. package/src/registry.ts +303 -0
  26. package/src/run.ts +37 -0
  27. package/src/start.ts +56 -0
  28. package/src/test.ts +289 -0
  29. package/templates/basic/README.md +16 -0
  30. package/templates/basic/package.json +10 -0
  31. package/templates/basic/src/actions/createMessage.action.server.ts +22 -0
  32. package/templates/basic/src/lib/auth.server.ts +11 -0
  33. package/templates/basic/src/queries/listMessages.server.ts +17 -0
  34. package/templates/basic/src/routes/index.tsx +12 -0
  35. package/templates/blog/README.md +17 -0
  36. package/templates/blog/package.json +12 -0
  37. package/templates/blog/public/assets/og-default.svg +17 -0
  38. package/templates/blog/src/content/loadPosts.server.ts +22 -0
  39. package/templates/blog/src/content/posts/hello-world.md +11 -0
  40. package/templates/blog/src/content/posts/release-notes.md +9 -0
  41. package/templates/blog/src/routes/index.tsx +22 -0
  42. package/templates/blog/src/routes/posts/[slug].tsx +19 -0
  43. package/templates/blog/src/seo/meta.ts +19 -0
  44. package/templates/dashboard/README.md +18 -0
  45. package/templates/dashboard/package.json +10 -0
  46. package/templates/dashboard/src/actions/acknowledgeAlert.action.server.ts +6 -0
  47. package/templates/dashboard/src/queries/getDashboardMetrics.server.ts +30 -0
  48. package/templates/dashboard/src/realtime/useDashboardRealtime.client.ts +13 -0
  49. package/templates/dashboard/src/routes/index.tsx +19 -0
  50. package/templates/dashboard/src/widgets/DataGrid.client.ts +8 -0
  51. package/templates/dashboard/src/widgets/MetricChart.client.ts +8 -0
  52. package/templates/desktop/README.md +18 -0
  53. package/templates/desktop/package.json +11 -0
  54. package/templates/desktop/src/actions/saveDesktopPreference.action.server.ts +28 -0
  55. package/templates/desktop/src/desktop/secureStorage.client.ts +20 -0
  56. package/templates/desktop/src/desktop/tauriBridge.client.ts +14 -0
  57. package/templates/desktop/src/queries/getDesktopSyncStatus.server.ts +9 -0
  58. package/templates/desktop/src/routes/index.tsx +27 -0
  59. package/templates/desktop/src/sync/offlineSyncBoundary.server.ts +27 -0
  60. package/templates/feature-skeleton/README.md +13 -0
  61. package/templates/feature-skeleton/actions/createFeature.action.server.ts +19 -0
  62. package/templates/feature-skeleton/index.ts +8 -0
  63. package/templates/feature-skeleton/queries/listFeature.server.ts +15 -0
  64. package/templates/feature-skeleton/realtime/useFeatureRealtime.client.ts +16 -0
  65. package/templates/feature-skeleton/template.manifest.json +15 -0
  66. package/templates/feature-skeleton/ui/FeatureView.client.tsx +14 -0
  67. package/templates/mobile/README.md +17 -0
  68. package/templates/mobile/package.json +11 -0
  69. package/templates/mobile/src/mobile/auth/session-handoff.client.ts +69 -0
  70. package/templates/mobile/src/mobile/generated/mobile-api-sdk.ts +62 -0
  71. package/templates/mobile/src/mobile/transport/mobile-api-transport.client.ts +122 -0
  72. package/templates/mobile/src/routes/index.tsx +134 -0
  73. package/templates/monorepo/README.md +18 -0
  74. package/templates/monorepo/apps/web/package.json +9 -0
  75. package/templates/monorepo/apps/web/src/routes/index.tsx +1 -0
  76. package/templates/monorepo/package.json +13 -0
  77. package/templates/monorepo/packages/shared/README.md +3 -0
  78. package/templates/monorepo/packages/ui/README.md +3 -0
  79. package/templates/saas/README.md +17 -0
  80. package/templates/saas/package.json +10 -0
  81. package/templates/saas/src/admin/getDashboard.server.ts +18 -0
  82. package/templates/saas/src/auth/session.server.ts +13 -0
  83. package/templates/saas/src/billing/checkout.server.ts +11 -0
  84. package/templates/saas/src/email/sendWelcome.server.ts +8 -0
  85. package/templates/saas/src/realtime/notifications.server.ts +8 -0
  86. package/templates/saas/src/routes/index.tsx +20 -0
  87. package/test/ai.test.ts +94 -0
  88. package/test/analyze.test.ts +301 -0
  89. package/test/build.test.ts +135 -0
  90. package/test/create-feature.test.ts +145 -0
  91. package/test/create-route.test.ts +117 -0
  92. package/test/create.test.ts +222 -0
  93. package/test/dev.test.ts +52 -0
  94. package/test/devtools.test.ts +130 -0
  95. package/test/doctor.test.ts +129 -0
  96. package/test/explain.test.ts +232 -0
  97. package/test/feature-skeleton.test.ts +53 -0
  98. package/test/fixtures/analyze/cache-input.invalid.json +1 -0
  99. package/test/fixtures/analyze/cache-input.missing-keyhash.v1.json +10 -0
  100. package/test/fixtures/analyze/cache-input.unsupported-version.v2.json +10 -0
  101. package/test/fixtures/analyze/cache-input.v1.json +12 -0
  102. package/test/fixtures/analyze/compiler-manifest/manifest.json +11 -0
  103. package/test/fixtures/analyze/guardrails-input.unsupported-version.v2.json +4 -0
  104. package/test/fixtures/analyze/guardrails-input.v1.json +49 -0
  105. package/test/fixtures/analyze/query-input.invalid-cache-status.v1.json +11 -0
  106. package/test/fixtures/analyze/query-input.unsupported-version.v2.json +11 -0
  107. package/test/fixtures/analyze/query-input.v1.json +18 -0
  108. package/test/fixtures/analyze/realtime-input.missing-lag-p95.v1.json +10 -0
  109. package/test/fixtures/analyze/realtime-input.unsupported-version.v2.json +8 -0
  110. package/test/fixtures/analyze/realtime-input.v1.json +12 -0
  111. package/test/fixtures/cache-inspector/cache-input.v1.json +23 -0
  112. package/test/fixtures/cache-inspector/invalid.json +1 -0
  113. package/test/fixtures/cache-inspector/snapshot.v1.json +34 -0
  114. package/test/fixtures/cache-inspector/unsupported-version.v2.json +13 -0
  115. package/test/fixtures/devtools/healthy.v1.json +130 -0
  116. package/test/fixtures/devtools/invalid.json +1 -0
  117. package/test/fixtures/devtools/unsupported-version.v2.json +8 -0
  118. package/test/fixtures/devtools/warn.v1.json +114 -0
  119. package/test/fixtures/doctor/clean/src/page.tsx +3 -0
  120. package/test/fixtures/doctor/findings/src/accessibility.client.tsx +7 -0
  121. package/test/fixtures/doctor/findings/src/migration.config.ts +3 -0
  122. package/test/fixtures/doctor/findings/src/page.client.tsx +5 -0
  123. package/test/fixtures/doctor/findings/src/perf.server.ts +15 -0
  124. package/test/fixtures/doctor/findings/src/routes.js +3 -0
  125. package/test/fixtures/doctor/findings/src/security.server.ts +7 -0
  126. package/test/fixtures/doctor/findings/src/users.server.ts +3 -0
  127. package/test/fixtures/doctor/governance/src/features/analytics/OWNERS.ts +2 -0
  128. package/test/fixtures/doctor/governance/src/features/analytics/page.tsx +3 -0
  129. package/test/fixtures/doctor/governance/src/features/billing/page.tsx +3 -0
  130. package/test/fixtures/explain/invalid.json +1 -0
  131. package/test/fixtures/explain/module-report.unsupported-version.v2.json +6 -0
  132. package/test/fixtures/explain/module-report.v1.json +72 -0
  133. package/test/fixtures/query-profiler/healthy.v1.json +11 -0
  134. package/test/fixtures/query-profiler/invalid.json +1 -0
  135. package/test/fixtures/query-profiler/unsupported-version.v2.json +6 -0
  136. package/test/fixtures/query-profiler/warning.v1.json +10 -0
  137. package/test/fixtures/realtime-monitor/healthy.v1.json +8 -0
  138. package/test/fixtures/realtime-monitor/invalid.json +1 -0
  139. package/test/fixtures/realtime-monitor/unsupported-version.v2.json +8 -0
  140. package/test/fixtures/realtime-monitor/warning.v1.json +8 -0
  141. package/test/help-parity.test.ts +104 -0
  142. package/test/init.test.ts +164 -0
  143. package/test/inspect-cache.test.ts +112 -0
  144. package/test/lsp-inline-hints.test.ts +65 -0
  145. package/test/query-profiler.test.ts +123 -0
  146. package/test/realtime-monitor.test.ts +115 -0
  147. package/test/registry.test.ts +41 -0
  148. package/test/start.test.ts +23 -0
  149. package/test/test-command.test.ts +65 -0
  150. package/tsconfig.json +19 -0
package/src/init.ts ADDED
@@ -0,0 +1,1011 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, relative, resolve } from "node:path";
3
+
4
+ import { type CommandContext, type CommandResult } from "./registry";
5
+
6
+ type MigrationFramework = "next" | "remix" | "sveltekit";
7
+ type InitMode = "incremental" | "side-by-side";
8
+ type InitBridge = "react" | "none";
9
+ type InitFormat = "text" | "json";
10
+
11
+ interface InitOptions {
12
+ framework: MigrationFramework;
13
+ mode: InitMode;
14
+ bridge: InitBridge;
15
+ outDir: string;
16
+ dryRun: boolean;
17
+ format: InitFormat;
18
+ }
19
+
20
+ interface RouteMapping {
21
+ sourcePath: string;
22
+ frameworkPath: string;
23
+ auroraPath: string;
24
+ targetFiles: readonly string[];
25
+ convention: string;
26
+ }
27
+
28
+ interface MigrationChecklistItem {
29
+ id: string;
30
+ text: string;
31
+ }
32
+
33
+ interface MigrationScanResult {
34
+ framework: MigrationFramework;
35
+ routes: readonly RouteMapping[];
36
+ loaderCandidates: number;
37
+ actionCandidates: number;
38
+ warnings: readonly string[];
39
+ }
40
+
41
+ interface GeneratedScaffoldFile {
42
+ path: string;
43
+ content: string;
44
+ }
45
+
46
+ interface InitMigrationReport {
47
+ mode: "init-migration";
48
+ projectRoot: string;
49
+ framework: MigrationFramework;
50
+ migrationMode: InitMode;
51
+ bridge: InitBridge;
52
+ outDir: string;
53
+ dryRun: boolean;
54
+ detectedRoutes: number;
55
+ loaderCandidates: number;
56
+ actionCandidates: number;
57
+ mappedRoutes: readonly RouteMapping[];
58
+ checklist: readonly MigrationChecklistItem[];
59
+ warnings: readonly string[];
60
+ generatedFiles: readonly string[];
61
+ }
62
+
63
+ const VALID_FRAMEWORKS: readonly MigrationFramework[] = ["next", "remix", "sveltekit"];
64
+ const VALID_MODES: readonly InitMode[] = ["incremental", "side-by-side"];
65
+ const VALID_BRIDGES: readonly InitBridge[] = ["react", "none"];
66
+ const DEFAULT_OUT_DIR = ".aurora/migration";
67
+ const CODE_FILE_PATTERN = /\.[cm]?[jt]sx?$/;
68
+ const IGNORED_DIRECTORIES = new Set([
69
+ ".git",
70
+ ".next",
71
+ ".nuxt",
72
+ ".aurora",
73
+ "node_modules",
74
+ "dist",
75
+ "build",
76
+ "coverage",
77
+ "target",
78
+ ]);
79
+
80
+ function parseFramework(value: string | undefined): MigrationFramework | CommandResult {
81
+ if (!value) {
82
+ return {
83
+ exitCode: 2,
84
+ stderr: `aurora init: --from requires ${VALID_FRAMEWORKS.join("|")}`,
85
+ };
86
+ }
87
+
88
+ if (VALID_FRAMEWORKS.includes(value as MigrationFramework)) {
89
+ return value as MigrationFramework;
90
+ }
91
+
92
+ return {
93
+ exitCode: 2,
94
+ stderr: `aurora init: invalid --from value '${value}'. Expected ${VALID_FRAMEWORKS.join(", ")}`,
95
+ };
96
+ }
97
+
98
+ function parseMode(value: string | undefined): InitMode | CommandResult {
99
+ if (!value) {
100
+ return {
101
+ exitCode: 2,
102
+ stderr: `aurora init: --mode requires ${VALID_MODES.join("|")}`,
103
+ };
104
+ }
105
+
106
+ if (VALID_MODES.includes(value as InitMode)) {
107
+ return value as InitMode;
108
+ }
109
+
110
+ return {
111
+ exitCode: 2,
112
+ stderr: `aurora init: invalid --mode '${value}'. Expected ${VALID_MODES.join(", ")}`,
113
+ };
114
+ }
115
+
116
+ function parseBridge(value: string | undefined): InitBridge | CommandResult {
117
+ if (!value) {
118
+ return {
119
+ exitCode: 2,
120
+ stderr: `aurora init: --bridge requires ${VALID_BRIDGES.join("|")}`,
121
+ };
122
+ }
123
+
124
+ if (VALID_BRIDGES.includes(value as InitBridge)) {
125
+ return value as InitBridge;
126
+ }
127
+
128
+ return {
129
+ exitCode: 2,
130
+ stderr: `aurora init: invalid --bridge '${value}'. Expected ${VALID_BRIDGES.join(", ")}`,
131
+ };
132
+ }
133
+
134
+ function parseFormat(value: string | undefined): InitFormat | CommandResult {
135
+ if (!value) {
136
+ return {
137
+ exitCode: 2,
138
+ stderr: "aurora init: --format requires 'text' or 'json'",
139
+ };
140
+ }
141
+
142
+ if (value === "text" || value === "json") {
143
+ return value;
144
+ }
145
+
146
+ return {
147
+ exitCode: 2,
148
+ stderr: `aurora init: invalid format '${value}'. Expected 'text' or 'json'`,
149
+ };
150
+ }
151
+
152
+ function parseInitOptions(args: ReadonlyArray<string>): InitOptions | CommandResult {
153
+ const parsed: Partial<InitOptions> = {
154
+ mode: "incremental",
155
+ bridge: "react",
156
+ outDir: DEFAULT_OUT_DIR,
157
+ dryRun: false,
158
+ format: "text",
159
+ };
160
+
161
+ for (let index = 0; index < args.length; index += 1) {
162
+ const arg = args[index];
163
+
164
+ if (arg === "--from") {
165
+ const framework = parseFramework(args[index + 1]);
166
+ if (typeof framework !== "string") {
167
+ return framework;
168
+ }
169
+ parsed.framework = framework;
170
+ index += 1;
171
+ continue;
172
+ }
173
+
174
+ if (arg === "--mode") {
175
+ const mode = parseMode(args[index + 1]);
176
+ if (typeof mode !== "string") {
177
+ return mode;
178
+ }
179
+ parsed.mode = mode;
180
+ index += 1;
181
+ continue;
182
+ }
183
+
184
+ if (arg === "--bridge") {
185
+ const bridge = parseBridge(args[index + 1]);
186
+ if (typeof bridge !== "string") {
187
+ return bridge;
188
+ }
189
+ parsed.bridge = bridge;
190
+ index += 1;
191
+ continue;
192
+ }
193
+
194
+ if (arg === "--out-dir") {
195
+ const value = args[index + 1];
196
+ if (!value || value.trim().length === 0) {
197
+ return {
198
+ exitCode: 2,
199
+ stderr: "aurora init: --out-dir requires a non-empty path",
200
+ };
201
+ }
202
+
203
+ parsed.outDir = value;
204
+ index += 1;
205
+ continue;
206
+ }
207
+
208
+ if (arg === "--dry-run") {
209
+ parsed.dryRun = true;
210
+ continue;
211
+ }
212
+
213
+ if (arg === "--format") {
214
+ const format = parseFormat(args[index + 1]);
215
+ if (typeof format !== "string") {
216
+ return format;
217
+ }
218
+ parsed.format = format;
219
+ index += 1;
220
+ continue;
221
+ }
222
+
223
+ if (arg.startsWith("--")) {
224
+ return {
225
+ exitCode: 2,
226
+ stderr: `aurora init: unknown option '${arg}'`,
227
+ };
228
+ }
229
+
230
+ return {
231
+ exitCode: 2,
232
+ stderr: `aurora init: unexpected positional argument '${arg}'`,
233
+ };
234
+ }
235
+
236
+ if (!parsed.framework) {
237
+ return {
238
+ exitCode: 2,
239
+ stderr: "aurora init: --from is required (next|remix|sveltekit)",
240
+ };
241
+ }
242
+
243
+ return parsed as InitOptions;
244
+ }
245
+
246
+ function normalizeRelativePath(path: string): string {
247
+ return path.replaceAll("\\", "/");
248
+ }
249
+
250
+ function fileBaseName(path: string): string {
251
+ const normalized = normalizeRelativePath(path);
252
+ const lastSeparator = normalized.lastIndexOf("/");
253
+ return lastSeparator >= 0 ? normalized.slice(lastSeparator + 1) : normalized;
254
+ }
255
+
256
+ function collectFiles(rootPath: string): string[] {
257
+ if (!existsSync(rootPath)) {
258
+ return [];
259
+ }
260
+
261
+ const collected: string[] = [];
262
+
263
+ function visit(directory: string): void {
264
+ const entries = readdirSync(directory, { withFileTypes: true }).sort((left, right) =>
265
+ left.name.localeCompare(right.name),
266
+ );
267
+
268
+ for (const entry of entries) {
269
+ const absolutePath = join(directory, entry.name);
270
+ if (entry.isDirectory()) {
271
+ if (IGNORED_DIRECTORIES.has(entry.name)) {
272
+ continue;
273
+ }
274
+ visit(absolutePath);
275
+ continue;
276
+ }
277
+
278
+ if (!entry.isFile()) {
279
+ continue;
280
+ }
281
+
282
+ collected.push(absolutePath);
283
+ }
284
+ }
285
+
286
+ visit(rootPath);
287
+ return collected;
288
+ }
289
+
290
+ function readTextIfExists(path: string): string {
291
+ try {
292
+ return readFileSync(path, "utf8");
293
+ } catch {
294
+ return "";
295
+ }
296
+ }
297
+
298
+ function stripCodeExtension(path: string): string {
299
+ return path.replace(/\.[cm]?[jt]sx?$/i, "");
300
+ }
301
+
302
+ function mapBracketSegment(segment: string): string {
303
+ const catchAll = segment.match(/^\[\.\.\.(.+)\]$/);
304
+ if (catchAll) {
305
+ return `*${catchAll[1]}`;
306
+ }
307
+
308
+ const optionalCatchAll = segment.match(/^\[\[\.\.\.(.+)\]\]$/);
309
+ if (optionalCatchAll) {
310
+ return `*${optionalCatchAll[1]}`;
311
+ }
312
+
313
+ const dynamic = segment.match(/^\[(.+)\]$/);
314
+ if (dynamic) {
315
+ return `:${dynamic[1]}`;
316
+ }
317
+
318
+ return segment;
319
+ }
320
+
321
+ function toRoutePathFromSegments(segments: readonly string[]): string {
322
+ const normalized = segments.filter((segment) => segment.length > 0);
323
+ if (normalized.length === 0) {
324
+ return "/";
325
+ }
326
+
327
+ return `/${normalized.join("/")}`;
328
+ }
329
+
330
+ function nextPagesRouteFromRelative(relativeFilePath: string): string | undefined {
331
+ const withoutExt = stripCodeExtension(normalizeRelativePath(relativeFilePath));
332
+ if (withoutExt.startsWith("api/")) {
333
+ return undefined;
334
+ }
335
+
336
+ const base = fileBaseName(withoutExt);
337
+ if (base === "_app" || base === "_document" || base === "_error") {
338
+ return undefined;
339
+ }
340
+
341
+ const parts = withoutExt.split("/");
342
+ const routeSegments: string[] = [];
343
+ for (let index = 0; index < parts.length; index += 1) {
344
+ const part = parts[index];
345
+ const isLast = index === parts.length - 1;
346
+ if (isLast && part === "index") {
347
+ continue;
348
+ }
349
+ routeSegments.push(mapBracketSegment(part));
350
+ }
351
+
352
+ return toRoutePathFromSegments(routeSegments);
353
+ }
354
+
355
+ function nextAppRouteFromRelative(relativeFilePath: string): string | undefined {
356
+ const normalized = normalizeRelativePath(relativeFilePath);
357
+ if (!normalized.endsWith("/page.tsx") && !normalized.endsWith("/page.ts") &&
358
+ !normalized.endsWith("/page.jsx") && !normalized.endsWith("/page.js")) {
359
+ return undefined;
360
+ }
361
+
362
+ const dir = dirname(normalized);
363
+ if (dir === ".") {
364
+ return "/";
365
+ }
366
+
367
+ const routeSegments = dir
368
+ .split("/")
369
+ .map((segment) => segment.trim())
370
+ .filter((segment) => segment.length > 0)
371
+ .filter((segment) => !(segment.startsWith("(") && segment.endsWith(")")))
372
+ .map((segment) => mapBracketSegment(segment));
373
+
374
+ return toRoutePathFromSegments(routeSegments);
375
+ }
376
+
377
+ function remixRouteFromRelative(relativeFilePath: string): string {
378
+ const normalized = stripCodeExtension(normalizeRelativePath(relativeFilePath));
379
+ const rawSegments = normalized.split("/");
380
+ const routeSegments: string[] = [];
381
+
382
+ for (const rawSegment of rawSegments) {
383
+ const parts = rawSegment.split(".");
384
+ for (const part of parts) {
385
+ if (part === "index" || part === "_index") {
386
+ continue;
387
+ }
388
+
389
+ if (part.startsWith("_")) {
390
+ continue;
391
+ }
392
+
393
+ if (part === "$") {
394
+ routeSegments.push("*");
395
+ continue;
396
+ }
397
+
398
+ if (part.startsWith("$")) {
399
+ routeSegments.push(`:${part.slice(1)}`);
400
+ continue;
401
+ }
402
+
403
+ routeSegments.push(part);
404
+ }
405
+ }
406
+
407
+ return toRoutePathFromSegments(routeSegments);
408
+ }
409
+
410
+ function sveltekitRouteFromRelative(relativeFilePath: string): string | undefined {
411
+ const normalized = normalizeRelativePath(relativeFilePath);
412
+ if (
413
+ !normalized.endsWith("/+page.svelte") &&
414
+ !normalized.endsWith("/+page.ts") &&
415
+ !normalized.endsWith("/+page.js") &&
416
+ !normalized.endsWith("/+page.tsx") &&
417
+ !normalized.endsWith("/+page.jsx")
418
+ ) {
419
+ return undefined;
420
+ }
421
+
422
+ const dir = dirname(normalized);
423
+ if (dir === ".") {
424
+ return "/";
425
+ }
426
+
427
+ const routeSegments = dir
428
+ .split("/")
429
+ .map((segment) => segment.trim())
430
+ .filter((segment) => segment.length > 0)
431
+ .filter((segment) => !(segment.startsWith("(") && segment.endsWith(")")))
432
+ .map((segment) => mapBracketSegment(segment));
433
+
434
+ return toRoutePathFromSegments(routeSegments);
435
+ }
436
+
437
+ function toAuroraRouteDirectory(auroraPath: string): string {
438
+ if (auroraPath === "/") {
439
+ return "index";
440
+ }
441
+
442
+ return auroraPath
443
+ .slice(1)
444
+ .split("/")
445
+ .map((segment) => {
446
+ if (segment.startsWith(":")) {
447
+ return `[${segment.slice(1)}]`;
448
+ }
449
+ if (segment.startsWith("*")) {
450
+ const label = segment.slice(1).length > 0 ? segment.slice(1) : "all";
451
+ return `[...${label}]`;
452
+ }
453
+ return segment;
454
+ })
455
+ .join("/");
456
+ }
457
+
458
+ function toRouteBaseName(auroraPath: string): string {
459
+ if (auroraPath === "/") {
460
+ return "index";
461
+ }
462
+
463
+ const segments = auroraPath.slice(1).split("/");
464
+ const last = segments[segments.length - 1] ?? "route";
465
+ const normalized = last
466
+ .replace(/^[:*]/, "")
467
+ .replace(/[^a-zA-Z0-9_-]+/g, "-")
468
+ .replace(/^-+|-+$/g, "");
469
+
470
+ return normalized.length > 0 ? normalized : "route";
471
+ }
472
+
473
+ function toRouteTargetFiles(auroraPath: string): string[] {
474
+ const routeDir = toAuroraRouteDirectory(auroraPath);
475
+ const baseName = toRouteBaseName(auroraPath);
476
+ const prefix = join("aurora-routes", routeDir).replaceAll("\\", "/");
477
+
478
+ return [
479
+ `${prefix}/page.tsx`,
480
+ `${prefix}/${baseName}.client.tsx`,
481
+ `${prefix}/${baseName}.server.ts`,
482
+ ];
483
+ }
484
+
485
+ function dedupeMappings(
486
+ mappings: readonly Omit<RouteMapping, "targetFiles">[],
487
+ ): { unique: RouteMapping[]; warnings: string[] } {
488
+ const byAuroraPath = new Map<string, RouteMapping>();
489
+ const warnings: string[] = [];
490
+
491
+ for (const entry of mappings) {
492
+ const existing = byAuroraPath.get(entry.auroraPath);
493
+ if (existing) {
494
+ warnings.push(
495
+ `duplicate mapping for '${entry.auroraPath}' from '${entry.sourcePath}' (keeping '${existing.sourcePath}')`,
496
+ );
497
+ continue;
498
+ }
499
+
500
+ byAuroraPath.set(entry.auroraPath, {
501
+ ...entry,
502
+ targetFiles: toRouteTargetFiles(entry.auroraPath),
503
+ });
504
+ }
505
+
506
+ return {
507
+ unique: [...byAuroraPath.values()].sort((left, right) =>
508
+ left.auroraPath.localeCompare(right.auroraPath),
509
+ ),
510
+ warnings,
511
+ };
512
+ }
513
+
514
+ function detectNextMappings(context: CommandContext): MigrationScanResult {
515
+ const pagesRoot = resolve(context.cwd, "pages");
516
+ const appRoot = resolve(context.cwd, "app");
517
+ const sourceFiles = [
518
+ ...collectFiles(pagesRoot),
519
+ ...collectFiles(appRoot),
520
+ ].filter((path) => CODE_FILE_PATTERN.test(path));
521
+
522
+ const rawMappings: Omit<RouteMapping, "targetFiles">[] = [];
523
+
524
+ for (const absolutePath of collectFiles(pagesRoot)) {
525
+ if (!CODE_FILE_PATTERN.test(absolutePath)) {
526
+ continue;
527
+ }
528
+ const relativeFromPages = normalizeRelativePath(relative(pagesRoot, absolutePath));
529
+ const mappedPath = nextPagesRouteFromRelative(relativeFromPages);
530
+ if (!mappedPath) {
531
+ continue;
532
+ }
533
+ rawMappings.push({
534
+ sourcePath: normalizeRelativePath(relative(context.cwd, absolutePath)),
535
+ frameworkPath: mappedPath,
536
+ auroraPath: mappedPath,
537
+ convention: "next-pages",
538
+ });
539
+ }
540
+
541
+ for (const absolutePath of collectFiles(appRoot)) {
542
+ if (!CODE_FILE_PATTERN.test(absolutePath)) {
543
+ continue;
544
+ }
545
+ const relativeFromApp = normalizeRelativePath(relative(appRoot, absolutePath));
546
+ const mappedPath = nextAppRouteFromRelative(relativeFromApp);
547
+ if (!mappedPath) {
548
+ continue;
549
+ }
550
+ rawMappings.push({
551
+ sourcePath: normalizeRelativePath(relative(context.cwd, absolutePath)),
552
+ frameworkPath: mappedPath,
553
+ auroraPath: mappedPath,
554
+ convention: "next-app",
555
+ });
556
+ }
557
+
558
+ const loaderCandidates = sourceFiles.reduce((count, filePath) => {
559
+ const source = readTextIfExists(filePath);
560
+ const hasLoader = /getServerSideProps|getStaticProps|getInitialProps/.test(source);
561
+ return count + (hasLoader ? 1 : 0);
562
+ }, 0);
563
+ const actionCandidates = sourceFiles.reduce((count, filePath) => {
564
+ const normalized = normalizeRelativePath(filePath);
565
+ const source = readTextIfExists(filePath);
566
+ const hasAction =
567
+ normalized.includes("/pages/api/") ||
568
+ /export\s+async\s+function\s+(POST|PUT|PATCH|DELETE)/.test(source);
569
+ return count + (hasAction ? 1 : 0);
570
+ }, 0);
571
+
572
+ const deduped = dedupeMappings(rawMappings);
573
+ const warnings = [...deduped.warnings];
574
+ if (deduped.unique.length === 0) {
575
+ warnings.push("no Next.js routes were detected under pages/ or app/");
576
+ }
577
+
578
+ return {
579
+ framework: "next",
580
+ routes: deduped.unique,
581
+ loaderCandidates,
582
+ actionCandidates,
583
+ warnings,
584
+ };
585
+ }
586
+
587
+ function detectRemixMappings(context: CommandContext): MigrationScanResult {
588
+ const routesRoot = resolve(context.cwd, "app/routes");
589
+ const routeFiles = collectFiles(routesRoot).filter((filePath) => CODE_FILE_PATTERN.test(filePath));
590
+
591
+ const rawMappings: Omit<RouteMapping, "targetFiles">[] = routeFiles.map((absolutePath) => {
592
+ const relativeFromRoutes = normalizeRelativePath(relative(routesRoot, absolutePath));
593
+ const mappedPath = remixRouteFromRelative(relativeFromRoutes);
594
+ return {
595
+ sourcePath: normalizeRelativePath(relative(context.cwd, absolutePath)),
596
+ frameworkPath: mappedPath,
597
+ auroraPath: mappedPath,
598
+ convention: "remix-routes",
599
+ };
600
+ });
601
+
602
+ const loaderCandidates = routeFiles.reduce((count, filePath) => {
603
+ const source = readTextIfExists(filePath);
604
+ return count + (/export\s+(async\s+)?(function\s+)?loader\b|export\s+const\s+loader\b/.test(source) ? 1 : 0);
605
+ }, 0);
606
+ const actionCandidates = routeFiles.reduce((count, filePath) => {
607
+ const source = readTextIfExists(filePath);
608
+ return count + (/export\s+(async\s+)?(function\s+)?action\b|export\s+const\s+action\b/.test(source) ? 1 : 0);
609
+ }, 0);
610
+
611
+ const deduped = dedupeMappings(rawMappings);
612
+ const warnings = [...deduped.warnings];
613
+ if (deduped.unique.length === 0) {
614
+ warnings.push("no Remix routes were detected under app/routes/");
615
+ }
616
+
617
+ return {
618
+ framework: "remix",
619
+ routes: deduped.unique,
620
+ loaderCandidates,
621
+ actionCandidates,
622
+ warnings,
623
+ };
624
+ }
625
+
626
+ function detectSveltekitMappings(context: CommandContext): MigrationScanResult {
627
+ const routesRoot = resolve(context.cwd, "src/routes");
628
+ const routeFiles = collectFiles(routesRoot).filter((filePath) =>
629
+ CODE_FILE_PATTERN.test(filePath) || filePath.endsWith(".svelte"),
630
+ );
631
+
632
+ const rawMappings: Omit<RouteMapping, "targetFiles">[] = [];
633
+ for (const absolutePath of routeFiles) {
634
+ const relativeFromRoutes = normalizeRelativePath(relative(routesRoot, absolutePath));
635
+ const mappedPath = sveltekitRouteFromRelative(relativeFromRoutes);
636
+ if (!mappedPath) {
637
+ continue;
638
+ }
639
+ rawMappings.push({
640
+ sourcePath: normalizeRelativePath(relative(context.cwd, absolutePath)),
641
+ frameworkPath: mappedPath,
642
+ auroraPath: mappedPath,
643
+ convention: "sveltekit-page",
644
+ });
645
+ }
646
+
647
+ const loaderCandidates = routeFiles.reduce((count, filePath) => {
648
+ const source = readTextIfExists(filePath);
649
+ return count + (/export\s+(async\s+)?function\s+load\b|export\s+const\s+load\b/.test(source) ? 1 : 0);
650
+ }, 0);
651
+ const actionCandidates = routeFiles.reduce((count, filePath) => {
652
+ const source = readTextIfExists(filePath);
653
+ return count + (/export\s+const\s+actions\b/.test(source) ? 1 : 0);
654
+ }, 0);
655
+
656
+ const deduped = dedupeMappings(rawMappings);
657
+ const warnings = [...deduped.warnings];
658
+ if (deduped.unique.length === 0) {
659
+ warnings.push("no SvelteKit +page routes were detected under src/routes/");
660
+ }
661
+
662
+ return {
663
+ framework: "sveltekit",
664
+ routes: deduped.unique,
665
+ loaderCandidates,
666
+ actionCandidates,
667
+ warnings,
668
+ };
669
+ }
670
+
671
+ function detectFrameworkProject(
672
+ options: InitOptions,
673
+ context: CommandContext,
674
+ ): MigrationScanResult {
675
+ if (options.framework === "next") {
676
+ return detectNextMappings(context);
677
+ }
678
+
679
+ if (options.framework === "remix") {
680
+ return detectRemixMappings(context);
681
+ }
682
+
683
+ return detectSveltekitMappings(context);
684
+ }
685
+
686
+ function buildChecklist(options: InitOptions): MigrationChecklistItem[] {
687
+ const items: MigrationChecklistItem[] = [
688
+ {
689
+ id: "route-map",
690
+ text: "Validate generated route map against existing URLs and dynamic params.",
691
+ },
692
+ {
693
+ id: "data-hooks",
694
+ text: "Port framework loaders/actions into Aurora query/action modules with explicit auth + input contracts.",
695
+ },
696
+ {
697
+ id: "run-doctor",
698
+ text: "Run `aurora doctor` and resolve migration/security/performance findings before cutover.",
699
+ },
700
+ {
701
+ id: "test-parity",
702
+ text: "Add integration checks for route parity and mutation behavior before traffic switching.",
703
+ },
704
+ ];
705
+
706
+ if (options.mode === "incremental") {
707
+ items.push({
708
+ id: "incremental-mount",
709
+ text: "Mount generated Aurora handler under a sub-path and shift traffic route-by-route.",
710
+ });
711
+ }
712
+
713
+ if (options.bridge === "react") {
714
+ items.push({
715
+ id: "react-interop",
716
+ text: "Wrap legacy React components via `fromReact(...)` only where migration bridging is required.",
717
+ });
718
+ }
719
+
720
+ return items;
721
+ }
722
+
723
+ function renderRoutePageStub(mapping: RouteMapping): string {
724
+ return [
725
+ `// Source: ${mapping.sourcePath}`,
726
+ `// Convention: ${mapping.convention}`,
727
+ "",
728
+ `export const routePath = "${mapping.auroraPath}";`,
729
+ "",
730
+ "export default function Page(): string {",
731
+ ` return \"TODO: migrate ${mapping.sourcePath} to Aurora route contract.\";`,
732
+ "}",
733
+ "",
734
+ ].join("\n");
735
+ }
736
+
737
+ function renderRouteClientStub(mapping: RouteMapping): string {
738
+ return [
739
+ `// Source: ${mapping.sourcePath}`,
740
+ "",
741
+ "export function RouteClientView(): string {",
742
+ ` return \"TODO: client interop for ${mapping.auroraPath}\";`,
743
+ "}",
744
+ "",
745
+ ].join("\n");
746
+ }
747
+
748
+ function renderRouteServerStub(mapping: RouteMapping): string {
749
+ return [
750
+ `// Source: ${mapping.sourcePath}`,
751
+ "",
752
+ `export const auth = "user";`,
753
+ "",
754
+ "export async function loadRouteData(): Promise<Record<string, unknown>> {",
755
+ " return {",
756
+ ` migratedFrom: ${JSON.stringify(mapping.sourcePath)},`,
757
+ ` frameworkPath: ${JSON.stringify(mapping.frameworkPath)},`,
758
+ " };",
759
+ "}",
760
+ "",
761
+ ].join("\n");
762
+ }
763
+
764
+ function buildScaffoldFiles(
765
+ report: Omit<InitMigrationReport, "generatedFiles">,
766
+ ): GeneratedScaffoldFile[] {
767
+ const files: GeneratedScaffoldFile[] = [];
768
+ const checklistMarkdown = [
769
+ "# Aurora Migration Checklist",
770
+ "",
771
+ `Framework source: ${report.framework}`,
772
+ `Mode: ${report.migrationMode}`,
773
+ `Bridge: ${report.bridge}`,
774
+ "",
775
+ "## Tasks",
776
+ ...report.checklist.map((item) => `- [ ] (${item.id}) ${item.text}`),
777
+ "",
778
+ "## Route Summary",
779
+ `- detected routes: ${report.detectedRoutes}`,
780
+ `- loader candidates: ${report.loaderCandidates}`,
781
+ `- action candidates: ${report.actionCandidates}`,
782
+ "",
783
+ "## Warnings",
784
+ ...(report.warnings.length > 0 ? report.warnings.map((warning) => `- ${warning}`) : ["- none"]),
785
+ "",
786
+ ].join("\n");
787
+
788
+ files.push({
789
+ path: "migration-checklist.md",
790
+ content: checklistMarkdown,
791
+ });
792
+ files.push({
793
+ path: "route-map.json",
794
+ content: JSON.stringify(report.mappedRoutes, null, 2),
795
+ });
796
+ files.push({
797
+ path: "migration-report.json",
798
+ content: JSON.stringify(
799
+ {
800
+ mode: report.mode,
801
+ framework: report.framework,
802
+ migrationMode: report.migrationMode,
803
+ bridge: report.bridge,
804
+ detectedRoutes: report.detectedRoutes,
805
+ loaderCandidates: report.loaderCandidates,
806
+ actionCandidates: report.actionCandidates,
807
+ warnings: report.warnings,
808
+ generatedAt: "init-runtime",
809
+ },
810
+ null,
811
+ 2,
812
+ ),
813
+ });
814
+ files.push({
815
+ path: "README.md",
816
+ content: [
817
+ "# Aurora Migration Scaffold",
818
+ "",
819
+ "Generated by `aurora init`.",
820
+ "",
821
+ "Folders:",
822
+ "- `aurora-routes/` route stubs mapped to Aurora 3-file conventions",
823
+ "- `incremental/` mount helper for gradual rollout",
824
+ "- `interop/` React bridge example for migration-period reuse",
825
+ "",
826
+ ].join("\n"),
827
+ });
828
+
829
+ for (const mapping of report.mappedRoutes) {
830
+ const [pagePath, clientPath, serverPath] = mapping.targetFiles;
831
+ files.push({
832
+ path: pagePath,
833
+ content: renderRoutePageStub(mapping),
834
+ });
835
+ files.push({
836
+ path: clientPath,
837
+ content: renderRouteClientStub(mapping),
838
+ });
839
+ files.push({
840
+ path: serverPath,
841
+ content: renderRouteServerStub(mapping),
842
+ });
843
+ }
844
+
845
+ if (report.migrationMode === "incremental") {
846
+ files.push({
847
+ path: "incremental/mount.server.ts",
848
+ content: [
849
+ 'import { createAuroraHandler } from "@aurora/adapter-node";',
850
+ "",
851
+ "export const auroraMount = createAuroraHandler({",
852
+ ' basePath: "/aurora",',
853
+ ' routes: "./aurora-routes",',
854
+ " async authBridge() {",
855
+ " // TODO: map existing session context into Aurora auth shape.",
856
+ " return { role: \"user\", permissions: [] };",
857
+ " },",
858
+ "});",
859
+ "",
860
+ ].join("\n"),
861
+ });
862
+ }
863
+
864
+ if (report.bridge === "react") {
865
+ files.push({
866
+ path: "interop/react-bridge.tsx",
867
+ content: [
868
+ 'import { fromReact } from "@aurora/runtime-interop";',
869
+ 'import { DatePicker } from "existing-react-library";',
870
+ "",
871
+ "export const AuroraDatePicker = fromReact(DatePicker);",
872
+ "",
873
+ ].join("\n"),
874
+ });
875
+ }
876
+
877
+ return files;
878
+ }
879
+
880
+ function isNonEmptyDirectory(path: string): boolean {
881
+ if (!existsSync(path)) {
882
+ return false;
883
+ }
884
+
885
+ try {
886
+ return readdirSync(path).length > 0;
887
+ } catch {
888
+ return false;
889
+ }
890
+ }
891
+
892
+ function ensureParentDirectory(path: string): void {
893
+ mkdirSync(dirname(path), { recursive: true });
894
+ }
895
+
896
+ function buildReport(
897
+ options: InitOptions,
898
+ context: CommandContext,
899
+ ): InitMigrationReport | CommandResult {
900
+ const scan = detectFrameworkProject(options, context);
901
+ const checklist = buildChecklist(options);
902
+ const baseReport: Omit<InitMigrationReport, "generatedFiles"> = {
903
+ mode: "init-migration",
904
+ projectRoot: context.cwd,
905
+ framework: options.framework,
906
+ migrationMode: options.mode,
907
+ bridge: options.bridge,
908
+ outDir: options.outDir,
909
+ dryRun: options.dryRun,
910
+ detectedRoutes: scan.routes.length,
911
+ loaderCandidates: scan.loaderCandidates,
912
+ actionCandidates: scan.actionCandidates,
913
+ mappedRoutes: scan.routes,
914
+ checklist,
915
+ warnings: scan.warnings,
916
+ };
917
+
918
+ const scaffoldFiles = buildScaffoldFiles(baseReport);
919
+ const generatedFiles = scaffoldFiles.map((file) => file.path);
920
+ const outputRoot = resolve(context.cwd, options.outDir);
921
+
922
+ if (!options.dryRun) {
923
+ if (isNonEmptyDirectory(outputRoot)) {
924
+ return {
925
+ exitCode: 2,
926
+ stderr:
927
+ `aurora init: out-dir already exists and is not empty: ${options.outDir}\n` +
928
+ "Use --out-dir <new-path> or run with --dry-run first.",
929
+ };
930
+ }
931
+
932
+ for (const file of scaffoldFiles) {
933
+ const absolutePath = resolve(outputRoot, file.path);
934
+ ensureParentDirectory(absolutePath);
935
+ writeFileSync(absolutePath, file.content, "utf8");
936
+ }
937
+ }
938
+
939
+ return {
940
+ ...baseReport,
941
+ generatedFiles,
942
+ };
943
+ }
944
+
945
+ function renderTextReport(report: InitMigrationReport): string {
946
+ const lines = [
947
+ "aurora init migration report",
948
+ `project_root: ${report.projectRoot}`,
949
+ `framework: ${report.framework}`,
950
+ `mode: ${report.migrationMode}`,
951
+ `bridge: ${report.bridge}`,
952
+ `out_dir: ${report.outDir}`,
953
+ `dry_run: ${report.dryRun}`,
954
+ `detected_routes: ${report.detectedRoutes}`,
955
+ `loader_candidates: ${report.loaderCandidates}`,
956
+ `action_candidates: ${report.actionCandidates}`,
957
+ `generated_files: ${report.generatedFiles.length}`,
958
+ `warnings: ${report.warnings.length}`,
959
+ "",
960
+ "mapped_routes:",
961
+ ];
962
+
963
+ if (report.mappedRoutes.length === 0) {
964
+ lines.push("- none");
965
+ } else {
966
+ for (const mapping of report.mappedRoutes) {
967
+ lines.push(
968
+ `- ${mapping.sourcePath} -> ${mapping.auroraPath} (${mapping.convention})`,
969
+ );
970
+ }
971
+ }
972
+
973
+ lines.push("", "checklist:");
974
+ for (const item of report.checklist) {
975
+ lines.push(`- (${item.id}) ${item.text}`);
976
+ }
977
+
978
+ lines.push("", "output_files:");
979
+ for (const file of report.generatedFiles) {
980
+ lines.push(`- ${file}`);
981
+ }
982
+
983
+ return lines.join("\n");
984
+ }
985
+
986
+ export function runInitCommand(
987
+ args: ReadonlyArray<string>,
988
+ context: CommandContext,
989
+ ): CommandResult {
990
+ const options = parseInitOptions(args);
991
+ if ("exitCode" in options) {
992
+ return options;
993
+ }
994
+
995
+ const report = buildReport(options, context);
996
+ if ("exitCode" in report) {
997
+ return report;
998
+ }
999
+
1000
+ if (options.format === "json") {
1001
+ return {
1002
+ exitCode: 0,
1003
+ stdout: JSON.stringify(report, null, 2),
1004
+ };
1005
+ }
1006
+
1007
+ return {
1008
+ exitCode: 0,
1009
+ stdout: renderTextReport(report),
1010
+ };
1011
+ }