@stacksjs/buddy 0.70.257 → 0.70.259

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 (113) hide show
  1. package/dist/cli.d.ts +11 -1
  2. package/dist/cli.js +5 -164
  3. package/dist/commands/about.js +1 -35
  4. package/dist/commands/add.js +1 -25
  5. package/dist/commands/ai-context.js +4 -27
  6. package/dist/commands/auth.js +1 -68
  7. package/dist/commands/build.js +1 -221
  8. package/dist/commands/cd.js +8 -39
  9. package/dist/commands/changelog.js +1 -32
  10. package/dist/commands/clean.js +1 -44
  11. package/dist/commands/cloud.js +2 -519
  12. package/dist/commands/commit.js +1 -16
  13. package/dist/commands/completion.js +6 -42
  14. package/dist/commands/config-migrate.js +6 -94
  15. package/dist/commands/configure.js +2 -45
  16. package/dist/commands/create.js +1 -162
  17. package/dist/commands/deploy.d.ts +10 -4
  18. package/dist/commands/deploy.js +13 -1664
  19. package/dist/commands/desktop-apple.js +12 -399
  20. package/dist/commands/dev.js +6 -1083
  21. package/dist/commands/dns.js +3 -103
  22. package/dist/commands/docs/buddy-commands.js +10 -132
  23. package/dist/commands/docs/buddy-commands.test.js +1 -38
  24. package/dist/commands/docs/framework-repo.js +1 -14
  25. package/dist/commands/docs/framework-repo.test.js +1 -68
  26. package/dist/commands/docs/generated-artifacts.js +3 -70
  27. package/dist/commands/docs/generated-artifacts.test.js +1 -19
  28. package/dist/commands/docs/links.js +2 -85
  29. package/dist/commands/docs/links.test.js +4 -66
  30. package/dist/commands/docs.js +1 -24
  31. package/dist/commands/doctor.js +2 -478
  32. package/dist/commands/domains.js +2 -152
  33. package/dist/commands/email.js +31 -503
  34. package/dist/commands/env.js +1 -312
  35. package/dist/commands/extension.js +1 -239
  36. package/dist/commands/features.js +9 -345
  37. package/dist/commands/fresh.js +1 -46
  38. package/dist/commands/generate.js +2 -152
  39. package/dist/commands/http.js +1 -33
  40. package/dist/commands/index.js +1 -57
  41. package/dist/commands/install.js +1 -22
  42. package/dist/commands/key.js +1 -23
  43. package/dist/commands/link.js +2 -147
  44. package/dist/commands/lint.js +2 -46
  45. package/dist/commands/list.js +1 -140
  46. package/dist/commands/mail.js +15 -1477
  47. package/dist/commands/maintenance.js +1 -141
  48. package/dist/commands/make.js +1 -392
  49. package/dist/commands/migrate-project.js +1 -49
  50. package/dist/commands/migrate.js +27 -625
  51. package/dist/commands/outdated.js +1 -19
  52. package/dist/commands/package.js +1 -17
  53. package/dist/commands/phone.js +17 -181
  54. package/dist/commands/ports.js +2 -118
  55. package/dist/commands/prepublish.js +1 -17
  56. package/dist/commands/projects.js +1 -29
  57. package/dist/commands/protocol/craft-evidence.js +4 -98
  58. package/dist/commands/protocol/craft-evidence.test.js +1 -20
  59. package/dist/commands/protocol/desktop-lifecycle-report.js +3 -57
  60. package/dist/commands/protocol/desktop-lifecycle-report.test.js +1 -32
  61. package/dist/commands/protocol/desktop-support.js +2 -32
  62. package/dist/commands/protocol/desktop-workflow.test.js +3 -35
  63. package/dist/commands/protocol/driver-registry.js +2 -71
  64. package/dist/commands/protocol/pantry-evidence.js +4 -144
  65. package/dist/commands/protocol/pantry-evidence.test.js +1 -29
  66. package/dist/commands/protocol/release-manifest.js +3 -102
  67. package/dist/commands/protocol/release-manifest.test.js +2 -34
  68. package/dist/commands/protocol/run-conformance.js +4 -346
  69. package/dist/commands/protocol/run-conformance.test.js +1 -52
  70. package/dist/commands/protocol/run-driver-contracts.js +4 -83
  71. package/dist/commands/protocol/run-driver-contracts.test.js +1 -18
  72. package/dist/commands/protocol/source-manifest.js +3 -113
  73. package/dist/commands/protocol/sync-suite.js +3 -136
  74. package/dist/commands/protocol/sync-suite.test.js +1 -44
  75. package/dist/commands/protocol.js +1 -60
  76. package/dist/commands/publish.js +8 -500
  77. package/dist/commands/queue.js +1 -248
  78. package/dist/commands/release.js +1 -42
  79. package/dist/commands/route.js +1 -21
  80. package/dist/commands/run-tool.js +1 -9
  81. package/dist/commands/saas.js +1 -25
  82. package/dist/commands/schedule.js +1 -61
  83. package/dist/commands/search.js +1 -84
  84. package/dist/commands/seed.js +1 -47
  85. package/dist/commands/serve.js +1 -55
  86. package/dist/commands/setup-ai.js +2 -121
  87. package/dist/commands/setup.js +2 -305
  88. package/dist/commands/share.js +1 -214
  89. package/dist/commands/sms.js +32 -319
  90. package/dist/commands/stacks.js +1 -72
  91. package/dist/commands/telemetry.js +1 -74
  92. package/dist/commands/test.js +1 -130
  93. package/dist/commands/tinker.js +1 -37
  94. package/dist/commands/types.js +1 -18
  95. package/dist/commands/upgrade.js +1 -98
  96. package/dist/commands/user.js +3 -84
  97. package/dist/commands/version.js +1 -14
  98. package/dist/config.js +1 -231
  99. package/dist/custom-cli.js +1 -23
  100. package/dist/database-preflight.js +1 -88
  101. package/dist/global-options.js +1 -4
  102. package/dist/index.js +1 -1
  103. package/dist/lazy-commands.js +1 -241
  104. package/dist/migrators/index.js +2 -62
  105. package/dist/migrators/laravel/index.js +1 -148
  106. package/dist/migrators/laravel/migrations.js +4 -231
  107. package/dist/migrators/laravel/models.js +8 -111
  108. package/dist/migrators/rails/index.js +1 -11
  109. package/dist/production-server.js +1 -257
  110. package/dist/project-setup.js +1 -36
  111. package/dist/serve-entry.js +1 -5
  112. package/dist/version-info.js +1 -5
  113. package/package.json +54 -47
