@stacksjs/buddy 0.70.258 → 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 (112) 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.js +13 -1676
  18. package/dist/commands/desktop-apple.js +12 -399
  19. package/dist/commands/dev.js +6 -1083
  20. package/dist/commands/dns.js +3 -103
  21. package/dist/commands/docs/buddy-commands.js +10 -132
  22. package/dist/commands/docs/buddy-commands.test.js +1 -38
  23. package/dist/commands/docs/framework-repo.js +1 -14
  24. package/dist/commands/docs/framework-repo.test.js +1 -68
  25. package/dist/commands/docs/generated-artifacts.js +3 -70
  26. package/dist/commands/docs/generated-artifacts.test.js +1 -19
  27. package/dist/commands/docs/links.js +2 -85
  28. package/dist/commands/docs/links.test.js +4 -66
  29. package/dist/commands/docs.js +1 -24
  30. package/dist/commands/doctor.js +2 -478
  31. package/dist/commands/domains.js +2 -152
  32. package/dist/commands/email.js +31 -503
  33. package/dist/commands/env.js +1 -312
  34. package/dist/commands/extension.js +1 -239
  35. package/dist/commands/features.js +9 -345
  36. package/dist/commands/fresh.js +1 -46
  37. package/dist/commands/generate.js +2 -152
  38. package/dist/commands/http.js +1 -33
  39. package/dist/commands/index.js +1 -57
  40. package/dist/commands/install.js +1 -22
  41. package/dist/commands/key.js +1 -23
  42. package/dist/commands/link.js +2 -147
  43. package/dist/commands/lint.js +2 -84
  44. package/dist/commands/list.js +1 -140
  45. package/dist/commands/mail.js +15 -1477
  46. package/dist/commands/maintenance.js +1 -141
  47. package/dist/commands/make.js +1 -392
  48. package/dist/commands/migrate-project.js +1 -49
  49. package/dist/commands/migrate.js +27 -625
  50. package/dist/commands/outdated.js +1 -19
  51. package/dist/commands/package.js +1 -17
  52. package/dist/commands/phone.js +17 -181
  53. package/dist/commands/ports.js +2 -118
  54. package/dist/commands/prepublish.js +1 -17
  55. package/dist/commands/projects.js +1 -29
  56. package/dist/commands/protocol/craft-evidence.js +4 -98
  57. package/dist/commands/protocol/craft-evidence.test.js +1 -20
  58. package/dist/commands/protocol/desktop-lifecycle-report.js +3 -57
  59. package/dist/commands/protocol/desktop-lifecycle-report.test.js +1 -32
  60. package/dist/commands/protocol/desktop-support.js +2 -32
  61. package/dist/commands/protocol/desktop-workflow.test.js +3 -35
  62. package/dist/commands/protocol/driver-registry.js +2 -71
  63. package/dist/commands/protocol/pantry-evidence.js +4 -144
  64. package/dist/commands/protocol/pantry-evidence.test.js +1 -29
  65. package/dist/commands/protocol/release-manifest.js +3 -102
  66. package/dist/commands/protocol/release-manifest.test.js +2 -34
  67. package/dist/commands/protocol/run-conformance.js +4 -346
  68. package/dist/commands/protocol/run-conformance.test.js +1 -52
  69. package/dist/commands/protocol/run-driver-contracts.js +4 -83
  70. package/dist/commands/protocol/run-driver-contracts.test.js +1 -18
  71. package/dist/commands/protocol/source-manifest.js +3 -113
  72. package/dist/commands/protocol/sync-suite.js +3 -136
  73. package/dist/commands/protocol/sync-suite.test.js +1 -44
  74. package/dist/commands/protocol.js +1 -60
  75. package/dist/commands/publish.js +8 -500
  76. package/dist/commands/queue.js +1 -248
  77. package/dist/commands/release.js +1 -42
  78. package/dist/commands/route.js +1 -21
  79. package/dist/commands/run-tool.js +1 -9
  80. package/dist/commands/saas.js +1 -25
  81. package/dist/commands/schedule.js +1 -61
  82. package/dist/commands/search.js +1 -84
  83. package/dist/commands/seed.js +1 -47
  84. package/dist/commands/serve.js +1 -55
  85. package/dist/commands/setup-ai.js +2 -121
  86. package/dist/commands/setup.js +2 -305
  87. package/dist/commands/share.js +1 -214
  88. package/dist/commands/sms.js +32 -319
  89. package/dist/commands/stacks.js +1 -72
  90. package/dist/commands/telemetry.js +1 -74
  91. package/dist/commands/test.js +1 -130
  92. package/dist/commands/tinker.js +1 -37
  93. package/dist/commands/types.js +1 -18
  94. package/dist/commands/upgrade.js +1 -98
  95. package/dist/commands/user.js +3 -84
  96. package/dist/commands/version.js +1 -14
  97. package/dist/config.js +1 -231
  98. package/dist/custom-cli.js +1 -23
  99. package/dist/database-preflight.js +1 -88
  100. package/dist/global-options.js +1 -4
  101. package/dist/index.js +1 -1
  102. package/dist/lazy-commands.js +1 -241
  103. package/dist/migrators/index.js +2 -62
  104. package/dist/migrators/laravel/index.js +1 -148
  105. package/dist/migrators/laravel/migrations.js +4 -231
  106. package/dist/migrators/laravel/models.js +8 -111
  107. package/dist/migrators/rails/index.js +1 -11
  108. package/dist/production-server.js +1 -257
  109. package/dist/project-setup.js +1 -36
  110. package/dist/serve-entry.js +1 -5
  111. package/dist/version-info.js +1 -5
  112. package/package.json +53 -46
@@ -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."}]}};