@stacksjs/buddy 0.70.59 → 0.70.61

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 (79) hide show
  1. package/dist/cli.js +164 -4
  2. package/dist/commands/about.js +35 -0
  3. package/dist/commands/add.d.ts +2 -0
  4. package/dist/commands/add.js +33 -0
  5. package/dist/commands/auth.js +68 -0
  6. package/dist/commands/build.js +153 -0
  7. package/dist/commands/cd.js +39 -0
  8. package/dist/commands/changelog.js +32 -0
  9. package/dist/commands/clean.js +37 -0
  10. package/dist/commands/cloud.js +503 -0
  11. package/dist/commands/commit.js +16 -0
  12. package/dist/commands/completion.js +143 -0
  13. package/dist/commands/config-migrate.js +94 -0
  14. package/dist/commands/configure.js +48 -0
  15. package/dist/commands/create.js +137 -0
  16. package/dist/commands/deploy.js +1404 -0
  17. package/dist/commands/dev.js +911 -0
  18. package/dist/commands/dns.js +62 -0
  19. package/dist/commands/doctor.js +280 -0
  20. package/dist/commands/domains.js +159 -0
  21. package/dist/commands/email.js +526 -0
  22. package/dist/commands/env.js +246 -0
  23. package/dist/commands/features.js +422 -0
  24. package/dist/commands/fresh.js +39 -0
  25. package/dist/commands/generate.js +104 -0
  26. package/dist/commands/http.js +30 -0
  27. package/dist/commands/index.js +53 -0
  28. package/dist/commands/install.js +23 -0
  29. package/dist/commands/key.js +23 -0
  30. package/dist/commands/lint.js +50 -0
  31. package/dist/commands/list.js +108 -0
  32. package/dist/commands/mail.js +693 -0
  33. package/dist/commands/maintenance.d.ts +2 -0
  34. package/dist/commands/maintenance.js +141 -0
  35. package/dist/commands/make.js +375 -0
  36. package/dist/commands/migrate-project.js +49 -0
  37. package/dist/commands/migrate.js +373 -0
  38. package/dist/commands/outdated.js +19 -0
  39. package/dist/commands/package.d.ts +2 -0
  40. package/dist/commands/package.js +17 -0
  41. package/dist/commands/phone.js +188 -0
  42. package/dist/commands/ports.js +118 -0
  43. package/dist/commands/prepublish.js +17 -0
  44. package/dist/commands/projects.js +29 -0
  45. package/dist/commands/publish.js +169 -0
  46. package/dist/commands/queue.js +249 -0
  47. package/dist/commands/release.js +30 -0
  48. package/dist/commands/route.js +21 -0
  49. package/dist/commands/saas.js +25 -0
  50. package/dist/commands/schedule.js +61 -0
  51. package/dist/commands/search.js +84 -0
  52. package/dist/commands/seed.js +71 -0
  53. package/dist/commands/serve.js +176 -0
  54. package/dist/commands/setup.js +215 -0
  55. package/dist/commands/share.d.ts +2 -0
  56. package/dist/commands/share.js +209 -0
  57. package/dist/commands/sms.js +328 -0
  58. package/dist/commands/stacks.d.ts +2 -0
  59. package/dist/commands/stacks.js +69 -0
  60. package/dist/commands/telemetry.js +74 -0
  61. package/dist/commands/test.js +130 -0
  62. package/dist/commands/tinker.js +37 -0
  63. package/dist/commands/types.js +18 -0
  64. package/dist/commands/upgrade.js +97 -0
  65. package/dist/commands/version.js +16 -0
  66. package/dist/config.d.ts +43 -0
  67. package/dist/config.js +223 -0
  68. package/dist/custom-cli.d.ts +1 -0
  69. package/dist/custom-cli.js +23 -0
  70. package/dist/index.js +1 -3424
  71. package/dist/lazy-commands.d.ts +61 -0
  72. package/dist/lazy-commands.js +182 -0
  73. package/dist/migrators/index.js +62 -0
  74. package/dist/migrators/laravel/index.js +148 -0
  75. package/dist/migrators/laravel/migrations.js +231 -0
  76. package/dist/migrators/laravel/models.js +132 -0
  77. package/dist/migrators/rails/index.js +11 -0
  78. package/dist/migrators/types.js +0 -0
  79. package/package.json +1 -1