@@ -1,232 +1,5 @@
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?.[1] || createMatch[2] === void 0)
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?.[1] || methodMatch[2] === void 0 || methodMatch[3] === void 0)
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
- if (!mod)
103
- continue;
104
- switch (mod) {
105
- case "nullable":
106
- column.nullable = !0;
107
- break;
108
- case "unique":
109
- column.unique = !0;
110
- break;
111
- case "index":
112
- column.index = !0;
113
- break;
114
- case "default":
115
- column.defaultValue = parseDefaultArg(args);
116
- break;
117
- case "primary":
118
- column.primaryKey = !0;
119
- break;
120
- case "autoIncrement":
121
- column.autoIncrement = !0;
122
- break;
123
- case "unsigned":
124
- case "comment":
125
- case "after":
126
- case "change":
127
- break;
128
- default:
129
- break;
130
- }
131
- }
132
- }
133
- function parseStringArg(args) {
134
- return args.match(/^['"]([^'"]+)['"]/)?.[1] ?? null;
135
- }
136
- function parseIndexArg(args) {
137
- const single = parseStringArg(args);
138
- if (single)
139
- return [single];
140
- const arr = args.match(/\[([^\]]*)\]/);
141
- if (!arr?.[1])
142
- return [];
143
- return arr[1].split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
144
- }
145
- function parseDefaultArg(args) {
146
- const stringMatch = args.match(/^['"]([^'"]*)['"]/);
147
- if (stringMatch?.[1] !== void 0)
148
- return `'${stringMatch[1].replace(/'/g, "''")}'`;
149
- const trimmed = args.trim();
150
- if (trimmed === "true")
151
- return "1";
152
- if (trimmed === "false")
153
- return "0";
154
- if (trimmed === "null")
155
- return "NULL";
156
- if (/^-?\d+(\.\d+)?$/.test(trimmed))
157
- return trimmed;
158
- return trimmed;
159
- }
160
- const TYPE_MAP = {
161
- string: "TEXT",
162
- text: "TEXT",
163
- mediumText: "TEXT",
164
- longText: "TEXT",
165
- char: "TEXT",
166
- integer: "INTEGER",
167
- tinyInteger: "INTEGER",
168
- smallInteger: "INTEGER",
169
- mediumInteger: "INTEGER",
170
- bigInteger: "INTEGER",
171
- unsignedBigInteger: "INTEGER",
172
- unsignedInteger: "INTEGER",
173
- foreignId: "INTEGER",
174
- boolean: "INTEGER",
175
- date: "DATE",
176
- dateTime: "DATETIME",
177
- dateTimeTz: "DATETIME",
178
- timestamp: "DATETIME",
179
- timestampTz: "DATETIME",
180
- time: "TIME",
181
- timeTz: "TIME",
182
- year: "INTEGER",
183
- json: "TEXT",
184
- jsonb: "TEXT",
185
- uuid: "TEXT",
186
- ulid: "TEXT",
187
- ipAddress: "TEXT",
188
- macAddress: "TEXT",
189
- float: "REAL",
190
- double: "REAL",
191
- decimal: "REAL",
192
- binary: "BLOB",
193
- enum: "TEXT",
194
- set: "TEXT"
195
- };
196
- function mapType(laravelMethod) {
197
- return TYPE_MAP[laravelMethod] ?? null;
198
- }
199
- function emitCreateTable(table, columns, indexes) {
200
- const lines = [];
201
- lines.push(`CREATE TABLE IF NOT EXISTS ${table} (`);
202
- const colDefs = columns.map((c) => {
203
- const parts = [` ${c.name} ${c.type}`];
204
- if (c.primaryKey)
205
- parts.push("PRIMARY KEY");
206
- if (c.autoIncrement)
207
- parts.push("AUTOINCREMENT");
208
- if (!c.nullable && !c.primaryKey)
209
- parts.push("NOT NULL");
210
- if (c.defaultValue !== null)
211
- parts.push(`DEFAULT ${c.defaultValue}`);
212
- return parts.join(" ");
213
- });
214
- lines.push(colDefs.join(`,
215
- `));
216
- lines.push(");");
217
- for (const idx of indexes) {
218
- const cols = idx.columns.join(", "), indexName = idx.name || `${table}_${idx.columns.join("_")}_${idx.unique ? "unique" : "index"}`, verb = idx.unique ? "CREATE UNIQUE INDEX" : "CREATE INDEX";
219
- lines.push(`${verb} IF NOT EXISTS ${indexName} ON ${table}(${cols});`);
220
- }
221
- return `${lines.join(`
1
+ export function parseLaravelMigration(source){const createMatch=source.match(/Schema::create\(\s*['"]([a-z0-9_]+)['"]\s*,\s*function\s*\([^)]*\)\s*\{([\s\S]*?)\}\s*\)\s*;/i);if(!createMatch?.[1]||createMatch[2]===void 0)return null;const table=createMatch[1],body=createMatch[2],skipped=[],columns=[],indexes=[],lines=body.split(`
2
+ `).map((line)=>line.trim()).filter((line)=>line.length>0&&!line.startsWith("//")&&!line.startsWith("*")&&!line.startsWith("/*"));for(const line of lines){const trimmed=line.replace(/;\s*$/,"");if(!trimmed.startsWith("$table->")){skipped.push(line);continue}const parsed=parseColumnLine(trimmed);if(!parsed){skipped.push(line);continue}if(parsed.kind==="columns"){columns.push(...parsed.columns);if(parsed.indexes)indexes.push(...parsed.indexes)}else if(parsed.kind==="index")indexes.push(parsed.index);else if(parsed.kind==="skip")skipped.push(line)}const sql=emitCreateTable(table,columns,indexes);return{table,sql,skipped}}function parseColumnLine(line){const methodMatch=line.match(/^\$table->([a-zA-Z_]+)\s*\(([^)]*)\)(.*)$/);if(!methodMatch?.[1]||methodMatch[2]===void 0||methodMatch[3]===void 0)return null;const method=methodMatch[1],args=methodMatch[2].trim(),rest=methodMatch[3];if(method==="timestamps")return{kind:"columns",columns:[{name:"created_at",type:"DATETIME",nullable:!0,unique:!1,primaryKey:!1,autoIncrement:!1,defaultValue:"CURRENT_TIMESTAMP",index:!1},{name:"updated_at",type:"DATETIME",nullable:!0,unique:!1,primaryKey:!1,autoIncrement:!1,defaultValue:null,index:!1}]};if(method==="softDeletes")return{kind:"columns",columns:[{name:"deleted_at",type:"DATETIME",nullable:!0,unique:!1,primaryKey:!1,autoIncrement:!1,defaultValue:null,index:!1}]};if(method==="rememberToken")return{kind:"columns",columns:[{name:"remember_token",type:"TEXT",nullable:!0,unique:!1,primaryKey:!1,autoIncrement:!1,defaultValue:null,index:!1}]};if(method==="id"||method==="increments"||method==="bigIncrements")return{kind:"columns",columns:[{name:parseStringArg(args)||"id",type:"INTEGER",nullable:!1,unique:!1,primaryKey:!0,autoIncrement:!0,defaultValue:null,index:!1}]};if(method==="index"||method==="unique"){const cols=parseIndexArg(args);if(cols.length===0)return{kind:"skip"};return{kind:"index",index:{name:"",columns:cols,unique:method==="unique"}}}const columnName=parseStringArg(args);if(!columnName)return null;const sqlType=mapType(method);if(!sqlType)return{kind:"skip"};const column={name:columnName,type:sqlType,nullable:!1,unique:!1,primaryKey:!1,autoIncrement:!1,defaultValue:null,index:!1};applyModifiers(rest,column);const indexes=[];if(column.unique)indexes.push({name:"",columns:[columnName],unique:!0});if(column.index)indexes.push({name:"",columns:[columnName],unique:!1});return{kind:"columns",columns:[column],indexes}}function applyModifiers(rest,column){const modRegex=/->([a-zA-Z_]+)\s*\(([^)]*)\)/g;let match;while((match=modRegex.exec(rest))!==null){const mod=match[1],args=match[2]?.trim()??"";if(!mod)continue;switch(mod){case"nullable":column.nullable=!0;break;case"unique":column.unique=!0;break;case"index":column.index=!0;break;case"default":column.defaultValue=parseDefaultArg(args);break;case"primary":column.primaryKey=!0;break;case"autoIncrement":column.autoIncrement=!0;break;case"unsigned":case"comment":case"after":case"change":break;default:break}}}function parseStringArg(args){return args.match(/^['"]([^'"]+)['"]/)?.[1]??null}function parseIndexArg(args){const single=parseStringArg(args);if(single)return[single];const arr=args.match(/\[([^\]]*)\]/);if(!arr?.[1])return[];return arr[1].split(",").map((s)=>s.trim().replace(/^['"]|['"]$/g,"")).filter(Boolean)}function parseDefaultArg(args){const stringMatch=args.match(/^['"]([^'"]*)['"]/);if(stringMatch?.[1]!==void 0)return`'${stringMatch[1].replace(/'/g,"''")}'`;const trimmed=args.trim();if(trimmed==="true")return"1";if(trimmed==="false")return"0";if(trimmed==="null")return"NULL";if(/^-?\d+(\.\d+)?$/.test(trimmed))return trimmed;return trimmed}const TYPE_MAP={string:"TEXT",text:"TEXT",mediumText:"TEXT",longText:"TEXT",char:"TEXT",integer:"INTEGER",tinyInteger:"INTEGER",smallInteger:"INTEGER",mediumInteger:"INTEGER",bigInteger:"INTEGER",unsignedBigInteger:"INTEGER",unsignedInteger:"INTEGER",foreignId:"INTEGER",boolean:"INTEGER",date:"DATE",dateTime:"DATETIME",dateTimeTz:"DATETIME",timestamp:"DATETIME",timestampTz:"DATETIME",time:"TIME",timeTz:"TIME",year:"INTEGER",json:"TEXT",jsonb:"TEXT",uuid:"TEXT",ulid:"TEXT",ipAddress:"TEXT",macAddress:"TEXT",float:"REAL",double:"REAL",decimal:"REAL",binary:"BLOB",enum:"TEXT",set:"TEXT"};function mapType(laravelMethod){return TYPE_MAP[laravelMethod]??null}function emitCreateTable(table,columns,indexes){const lines=[];lines.push(`CREATE TABLE IF NOT EXISTS ${table} (`);const colDefs=columns.map((c)=>{const parts=[` ${c.name} ${c.type}`];if(c.primaryKey)parts.push("PRIMARY KEY");if(c.autoIncrement)parts.push("AUTOINCREMENT");if(!c.nullable&&!c.primaryKey)parts.push("NOT NULL");if(c.defaultValue!==null)parts.push(`DEFAULT ${c.defaultValue}`);return parts.join(" ")});lines.push(colDefs.join(`,
3
+ `));lines.push(");");for(const idx of indexes){const cols=idx.columns.join(", "),indexName=idx.name||`${table}_${idx.columns.join("_")}_${idx.unique?"unique":"index"}`,verb=idx.unique?"CREATE UNIQUE INDEX":"CREATE INDEX";lines.push(`${verb} IF NOT EXISTS ${indexName} ON ${table}(${cols});`)}return`${lines.join(`
222
4
  `)}
223
- `;
224
- }
225
- export function laravelFilenameToStacks(filename, sequence, table) {
226
- const padded = String(sequence).padStart(10, "0"), base = filename.toLowerCase();
227
- if (base.includes("create_") && base.includes("_table"))
228
- return `${padded}-create-${table}-table.sql`;
229
- if (base.includes("add_") || base.includes("alter_"))
230
- return `${padded}-alter-${table}-migration.sql`;
231
- return `${padded}-create-${table}-table.sql`;
232
- }
5
+ `}export function laravelFilenameToStacks(filename,sequence,table){const padded=String(sequence).padStart(10,"0"),base=filename.toLowerCase();if(base.includes("create_")&&base.includes("_table"))return`${padded}-create-${table}-table.sql`;if(base.includes("add_")||base.includes("alter_"))return`${padded}-alter-${table}-migration.sql`;return`${padded}-create-${table}-table.sql`}
@@ -1,65 +1,13 @@
1
- const RELATION_KINDS = ["belongsTo", "hasMany", "hasOne", "belongsToMany"];
2
- export function parseLaravelModel(source) {
3
- const classMatch = source.match(/\bclass\s+([A-Z][A-Za-z0-9_]*)\b/);
4
- if (!classMatch?.[1])
5
- return null;
6
- const className = classMatch[1], table = extractStringProperty(source, "table") ?? snakeCasePlural(className), fillable = extractArrayProperty(source, "fillable"), hidden = extractArrayProperty(source, "hidden"), casts = extractKeyedArrayProperty(source, "casts"), relationships = extractRelationships(source), notes = [];
7
- if (/protected\s+\$appends\s*=/.test(source))
8
- notes.push("$appends accessors require a custom Stacks computed-attribute setup");
9
- if (/protected\s+\$dates\s*=/.test(source))
10
- notes.push("$dates is implicit in Stacks \u2014 datetime columns are already typed via validation rules");
11
- if (/\buse\b[^;]*\bSoftDeletes\b/.test(source))
12
- notes.push("SoftDeletes trait \u2192 enable `useDeletedAt` (or matching trait) in the Stacks model traits block");
13
- if (/\buse\b[^;]*\bHasFactory\b/.test(source))
14
- notes.push("HasFactory \u2192 Stacks generates factories automatically when `useSeeder` is enabled");
15
- const tsSource = emitStacksModel({ className, table, fillable, hidden, casts, relationships });
16
- return { className, table, fillable, hidden, casts, relationships, tsSource, notes };
17
- }
18
- function extractStringProperty(source, name) {
19
- const re = new RegExp(`protected\\s+\\$${name}\\s*=\\s*['"]([^'"]+)['"]`);
20
- return source.match(re)?.[1] ?? null;
21
- }
22
- function extractArrayProperty(source, name) {
23
- const re = new RegExp(`protected\\s+\\$${name}\\s*=\\s*\\[([^\\]]*)\\]`, "s"), m = source.match(re);
24
- if (!m?.[1])
25
- return [];
26
- return m[1].split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
27
- }
28
- function extractKeyedArrayProperty(source, name) {
29
- const re = new RegExp(`protected\\s+\\$${name}\\s*=\\s*\\[([^\\]]*)\\]`, "s"), m = source.match(re);
30
- if (!m?.[1])
31
- return {};
32
- const out = {}, pairs = m[1].split(",");
33
- for (const pair of pairs) {
34
- const kv = pair.match(/['"]([^'"]+)['"]\s*=>\s*['"]([^'"]+)['"]/);
35
- if (kv?.[1] && kv[2] !== void 0)
36
- out[kv[1]] = kv[2];
37
- }
38
- return out;
39
- }
40
- function extractRelationships(source) {
41
- const found = [], methodRegex = /public\s+function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\([^)]*\)\s*(?::\s*[^\{]+)?\{[\s\S]*?return\s+\$this\s*->\s*(belongsTo|hasMany|hasOne|belongsToMany)\s*\(\s*([A-Z][A-Za-z0-9_]*)::class/g;
42
- let m;
43
- while ((m = methodRegex.exec(source)) !== null) {
44
- const name = m[1], kind = m[2], target = m[3];
45
- if (!name || !kind || !target)
46
- continue;
47
- found.push({ name, kind, target });
48
- }
49
- return found;
50
- }
51
- function emitStacksModel(args) {
52
- const attributeNames = unique([...args.fillable, ...Object.keys(args.casts)]), attrBlock = attributeNames.length === 0 ? " attributes: {}," : ` attributes: {
53
- ${attributeNames.map((n) => emitAttribute(n, args)).join(`
1
+ const RELATION_KINDS=["belongsTo","hasMany","hasOne","belongsToMany"];export function parseLaravelModel(source){const classMatch=source.match(/\bclass\s+([A-Z][A-Za-z0-9_]*)\b/);if(!classMatch?.[1])return null;const className=classMatch[1],table=extractStringProperty(source,"table")??snakeCasePlural(className),fillable=extractArrayProperty(source,"fillable"),hidden=extractArrayProperty(source,"hidden"),casts=extractKeyedArrayProperty(source,"casts"),relationships=extractRelationships(source),notes=[];if(/protected\s+\$appends\s*=/.test(source))notes.push("$appends accessors require a custom Stacks computed-attribute setup");if(/protected\s+\$dates\s*=/.test(source))notes.push("$dates is implicit in Stacks \u2014 datetime columns are already typed via validation rules");if(/\buse\b[^;]*\bSoftDeletes\b/.test(source))notes.push("SoftDeletes trait \u2192 enable `useDeletedAt` (or matching trait) in the Stacks model traits block");if(/\buse\b[^;]*\bHasFactory\b/.test(source))notes.push("HasFactory \u2192 Stacks generates factories automatically when `useSeeder` is enabled");const tsSource=emitStacksModel({className,table,fillable,hidden,casts,relationships});return{className,table,fillable,hidden,casts,relationships,tsSource,notes}}function extractStringProperty(source,name){const re=new RegExp(`protected\\s+\\$${name}\\s*=\\s*['"]([^'"]+)['"]`);return source.match(re)?.[1]??null}function extractArrayProperty(source,name){const re=new RegExp(`protected\\s+\\$${name}\\s*=\\s*\\[([^\\]]*)\\]`,"s"),m=source.match(re);if(!m?.[1])return[];return m[1].split(",").map((s)=>s.trim().replace(/^['"]|['"]$/g,"")).filter(Boolean)}function extractKeyedArrayProperty(source,name){const re=new RegExp(`protected\\s+\\$${name}\\s*=\\s*\\[([^\\]]*)\\]`,"s"),m=source.match(re);if(!m?.[1])return{};const out={},pairs=m[1].split(",");for(const pair of pairs){const kv=pair.match(/['"]([^'"]+)['"]\s*=>\s*['"]([^'"]+)['"]/);if(kv?.[1]&&kv[2]!==void 0)out[kv[1]]=kv[2]}return out}function extractRelationships(source){const found=[],methodRegex=/public\s+function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\([^)]*\)\s*(?::\s*[^\{]+)?\{[\s\S]*?return\s+\$this\s*->\s*(belongsTo|hasMany|hasOne|belongsToMany)\s*\(\s*([A-Z][A-Za-z0-9_]*)::class/g;let m;while((m=methodRegex.exec(source))!==null){const name=m[1],kind=m[2],target=m[3];if(!name||!kind||!target)continue;found.push({name,kind,target})}return found}function emitStacksModel(args){const attributeNames=unique([...args.fillable,...Object.keys(args.casts)]),attrBlock=attributeNames.length===0?" attributes: {},":` attributes: {
2
+ ${attributeNames.map((n)=>emitAttribute(n,args)).join(`
54
3
  `)}
55
- },`, relationsBlock = args.relationships.length === 0 ? "" : `
4
+ },`,relationsBlock=args.relationships.length===0?"":`
56
5
  // Relationships translated from Eloquent \u2014 refine targets / foreign keys to match your schema.
57
6
  relations: {
58
- ${args.relationships.map((r) => ` ${r.name}: { type: '${r.kind}', target: '${r.target}' },`).join(`
7
+ ${args.relationships.map((r)=>` ${r.name}: { type: '${r.kind}', target: '${r.target}' },`).join(`
59
8
  `)}
60
9
  },
61
- `;
62
- return `import { defineModel } from '@stacksjs/orm'
10
+ `;return`import { defineModel } from '@stacksjs/orm'
63
11
  import { schema } from '@stacksjs/validation'
64
12
 
65
13
  export default defineModel({
@@ -74,61 +22,10 @@ export default defineModel({
74
22
  ${relationsBlock}
75
23
  ${attrBlock}
76
24
  } as const)
77
- `;
78
- }
79
- function emitAttribute(name, args) {
80
- const rule = ruleForCast(args.casts[name]) ?? "schema.string()", fillable = args.fillable.includes(name), hidden = args.hidden.includes(name), notes = [];
81
- if (hidden)
82
- notes.push("hidden from API responses in source \u2014 handle via your Stacks resource/transformer");
83
- const noteLine = notes.length > 0 ? ` // ${notes.join("; ")}
84
- ` : "";
85
- return ` ${name}: {
25
+ `}function emitAttribute(name,args){const rule=ruleForCast(args.casts[name])??"schema.string()",fillable=args.fillable.includes(name),hidden=args.hidden.includes(name),notes=[];if(hidden)notes.push("hidden from API responses in source \u2014 handle via your Stacks resource/transformer");const noteLine=notes.length>0?` // ${notes.join("; ")}
26
+ `:"";return` ${name}: {
86
27
  fillable: ${fillable},
87
28
  ${noteLine} validation: {
88
29
  rule: ${rule},
89
30
  },
90
- },`;
91
- }
92
- function ruleForCast(cast) {
93
- if (!cast)
94
- return null;
95
- switch (cast.split(":")[0]) {
96
- case "integer":
97
- case "int":
98
- return "schema.number().int()";
99
- case "float":
100
- case "double":
101
- case "real":
102
- return "schema.number()";
103
- case "boolean":
104
- case "bool":
105
- return "schema.boolean()";
106
- case "string":
107
- return "schema.string()";
108
- case "array":
109
- case "collection":
110
- case "json":
111
- case "object":
112
- return "schema.unknown()";
113
- case "date":
114
- case "datetime":
115
- case "immutable_date":
116
- case "immutable_datetime":
117
- return "schema.string()";
118
- case "decimal":
119
- return "schema.number()";
120
- default:
121
- return null;
122
- }
123
- }
124
- function snakeCasePlural(className) {
125
- const snake = className.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
126
- if (snake.endsWith("y") && !/[aeiou]y$/.test(snake))
127
- return `${snake.slice(0, -1)}ies`;
128
- if (snake.endsWith("s") || snake.endsWith("x") || snake.endsWith("z") || snake.endsWith("ch") || snake.endsWith("sh"))
129
- return `${snake}es`;
130
- return `${snake}s`;
131
- }
132
- function unique(arr) {
133
- return [...new Set(arr)];
134
- }
31
+ },`}function ruleForCast(cast){if(!cast)return null;switch(cast.split(":")[0]){case"integer":case"int":return"schema.number().int()";case"float":case"double":case"real":return"schema.number()";case"boolean":case"bool":return"schema.boolean()";case"string":return"schema.string()";case"array":case"collection":case"json":case"object":return"schema.unknown()";case"date":case"datetime":case"immutable_date":case"immutable_datetime":return"schema.string()";case"decimal":return"schema.number()";default:return null}}function snakeCasePlural(className){const snake=className.replace(/([a-z0-9])([A-Z])/g,"$1_$2").toLowerCase();if(snake.endsWith("y")&&!/[aeiou]y$/.test(snake))return`${snake.slice(0,-1)}ies`;if(snake.endsWith("s")||snake.endsWith("x")||snake.endsWith("z")||snake.endsWith("ch")||snake.endsWith("sh"))return`${snake}es`;return`${snake}s`}function unique(arr){return[...new Set(arr)]}
@@ -1,11 +1 @@
1
- export const railsDriver = {
2
- name: "rails",
3
- async migrate(_req) {
4
- return [{
5
- source: ".",
6
- target: "",
7
- status: "skipped",
8
- note: "Rails driver is not yet implemented (stacksjs/stacks#1241 \u2014 Laravel driver shipped first). Track follow-up at github.com/stacksjs/stacks/issues/1241."
9
- }];
10
- }
11
- };
1
+ export const railsDriver={name:"rails",async migrate(_req){return[{source:".",target:"",status:"skipped",note:"Rails driver is not yet implemented (stacksjs/stacks#1241 \u2014 Laravel driver shipped first). Track follow-up at github.com/stacksjs/stacks/issues/1241."}]}};