@@ -0,0 +1,61 @@
1
+ import type { CLI } from '@stacksjs/types';
2
+ /** Mark a registrar as already invoked for this CLI so loadCommand skips it. */
3
+ export declare function markLoaded(buddy: CLI, commandName: string): void;
4
+ /**
5
+ * Load a specific command lazily
6
+ */
7
+ export declare function loadCommand(commandName: string, buddy: CLI): Promise<boolean>;
8
+ /**
9
+ * Load multiple commands with timeout.
10
+ *
11
+ * Many registry entries point at the same module (e.g. `down`, `up`, `status`,
12
+ * `maintenance` all share `./commands/maintenance.ts`, and `cloud:*` aliases
13
+ * share `./commands/cloud.ts`). Calling each registrar more than once would
14
+ * register the same sub-commands multiple times, which is what produces
15
+ * duplicate rows in `buddy list`. Dedupe by loader identity so each module's
16
+ * registrar runs at most once per CLI instance.
17
+ */
18
+ export declare function loadCommands(commandNames: string[], buddy: CLI): Promise<void>;
19
+ /**
20
+ * Load a group of related commands
21
+ */
22
+ export declare function loadCommandGroup(groupName: keyof typeof commandGroups, buddy: CLI): Promise<void>;
23
+ /**
24
+ * Load all commands (for interactive mode or list command)
25
+ */
26
+ export declare function loadAllCommands(buddy: CLI): Promise<void>;
27
+ /**
28
+ * Get command names for autocomplete/suggestions
29
+ * (without actually loading the commands)
30
+ */
31
+ export declare function getCommandNames(): string[];
32
+ /**
33
+ * Determine which commands to load based on CLI arguments.
34
+ *
35
+ * Help / no-command paths must load the full registry so the rendered help
36
+ * lists every command. Pure version flags only need `version`. A specific
37
+ * command (with or without `--help`) only needs that command's loader.
38
+ */
39
+ export declare function getCommandsToLoad(args: string[]): string[];
40
+ /**
41
+ * Commands that are commonly used together (for batch loading optimization)
42
+ * @defaultValue
43
+ * ```ts
44
+ * {
45
+ * 'minimal': ['version', 'help'],
46
+ * 'development': ['dev', 'build', 'test', 'lint'],
47
+ * 'database': ['migrate', 'seed', 'fresh'],
48
+ * 'scaffolding': ['make', 'generate'],
49
+ * 'deployment': ['deploy', 'release', 'cloud'],
50
+ * 'info': ['about', 'doctor', 'list']
51
+ * }
52
+ * ```
53
+ */
54
+ declare const commandGroups: {
55
+ 'minimal': string[];
56
+ 'development': string[];
57
+ 'database': string[];
58
+ 'scaffolding': string[];
59
+ 'deployment': string[];
60
+ 'info': string[]
61
+ };
@@ -0,0 +1,182 @@
1
+ const commandRegistry = {
2
+ about: { path: "./commands/about.js", exportName: "about" },
3
+ add: { path: "./commands/add.js", exportName: "add" },
4
+ auth: { path: "./commands/auth.js", exportName: "auth" },
5
+ build: { path: "./commands/build.js", exportName: "build" },
6
+ cd: { path: "./commands/cd.js", exportName: "cd" },
7
+ changelog: { path: "./commands/changelog.js", exportName: "changelog" },
8
+ clean: { path: "./commands/clean.js", exportName: "clean" },
9
+ cloud: { path: "./commands/cloud.js", exportName: "cloud" },
10
+ commit: { path: "./commands/commit.js", exportName: "commit" },
11
+ completion: { path: "./commands/completion.js", exportName: "completion" },
12
+ "config:migrate": { path: "./commands/config-migrate.js", exportName: "configMigrate" },
13
+ configure: { path: "./commands/configure.js", exportName: "configure" },
14
+ create: { path: "./commands/create.js", exportName: "create" },
15
+ deploy: { path: "./commands/deploy.js", exportName: "deploy" },
16
+ dev: { path: "./commands/dev.js", exportName: "dev" },
17
+ dns: { path: "./commands/dns.js", exportName: "dns" },
18
+ doctor: { path: "./commands/doctor.js", exportName: "doctor" },
19
+ domains: { path: "./commands/domains.js", exportName: "domains" },
20
+ email: { path: "./commands/email.js", exportName: "email" },
21
+ env: { path: "./commands/env.js", exportName: "env" },
22
+ "dashboard:install": { path: "./commands/features.js", exportName: "features" },
23
+ "dashboard:uninstall": { path: "./commands/features.js", exportName: "features" },
24
+ "commerce:install": { path: "./commands/features.js", exportName: "features" },
25
+ "commerce:uninstall": { path: "./commands/features.js", exportName: "features" },
26
+ "cms:install": { path: "./commands/features.js", exportName: "features" },
27
+ "cms:uninstall": { path: "./commands/features.js", exportName: "features" },
28
+ "marketing:install": { path: "./commands/features.js", exportName: "features" },
29
+ "marketing:uninstall": { path: "./commands/features.js", exportName: "features" },
30
+ "monitoring:install": { path: "./commands/features.js", exportName: "features" },
31
+ "monitoring:uninstall": { path: "./commands/features.js", exportName: "features" },
32
+ "realtime:install": { path: "./commands/features.js", exportName: "features" },
33
+ "realtime:uninstall": { path: "./commands/features.js", exportName: "features" },
34
+ "queue:install": { path: "./commands/features.js", exportName: "features" },
35
+ "queue:uninstall": { path: "./commands/features.js", exportName: "features" },
36
+ fresh: { path: "./commands/fresh.js", exportName: "fresh" },
37
+ generate: { path: "./commands/generate.js", exportName: "generate" },
38
+ http: { path: "./commands/http.js", exportName: "http" },
39
+ install: { path: "./commands/install.js", exportName: "install" },
40
+ key: { path: "./commands/key.js", exportName: "key" },
41
+ lint: { path: "./commands/lint.js", exportName: "lint" },
42
+ list: { path: "./commands/list.js", exportName: "list" },
43
+ mail: { path: "./commands/mail.js", exportName: "mailCommands" },
44
+ "mail:preview": { path: "./commands/mail.js", exportName: "mailCommands" },
45
+ maintenance: { path: "./commands/maintenance.js", exportName: "maintenance" },
46
+ down: { path: "./commands/maintenance.js", exportName: "maintenance" },
47
+ up: { path: "./commands/maintenance.js", exportName: "maintenance" },
48
+ status: { path: "./commands/maintenance.js", exportName: "maintenance" },
49
+ make: { path: "./commands/make.js", exportName: "make" },
50
+ migrate: { path: "./commands/migrate.js", exportName: "migrate" },
51
+ "migrate:fresh": { path: "./commands/migrate.js", exportName: "migrate" },
52
+ "migrate:switch": { path: "./commands/migrate.js", exportName: "migrate" },
53
+ "migrate:dns": { path: "./commands/migrate.js", exportName: "migrate" },
54
+ "migrate:project": { path: "./commands/migrate-project.js", exportName: "migrateProject" },
55
+ outdated: { path: "./commands/outdated.js", exportName: "outdated" },
56
+ package: { path: "./commands/package.js", exportName: "packageCommands" },
57
+ phone: { path: "./commands/phone.js", exportName: "phone" },
58
+ ports: { path: "./commands/ports.js", exportName: "ports" },
59
+ prepublish: { path: "./commands/prepublish.js", exportName: "prepublish" },
60
+ projects: { path: "./commands/projects.js", exportName: "projects" },
61
+ publish: { path: "./commands/publish.js", exportName: "publish" },
62
+ "publish:model": { path: "./commands/publish.js", exportName: "publish" },
63
+ "publish:controller": { path: "./commands/publish.js", exportName: "publish" },
64
+ "publish:middleware": { path: "./commands/publish.js", exportName: "publish" },
65
+ "publish:action": { path: "./commands/publish.js", exportName: "publish" },
66
+ "publish:core": { path: "./commands/publish.js", exportName: "publish" },
67
+ queue: { path: "./commands/queue.js", exportName: "queue" },
68
+ release: { path: "./commands/release.js", exportName: "release" },
69
+ route: { path: "./commands/route.js", exportName: "route" },
70
+ saas: { path: "./commands/saas.js", exportName: "saas" },
71
+ schedule: { path: "./commands/schedule.js", exportName: "schedule" },
72
+ search: { path: "./commands/search.js", exportName: "search" },
73
+ seed: { path: "./commands/seed.js", exportName: "seed" },
74
+ "seed:roles": { path: "./commands/seed.js", exportName: "seed" },
75
+ "roles:seed": { path: "./commands/seed.js", exportName: "seed" },
76
+ serve: { path: "./commands/serve.js", exportName: "serve" },
77
+ "serve:api": { path: "./commands/serve.js", exportName: "serveApi" },
78
+ setup: { path: "./commands/setup.js", exportName: "setup" },
79
+ share: { path: "./commands/share.js", exportName: "share" },
80
+ stack: { path: "./commands/stacks.js", exportName: "stacks" },
81
+ sms: { path: "./commands/sms.js", exportName: "sms" },
82
+ telemetry: { path: "./commands/telemetry.js", exportName: "telemetryCommand" },
83
+ test: { path: "./commands/test.js", exportName: "test" },
84
+ tinker: { path: "./commands/tinker.js", exportName: "tinker" },
85
+ types: { path: "./commands/types.js", exportName: "types" },
86
+ undeploy: { path: "./commands/cloud.js", exportName: "cloud" },
87
+ upgrade: { path: "./commands/upgrade.js", exportName: "upgrade" },
88
+ version: { path: "./commands/version.js", exportName: "version" }
89
+ }, commandGroups = {
90
+ minimal: ["version", "help"],
91
+ development: ["dev", "build", "test", "lint"],
92
+ database: ["migrate", "seed", "fresh"],
93
+ scaffolding: ["make", "generate"],
94
+ deployment: ["deploy", "release", "cloud"],
95
+ info: ["about", "doctor", "list"]
96
+ }, loadedRegistrars = new WeakMap;
97
+ function loaderKey(loader) {
98
+ return `${loader.path}#${loader.exportName}`;
99
+ }
100
+ export function markLoaded(buddy, commandName) {
101
+ const loader = commandRegistry[commandName];
102
+ if (!loader)
103
+ return;
104
+ let set = loadedRegistrars.get(buddy);
105
+ if (!set) {
106
+ set = new Set;
107
+ loadedRegistrars.set(buddy, set);
108
+ }
109
+ set.add(loaderKey(loader));
110
+ }
111
+ export async function loadCommand(commandName, buddy) {
112
+ const loader = commandRegistry[commandName];
113
+ if (!loader)
114
+ return !1;
115
+ let set = loadedRegistrars.get(buddy);
116
+ if (!set) {
117
+ set = new Set;
118
+ loadedRegistrars.set(buddy, set);
119
+ }
120
+ const key = loaderKey(loader);
121
+ if (set.has(key))
122
+ return !0;
123
+ set.add(key);
124
+ try {
125
+ const commandFunction = (await import(loader.path))[loader.exportName];
126
+ if (typeof commandFunction === "function") {
127
+ commandFunction(buddy);
128
+ return !0;
129
+ } else
130
+ return !1;
131
+ } catch {
132
+ return !1;
133
+ }
134
+ }
135
+ export async function loadCommands(commandNames, buddy) {
136
+ const timeout = (ms) => new Promise((_, reject) => setTimeout(() => reject(Error("timeout")), ms)), seen = new Set, unique = [];
137
+ for (const name of commandNames) {
138
+ const loader = commandRegistry[name], key = loader ? `${loader.path}#${loader.exportName}` : `name:${name}`;
139
+ if (seen.has(key))
140
+ continue;
141
+ seen.add(key);
142
+ unique.push(name);
143
+ }
144
+ await Promise.all(unique.map(async (name) => {
145
+ try {
146
+ await Promise.race([
147
+ loadCommand(name, buddy),
148
+ timeout(5000)
149
+ ]);
150
+ } catch {}
151
+ }));
152
+ }
153
+ export async function loadCommandGroup(groupName, buddy) {
154
+ const commands = commandGroups[groupName];
155
+ if (commands)
156
+ await loadCommands(commands, buddy);
157
+ }
158
+ export async function loadAllCommands(buddy) {
159
+ const allCommands = Object.keys(commandRegistry);
160
+ await loadCommands(allCommands, buddy);
161
+ }
162
+ export function getCommandNames() {
163
+ return Object.keys(commandRegistry);
164
+ }
165
+ export function getCommandsToLoad(args) {
166
+ const requestedCommand = args[0], isVersionFlag = requestedCommand === "--version" || requestedCommand === "-v", isHelpFlag = requestedCommand === "--help" || requestedCommand === "-h", isHelpWord = requestedCommand === "help";
167
+ if (isVersionFlag)
168
+ return ["version"];
169
+ if (!requestedCommand || isHelpFlag || isHelpWord)
170
+ return Object.keys(commandRegistry);
171
+ const baseCommand = requestedCommand.split(":")[0];
172
+ if (baseCommand === "list")
173
+ return ["list", ...Object.keys(commandRegistry).filter((k) => k !== "list")];
174
+ if (commandRegistry[baseCommand])
175
+ return [baseCommand];
176
+ if (requestedCommand.includes(":")) {
177
+ const namespace = baseCommand;
178
+ if (commandRegistry[namespace])
179
+ return [namespace];
180
+ }
181
+ return Object.keys(commandRegistry);
182
+ }
@@ -0,0 +1,62 @@
1
+ import { laravelDriver } from "./laravel";
2
+ import { railsDriver } from "./rails";
3
+ export const DRIVERS = {
4
+ laravel: laravelDriver,
5
+ rails: railsDriver
6
+ };
7
+ export async function runMigrator(req) {
8
+ const driver = DRIVERS[req.from];
9
+ if (!driver)
10
+ throw Error(`Unknown source framework: ${req.from}. Supported: ${Object.keys(DRIVERS).join(", ")}`);
11
+ const startedAt = new Date().toISOString(), entries = await driver.migrate(req), finishedAt = new Date().toISOString();
12
+ return {
13
+ source: req.source,
14
+ target: req.target,
15
+ from: req.from,
16
+ startedAt,
17
+ finishedAt,
18
+ entries
19
+ };
20
+ }
21
+ export function renderReport(report) {
22
+ const lines = [];
23
+ lines.push(`# Migration report \u2014 ${report.from} \u2192 Stacks`);
24
+ lines.push("");
25
+ lines.push(`- **Source**: \`${report.source}\``);
26
+ lines.push(`- **Target**: \`${report.target}\``);
27
+ lines.push(`- **Started**: ${report.startedAt}`);
28
+ lines.push(`- **Finished**: ${report.finishedAt}`);
29
+ lines.push("");
30
+ const groups = groupEntriesByStatus(report.entries);
31
+ for (const [status, label] of [
32
+ ["translated", "Translated"],
33
+ ["copied", "Copied verbatim"],
34
+ ["skipped", "Skipped (hand-port required)"],
35
+ ["failed", "Failed"]
36
+ ]) {
37
+ const group = groups[status] ?? [];
38
+ if (group.length === 0)
39
+ continue;
40
+ lines.push(`## ${label} (${group.length})`);
41
+ lines.push("");
42
+ for (const e of group) {
43
+ const arrow = e.target ? ` \u2192 \`${e.target}\`` : "", note = e.note ? ` \u2014 ${e.note}` : "";
44
+ lines.push(`- \`${e.source}\`${arrow}${note}`);
45
+ }
46
+ lines.push("");
47
+ }
48
+ lines.push("---");
49
+ lines.push("");
50
+ lines.push("Re-run `./buddy migrate:project <name> --from=" + report.from + "` after fixing the source project to refresh emitted files. Translated outputs are overwritten; skipped entries are not.");
51
+ return lines.join(`
52
+ `);
53
+ }
54
+ function groupEntriesByStatus(entries) {
55
+ const out = {};
56
+ for (const e of entries) {
57
+ if (!out[e.status])
58
+ out[e.status] = [];
59
+ out[e.status].push(e);
60
+ }
61
+ return out;
62
+ }
@@ -0,0 +1,148 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { basename, join } from "node:path";
4
+ import { laravelFilenameToStacks, parseLaravelMigration } from "./migrations";
5
+ import { parseLaravelModel } from "./models";
6
+ export const laravelDriver = {
7
+ name: "laravel",
8
+ async migrate(req) {
9
+ const entries = [];
10
+ entries.push(...await migrateEnv(req));
11
+ entries.push(...await migrateMigrations(req));
12
+ entries.push(...await migrateModels(req));
13
+ entries.push(...summariseDeferred(req));
14
+ return entries;
15
+ }
16
+ };
17
+ async function migrateEnv(req) {
18
+ const out = [];
19
+ for (const file of [".env", ".env.example"]) {
20
+ const sourcePath = join(req.source, file);
21
+ if (!existsSync(sourcePath))
22
+ continue;
23
+ const targetPath = join(req.target, file);
24
+ if (!req.dryRun) {
25
+ await mkdir(req.target, { recursive: !0 });
26
+ await copyFile(sourcePath, targetPath);
27
+ }
28
+ out.push({
29
+ source: file,
30
+ target: file,
31
+ status: "copied",
32
+ note: "Laravel-specific keys (BROADCAST_DRIVER, FILESYSTEM_DISK names) may need renaming; Stacks reads APP_*/DB_*/MAIL_* keys as-is."
33
+ });
34
+ }
35
+ return out;
36
+ }
37
+ async function migrateMigrations(req) {
38
+ const dir = join(req.source, "database", "migrations");
39
+ if (!existsSync(dir))
40
+ return [];
41
+ const files = readdirSync(dir).filter((f) => f.endsWith(".php")).sort(), targetDir = join(req.target, "database", "migrations");
42
+ if (!req.dryRun)
43
+ await mkdir(targetDir, { recursive: !0 });
44
+ const out = [];
45
+ for (let i = 0;i < files.length; i++) {
46
+ const file = files[i], rel = `database/migrations/${file}`;
47
+ try {
48
+ const body = await readFile(join(dir, file), "utf8"), parsed = parseLaravelMigration(body);
49
+ if (!parsed) {
50
+ out.push({
51
+ source: rel,
52
+ target: "",
53
+ status: "skipped",
54
+ note: "No `Schema::create(...)` block found (likely an alter / drop / data-only migration)."
55
+ });
56
+ continue;
57
+ }
58
+ const sequence = i + 1, outRel = `database/migrations/${laravelFilenameToStacks(file, sequence, parsed.table)}`;
59
+ if (!req.dryRun)
60
+ await writeFile(join(req.target, outRel), parsed.sql);
61
+ const skippedNote = parsed.skipped.length > 0 ? ` Skipped ${parsed.skipped.length} line(s) inside the create block \u2014 hand-port from the source migration.` : "";
62
+ out.push({
63
+ source: rel,
64
+ target: outRel,
65
+ status: "translated",
66
+ note: `CREATE TABLE \`${parsed.table}\` emitted.${skippedNote}`
67
+ });
68
+ } catch (err) {
69
+ out.push({
70
+ source: rel,
71
+ target: "",
72
+ status: "failed",
73
+ note: `Parse error: ${err instanceof Error ? err.message : String(err)}`
74
+ });
75
+ }
76
+ }
77
+ return out;
78
+ }
79
+ async function migrateModels(req) {
80
+ const dir = join(req.source, "app", "Models");
81
+ if (!existsSync(dir))
82
+ return [];
83
+ const files = readdirSync(dir).filter((f) => f.endsWith(".php")), targetDir = join(req.target, "app", "Models");
84
+ if (!req.dryRun)
85
+ await mkdir(targetDir, { recursive: !0 });
86
+ const out = [];
87
+ for (const file of files) {
88
+ const rel = `app/Models/${file}`;
89
+ try {
90
+ const body = await readFile(join(dir, file), "utf8"), parsed = parseLaravelModel(body);
91
+ if (!parsed) {
92
+ out.push({
93
+ source: rel,
94
+ target: "",
95
+ status: "skipped",
96
+ note: "No class declaration found."
97
+ });
98
+ continue;
99
+ }
100
+ const outRel = `app/Models/${`${parsed.className}.ts`}`;
101
+ if (!req.dryRun)
102
+ await writeFile(join(req.target, outRel), parsed.tsSource);
103
+ const notes = [
104
+ `Attributes: ${parsed.fillable.length} fillable, ${Object.keys(parsed.casts).length} cast`,
105
+ parsed.relationships.length > 0 ? `Relationships: ${parsed.relationships.map((r) => `${r.name}(${r.kind})`).join(", ")}` : null,
106
+ ...parsed.notes
107
+ ].filter(Boolean).join(" | ");
108
+ out.push({
109
+ source: rel,
110
+ target: outRel,
111
+ status: "translated",
112
+ note: notes || `Model ${parsed.className} emitted.`
113
+ });
114
+ } catch (err) {
115
+ out.push({
116
+ source: rel,
117
+ target: "",
118
+ status: "failed",
119
+ note: `Parse error: ${err instanceof Error ? err.message : String(err)}`
120
+ });
121
+ }
122
+ }
123
+ return out;
124
+ }
125
+ const DEFERRED_SURFACES = [
126
+ { path: "routes", note: "routes/web.php + routes/api.php \u2014 port to routes/web.ts + routes/api.ts using the Stacks router DSL." },
127
+ { path: "app/Http/Controllers", note: "Controllers \u2192 Stacks Actions in app/Actions/. Each controller method maps to one Action file." },
128
+ { path: "resources/views", note: "Blade templates \u2192 stx components in resources/views/." },
129
+ { path: "database/seeders", note: "Seeders \u2192 database/seeders/*.ts using Stacks SeederContract." },
130
+ { path: "app/Console/Commands", note: "Artisan commands \u2192 app/Commands/*.ts in your Stacks project." },
131
+ { path: "app/Jobs", note: "Queue jobs \u2192 app/Jobs/*.ts; pair with config/queue.ts driver settings." },
132
+ { path: "composer.json", note: "Dependency map \u2014 Laravel packages don't translate 1:1; the Stacks docs cover the closest @stacksjs/* equivalents." }
133
+ ];
134
+ function summariseDeferred(req) {
135
+ const out = [];
136
+ for (const surface of DEFERRED_SURFACES) {
137
+ const sourcePath = join(req.source, surface.path);
138
+ if (!existsSync(sourcePath))
139
+ continue;
140
+ out.push({
141
+ source: surface.path,
142
+ target: "",
143
+ status: "skipped",
144
+ note: surface.note
145
+ });
146
+ }
147
+ return out;
148
+ }
@@ -0,0 +1,231 @@
1
+ export function parseLaravelMigration(source) {
2
+ const createMatch = source.match(/Schema::create\(\s*['"]([a-z0-9_]+)['"]\s*,\s*function\s*\([^)]*\)\s*\{([\s\S]*?)\}\s*\)\s*;/i);
3
+ if (!createMatch)
4
+ return null;
5
+ const table = createMatch[1], body = createMatch[2], skipped = [], columns = [], indexes = [], lines = body.split(`
6
+ `).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("//") && !line.startsWith("*") && !line.startsWith("/*"));
7
+ for (const line of lines) {
8
+ const trimmed = line.replace(/;\s*$/, "");
9
+ if (!trimmed.startsWith("$table->")) {
10
+ skipped.push(line);
11
+ continue;
12
+ }
13
+ const parsed = parseColumnLine(trimmed);
14
+ if (!parsed) {
15
+ skipped.push(line);
16
+ continue;
17
+ }
18
+ if (parsed.kind === "columns") {
19
+ columns.push(...parsed.columns);
20
+ if (parsed.indexes)
21
+ indexes.push(...parsed.indexes);
22
+ } else if (parsed.kind === "index")
23
+ indexes.push(parsed.index);
24
+ else if (parsed.kind === "skip")
25
+ skipped.push(line);
26
+ }
27
+ const sql = emitCreateTable(table, columns, indexes);
28
+ return { table, sql, skipped };
29
+ }
30
+ function parseColumnLine(line) {
31
+ const methodMatch = line.match(/^\$table->([a-zA-Z_]+)\s*\(([^)]*)\)(.*)$/);
32
+ if (!methodMatch)
33
+ return null;
34
+ const method = methodMatch[1], args = methodMatch[2].trim(), rest = methodMatch[3];
35
+ if (method === "timestamps")
36
+ return {
37
+ kind: "columns",
38
+ columns: [
39
+ { name: "created_at", type: "DATETIME", nullable: !0, unique: !1, primaryKey: !1, autoIncrement: !1, defaultValue: "CURRENT_TIMESTAMP", index: !1 },
40
+ { name: "updated_at", type: "DATETIME", nullable: !0, unique: !1, primaryKey: !1, autoIncrement: !1, defaultValue: null, index: !1 }
41
+ ]
42
+ };
43
+ if (method === "softDeletes")
44
+ return {
45
+ kind: "columns",
46
+ columns: [
47
+ { name: "deleted_at", type: "DATETIME", nullable: !0, unique: !1, primaryKey: !1, autoIncrement: !1, defaultValue: null, index: !1 }
48
+ ]
49
+ };
50
+ if (method === "rememberToken")
51
+ return {
52
+ kind: "columns",
53
+ columns: [
54
+ { name: "remember_token", type: "TEXT", nullable: !0, unique: !1, primaryKey: !1, autoIncrement: !1, defaultValue: null, index: !1 }
55
+ ]
56
+ };
57
+ if (method === "id" || method === "increments" || method === "bigIncrements")
58
+ return {
59
+ kind: "columns",
60
+ columns: [
61
+ { name: parseStringArg(args) || "id", type: "INTEGER", nullable: !1, unique: !1, primaryKey: !0, autoIncrement: !0, defaultValue: null, index: !1 }
62
+ ]
63
+ };
64
+ if (method === "index" || method === "unique") {
65
+ const cols = parseIndexArg(args);
66
+ if (cols.length === 0)
67
+ return { kind: "skip" };
68
+ return {
69
+ kind: "index",
70
+ index: { name: "", columns: cols, unique: method === "unique" }
71
+ };
72
+ }
73
+ const columnName = parseStringArg(args);
74
+ if (!columnName)
75
+ return null;
76
+ const sqlType = mapType(method);
77
+ if (!sqlType)
78
+ return { kind: "skip" };
79
+ const column = {
80
+ name: columnName,
81
+ type: sqlType,
82
+ nullable: !1,
83
+ unique: !1,
84
+ primaryKey: !1,
85
+ autoIncrement: !1,
86
+ defaultValue: null,
87
+ index: !1
88
+ };
89
+ applyModifiers(rest, column);
90
+ const indexes = [];
91
+ if (column.unique)
92
+ indexes.push({ name: "", columns: [columnName], unique: !0 });
93
+ if (column.index)
94
+ indexes.push({ name: "", columns: [columnName], unique: !1 });
95
+ return { kind: "columns", columns: [column], indexes };
96
+ }
97
+ function applyModifiers(rest, column) {
98
+ const modRegex = /->([a-zA-Z_]+)\s*\(([^)]*)\)/g;
99
+ let match;
100
+ while ((match = modRegex.exec(rest)) !== null) {
101
+ const mod = match[1], args = match[2].trim();
102
+ switch (mod) {
103
+ case "nullable":
104
+ column.nullable = !0;
105
+ break;
106
+ case "unique":
107
+ column.unique = !0;
108
+ break;
109
+ case "index":
110
+ column.index = !0;
111
+ break;
112
+ case "default":
113
+ column.defaultValue = parseDefaultArg(args);
114
+ break;
115
+ case "primary":
116
+ column.primaryKey = !0;
117
+ break;
118
+ case "autoIncrement":
119
+ column.autoIncrement = !0;
120
+ break;
121
+ case "unsigned":
122
+ case "comment":
123
+ case "after":
124
+ case "change":
125
+ break;
126
+ default:
127
+ break;
128
+ }
129
+ }
130
+ }
131
+ function parseStringArg(args) {
132
+ const m = args.match(/^['"]([^'"]+)['"]/);
133
+ return m ? m[1] : null;
134
+ }
135
+ function parseIndexArg(args) {
136
+ const single = parseStringArg(args);
137
+ if (single)
138
+ return [single];
139
+ const arr = args.match(/\[([^\]]*)\]/);
140
+ if (!arr)
141
+ return [];
142
+ return arr[1].split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
143
+ }
144
+ function parseDefaultArg(args) {
145
+ const stringMatch = args.match(/^['"]([^'"]*)['"]/);
146
+ if (stringMatch)
147
+ return `'${stringMatch[1].replace(/'/g, "''")}'`;
148
+ const trimmed = args.trim();
149
+ if (trimmed === "true")
150
+ return "1";
151
+ if (trimmed === "false")
152
+ return "0";
153
+ if (trimmed === "null")
154
+ return "NULL";
155
+ if (/^-?\d+(\.\d+)?$/.test(trimmed))
156
+ return trimmed;
157
+ return trimmed;
158
+ }
159
+ const TYPE_MAP = {
160
+ string: "TEXT",
161
+ text: "TEXT",
162
+ mediumText: "TEXT",
163
+ longText: "TEXT",
164
+ char: "TEXT",
165
+ integer: "INTEGER",
166
+ tinyInteger: "INTEGER",
167
+ smallInteger: "INTEGER",
168
+ mediumInteger: "INTEGER",
169
+ bigInteger: "INTEGER",
170
+ unsignedBigInteger: "INTEGER",
171
+ unsignedInteger: "INTEGER",
172
+ foreignId: "INTEGER",
173
+ boolean: "INTEGER",
174
+ date: "DATE",
175
+ dateTime: "DATETIME",
176
+ dateTimeTz: "DATETIME",
177
+ timestamp: "DATETIME",
178
+ timestampTz: "DATETIME",
179
+ time: "TIME",
180
+ timeTz: "TIME",
181
+ year: "INTEGER",
182
+ json: "TEXT",
183
+ jsonb: "TEXT",
184
+ uuid: "TEXT",
185
+ ulid: "TEXT",
186
+ ipAddress: "TEXT",
187
+ macAddress: "TEXT",
188
+ float: "REAL",
189
+ double: "REAL",
190
+ decimal: "REAL",
191
+ binary: "BLOB",
192
+ enum: "TEXT",
193
+ set: "TEXT"
194
+ };
195
+ function mapType(laravelMethod) {
196
+ return TYPE_MAP[laravelMethod] ?? null;
197
+ }
198
+ function emitCreateTable(table, columns, indexes) {
199
+ const lines = [];
200
+ lines.push(`CREATE TABLE IF NOT EXISTS ${table} (`);
201
+ const colDefs = columns.map((c) => {
202
+ const parts = [` ${c.name} ${c.type}`];
203
+ if (c.primaryKey)
204
+ parts.push("PRIMARY KEY");
205
+ if (c.autoIncrement)
206
+ parts.push("AUTOINCREMENT");
207
+ if (!c.nullable && !c.primaryKey)
208
+ parts.push("NOT NULL");
209
+ if (c.defaultValue !== null)
210
+ parts.push(`DEFAULT ${c.defaultValue}`);
211
+ return parts.join(" ");
212
+ });
213
+ lines.push(colDefs.join(`,
214
+ `));
215
+ lines.push(");");
216
+ for (const idx of indexes) {
217
+ const cols = idx.columns.join(", "), indexName = idx.name || `${table}_${idx.columns.join("_")}_${idx.unique ? "unique" : "index"}`, verb = idx.unique ? "CREATE UNIQUE INDEX" : "CREATE INDEX";
218
+ lines.push(`${verb} IF NOT EXISTS ${indexName} ON ${table}(${cols});`);
219
+ }
220
+ return `${lines.join(`
221
+ `)}
222
+ `;
223
+ }
224
+ export function laravelFilenameToStacks(filename, sequence, table) {
225
+ const padded = String(sequence).padStart(10, "0"), base = filename.toLowerCase();
226
+ if (base.includes("create_") && base.includes("_table"))
227
+ return `${padded}-create-${table}-table.sql`;
228
+ if (base.includes("add_") || base.includes("alter_"))
229
+ return `${padded}-alter-${table}-migration.sql`;
230
+ return `${padded}-create-${table}-table.sql`;
231
+ }