@stacksjs/buddy 0.70.73 → 0.70.74

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.
package/dist/cli.d.ts CHANGED
@@ -1 +1 @@
1
-
1
+ export {};
@@ -7,7 +7,7 @@ export function completion(buddy) {
7
7
  };
8
8
  buddy.command("completion [shell]", descriptions.completion).option("-s, --shell [shell]", descriptions.shell).example("buddy completion bash").example("buddy completion zsh").example("buddy completion fish").example("buddy completion bash > /usr/local/etc/bash_completion.d/buddy").action(async (shell, options) => {
9
9
  log.debug("Running `buddy completion` ...", options);
10
- const targetShell = shell || options.shell || "bash", commandNames = (buddy.commands || []).map((cmd) => cmd.name).filter(Boolean);
10
+ const targetShell = shell || options.shell || "bash", commandNames = (buddy.commands || []).map((cmd) => cmd.name).filter((name) => Boolean(name));
11
11
  switch (targetShell) {
12
12
  case "bash":
13
13
  console.log(generateBashCompletion(commandNames));
@@ -85,7 +85,7 @@ export declare function deploy(buddy: CLI): void;
85
85
  * success: (...args: any[]) => unknown,
86
86
  * warn: (...args: any[]) => unknown,
87
87
  * error: (...args: any[]) => unknown,
88
- * debug: (...args: any[]) => unknown
88
+ * debug: (...args: any[]) => void
89
89
  * }
90
90
  * ```
91
91
  */
@@ -94,7 +94,7 @@ declare const log: {
94
94
  success: (...args: any[]) => unknown;
95
95
  warn: (...args: any[]) => unknown;
96
96
  error: (...args: any[]) => unknown;
97
- debug: (...args: any[]) => unknown
97
+ debug: (...args: any[]) => void
98
98
  };
99
99
  /** What a mail-tenant reconcile resolved + provisioned, for the DNS step. */
100
100
  export declare interface MailTenantResult {
@@ -107,18 +107,20 @@ function loadAwsCredentialsFromFile() {
107
107
  const profileCredentials = {};
108
108
  for (const line of lines) {
109
109
  const trimmed = line.trim(), profileMatch = trimmed.match(/^\[(.+)\]$/);
110
- if (profileMatch) {
110
+ if (profileMatch?.[1]) {
111
111
  currentProfile = profileMatch[1];
112
112
  profileCredentials[currentProfile] = {};
113
113
  continue;
114
114
  }
115
115
  const keyValue = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
116
116
  if (keyValue && currentProfile) {
117
- const [, key, value] = keyValue;
117
+ const [, key, value] = keyValue, target = profileCredentials[currentProfile];
118
+ if (!target || value === void 0)
119
+ continue;
118
120
  if (key === "aws_access_key_id")
119
- profileCredentials[currentProfile].accessKeyId = value;
121
+ target.accessKeyId = value;
120
122
  else if (key === "aws_secret_access_key")
121
- profileCredentials[currentProfile].secretAccessKey = value;
123
+ target.secretAccessKey = value;
122
124
  }
123
125
  }
124
126
  for (const profile of profiles)
@@ -138,7 +140,7 @@ function loadAwsCredentialsFromFile() {
138
140
  let region;
139
141
  if (existsSync(configPath)) {
140
142
  const regionMatch = readFileSync(configPath, "utf-8").match(/region\s*=\s*(.+)/);
141
- if (regionMatch)
143
+ if (regionMatch?.[1])
142
144
  region = regionMatch[1].trim();
143
145
  }
144
146
  return { ...credentials, region };
@@ -587,7 +589,7 @@ async function ghCliAvailable() {
587
589
  async function resolveSiteGithubSource(root) {
588
590
  try {
589
591
  const { execSync } = await import("node:child_process"), run = (cmd) => execSync(cmd, { cwd: root, stdio: ["ignore", "pipe", "ignore"] }).toString().trim(), match = run("git config --get remote.origin.url").match(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
590
- if (!match)
592
+ if (!match?.[1])
591
593
  return null;
592
594
  return { repo: match[1], ref: run("git rev-parse HEAD") };
593
595
  } catch {
@@ -824,7 +826,7 @@ function resolveMailboxes(mailboxes, domain) {
824
826
  }
825
827
  if (!raw || typeof raw !== "string")
826
828
  continue;
827
- const localPart = (raw.includes("@") ? raw.split("@")[0] : raw).trim();
829
+ const localPart = (raw.includes("@") ? raw.split("@")[0] ?? "" : raw).trim();
828
830
  if (!localPart)
829
831
  continue;
830
832
  const address = `${localPart}@${domain}`, envPw = explicitPw || process.env[`MAIL_PASSWORD_${localPart.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`];
@@ -951,7 +953,7 @@ if [ "$ENV_CHANGED" = 1 ]; then systemctl restart mail 2>/dev/null || true; echo
951
953
  input: script,
952
954
  encoding: "utf8",
953
955
  stdio: ["pipe", "pipe", "pipe"]
954
- }), line = (out.match(/MAILTENANT:[^\n]*/) || [])[0] || "MAILTENANT:done", mailHost = (out.match(/MAILHOST:([^\n]*)/) || [])[1]?.trim() || `mail.${domain}`, dkimPubB64 = (out.match(/DKIMPUB:([^\n]*)/) || [])[1]?.trim() || void 0, madeAddrs = new Set([...out.matchAll(/MADE:([^\n]+)/g)].map((m) => m[1].trim())), created = boxes.filter((b) => madeAddrs.has(b.address)).map((b) => ({ address: b.address, password: b.password }));
956
+ }), line = (out.match(/MAILTENANT:[^\n]*/) || [])[0] || "MAILTENANT:done", mailHost = (out.match(/MAILHOST:([^\n]*)/) || [])[1]?.trim() || `mail.${domain}`, dkimPubB64 = (out.match(/DKIMPUB:([^\n]*)/) || [])[1]?.trim() || void 0, madeAddrs = new Set([...out.matchAll(/MADE:([^\n]+)/g)].flatMap((m) => m[1] ? [m[1].trim()] : [])), created = boxes.filter((b) => madeAddrs.has(b.address)).map((b) => ({ address: b.address, password: b.password }));
955
957
  logger.success(`Mail routing reconciled (${line.replace("MAILTENANT:", "")})`);
956
958
  if (created.length) {
957
959
  logger.info(`Mail: created ${created.length} mailbox(es) \u2014 credentials below (save them; shown once):`);
@@ -1135,7 +1137,7 @@ export function deploy(buddy) {
1135
1137
  const prodEnvPath = p.projectPath(".env.production");
1136
1138
  if (existsSync(prodEnvPath)) {
1137
1139
  const urlMatch = readFileSync(prodEnvPath, "utf-8").match(/^APP_URL=(.+)$/m);
1138
- if (urlMatch) {
1140
+ if (urlMatch?.[1]) {
1139
1141
  productionUrl = urlMatch[1].trim();
1140
1142
  log.debug("Using APP_URL from .env.production:", productionUrl);
1141
1143
  }
@@ -1390,6 +1392,7 @@ async function checkIfAwsIsBootstrapped(options) {
1390
1392
  return !0;
1391
1393
  }
1392
1394
  } catch (error) {
1395
+ const caught = error && typeof error === "object" ? error : { message: String(error) };
1393
1396
  log.debug(`Stack not found: ${getErrorMessage(error)}`);
1394
1397
  }
1395
1398
  if (!stackExists)
@@ -242,7 +242,7 @@ export async function startDevelopmentServer(_options, _startTime) {
242
242
  if (readyAnnounced)
243
243
  return;
244
244
  readyAnnounced = !0;
245
- const failed = results.map((ok, i) => ok ? null : ports[i].name).filter((x) => x !== null);
245
+ const failed = results.map((ok, i) => ok ? null : ports[i]?.name ?? null).filter((x) => x !== null);
246
246
  let proxyReachable = !1;
247
247
  if (hasCustomDomain && domain) {
248
248
  (async () => {
@@ -443,19 +443,15 @@ async function importCraftSdk() {
443
443
  const localCraftSdk = process.env.HOME ? `${process.env.HOME}/Code/Tools/craft/packages/typescript/src/index.ts` : void 0;
444
444
  if (localCraftSdk && existsSync(localCraftSdk))
445
445
  return await import(localCraftSdk);
446
- try {
447
- return await import("craft-native");
448
- } catch (primaryError) {
446
+ const packageNames = ["craft-native", "@craft-native/craft", "@stacksjs/ts-craft"];
447
+ let primaryError;
448
+ for (const packageName of packageNames)
449
449
  try {
450
- return await import("@craft-native/craft");
451
- } catch {
452
- try {
453
- return await import("@stacksjs/ts-craft");
454
- } catch {
455
- throw primaryError;
456
- }
450
+ return await import(packageName);
451
+ } catch (error) {
452
+ primaryError ??= error;
457
453
  }
458
- }
454
+ throw primaryError;
459
455
  }
460
456
  async function cleanupStaleDevProcesses(ports) {
461
457
  const projectRoot = projectPath(), actionDevPath = projectPath("storage/framework/core/actions/src/dev/"), pids = new Set;
@@ -121,7 +121,10 @@ export function doctor(buddy) {
121
121
  const skipped = result.skippedTables.length > 0 ? `, ${result.skippedTables.length} tables skipped (not migrated)` : "";
122
122
  if (result.missing.length === 0)
123
123
  return `${result.declared.length} declared unique constraints all indexed${skipped}`;
124
- const sample = result.missing.slice(0, 5).map((u) => `${u.table}.${u.columns.join("+")}`).join(", "), more = result.missing.length > 5 ? ` (+${result.missing.length - 5} more)` : "", first = result.missing[0], example = `CREATE UNIQUE INDEX IF NOT EXISTS "${first.table}_${first.columns.join("_")}_unique" ON "${first.table}" ("${first.columns.join('", "')}")`;
124
+ const sample = result.missing.slice(0, 5).map((u) => `${u.table}.${u.columns.join("+")}`).join(", "), more = result.missing.length > 5 ? ` (+${result.missing.length - 5} more)` : "", first = result.missing[0];
125
+ if (!first)
126
+ throw Error("Unique-index audit reported missing entries without details");
127
+ const example = `CREATE UNIQUE INDEX IF NOT EXISTS "${first.table}_${first.columns.join("_")}_unique" ON "${first.table}" ("${first.columns.join('", "')}")`;
125
128
  throw Error(`${result.missing.length}/${result.declared.length} declared unique constraints have no UNIQUE index: ${sample}${more}. Run \`buddy migrate\` (re-queues missing unique-index migrations, #1952) \u2014 dedupe duplicate rows first or migrate hard-fails; if no migration file exists run \`buddy generate:migrations\`, or create manually: ${example}`);
126
129
  }, 1e4);
127
130
  await probe(checks, "FK orphans", async () => {
@@ -131,6 +134,8 @@ export function doctor(buddy) {
131
134
  if (result.total === 0)
132
135
  return "No orphan rows (PRAGMA foreign_key_check clean)";
133
136
  const sample = result.orphans.slice(0, 5).map((o) => `${o.table}.${o.column} \u2192 ${o.parent} (${o.count})`).join(", "), more = result.orphans.length > 5 ? ` (+${result.orphans.length - 5} more)` : "", first = result.orphans[0];
137
+ if (!first)
138
+ throw Error("Foreign-key audit reported orphan rows without details");
134
139
  throw Error(`${result.total} orphan rows violate FKs: ${sample}${more}. Legacy rows written under foreign_keys=OFF (#1951). Review and clean manually, e.g. DELETE FROM ${first.table} WHERE ${first.column} IS NOT NULL AND ${first.column} NOT IN (SELECT id FROM ${first.parent}) \u2014 doctor never deletes data.`);
135
140
  }, 1e4);
136
141
  await probe(checks, "Cache", async () => {
@@ -505,7 +505,7 @@ function parseRawEmailHeaders(rawEmail) {
505
505
  const match = line.match(/^([^:]+):\s*(.*)$/);
506
506
  if (match) {
507
507
  currentKey = match[1].toLowerCase();
508
- currentValue = match[2];
508
+ currentValue = match[2] ?? "";
509
509
  headers[currentKey] = currentValue;
510
510
  }
511
511
  }
@@ -515,12 +515,10 @@ function parseRawEmailHeaders(rawEmail) {
515
515
  function extractEmailAddress(str) {
516
516
  if (!str)
517
517
  return "";
518
- const match = str.match(/<([^>]+)>/);
519
- return (match ? match[1] : str).toLowerCase().trim();
518
+ return (str.match(/<([^>]+)>/)?.[1] ?? str).toLowerCase().trim();
520
519
  }
521
520
  function extractEmailName(str) {
522
521
  if (!str)
523
522
  return "";
524
- const match = str.match(/^"?([^"<]+)"?\s*</);
525
- return match ? match[1].trim() : "";
523
+ return str.match(/^"?([^"<]+)"?\s*</)?.[1]?.trim() ?? "";
526
524
  }
@@ -119,16 +119,20 @@ export declare const FEATURE_NAMES: readonly ['dashboard', 'commerce', 'cms', 'm
119
119
  * manifest update — directory entries are recursive. Only add an entry
120
120
  * when a feature introduces a new top-level path the framework didn't
121
121
  * already claim.
122
+ * @defaultValue
123
+ * ```ts
124
+ * {
125
+ * cms: [ 'app/Actions/Cms/', 'app/Actions/Dashboard/Content/', 'app/Models/Content/', 'app/Models/Tag.ts', 'app/Models/Comment.ts', 'resources/views/dashboard/content/', ],
126
+ * commerce: [ 'app/Actions/Commerce/', 'app/Actions/Dashboard/Commerce/', 'app/Models/commerce/', 'resources/components/Dashboard/Commerce/', 'resources/views/dashboard/commerce/', ],
127
+ * dashboard: [ 'app/Actions/Dashboard/', 'resources/components/Dashboard/', 'resources/views/dashboard/', 'routes/dashboard.ts', 'routes/dashboard-api.ts', ],
128
+ * marketing: [ 'app/Actions/Dashboard/Marketing/', 'app/Models/Campaign.ts', 'app/Models/CampaignSend.ts', 'app/Models/EmailList.ts', 'app/Models/EmailListSubscriber.ts', 'app/Models/SocialPost.ts', 'resources/components/Marketing/', 'resources/views/dashboard/marketing/', ],
129
+ * monitoring: [ 'app/Actions/Monitoring/', 'app/Actions/TestErrorAction.ts', 'app/Models/Error.ts', 'functions/monitoring/', 'resources/views/dashboard/monitoring/', 'resources/views/dashboard/errors/', ],
130
+ * realtime: [ 'app/Actions/Realtime/', 'app/Actions/Dashboard/Realtime/', 'app/Models/realtime/', 'app/Broadcasts/', 'functions/realtime/', 'resources/views/dashboard/realtime/', ],
131
+ * queue: [ 'app/Actions/Queue/', 'app/Actions/Dashboard/Jobs/', 'app/Jobs/', 'app/Models/Job.ts', 'app/Models/FailedJob.ts', 'functions/jobs.ts', 'resources/views/dashboard/queue/', 'resources/views/dashboard/jobs/', ]
132
+ * }
133
+ * ```
122
134
  */
123
- export declare const FEATURE_FILES: {
124
- cms: readonly ['app/Actions/Cms/', 'app/Actions/Dashboard/Content/', 'app/Models/Content/', 'app/Models/Tag.ts', 'app/Models/Comment.ts', 'resources/views/dashboard/content/'];
125
- commerce: readonly ['app/Actions/Commerce/', 'app/Actions/Dashboard/Commerce/', 'app/Models/commerce/', 'resources/components/Dashboard/Commerce/', 'resources/views/dashboard/commerce/'];
126
- dashboard: readonly ['app/Actions/Dashboard/', 'resources/components/Dashboard/', 'resources/views/dashboard/', 'routes/dashboard.ts', 'routes/dashboard-api.ts'];
127
- marketing: readonly ['app/Actions/Dashboard/Marketing/', 'app/Models/Campaign.ts', 'app/Models/CampaignSend.ts', 'app/Models/EmailList.ts', 'app/Models/EmailListSubscriber.ts', 'app/Models/SocialPost.ts', 'resources/components/Marketing/', 'resources/views/dashboard/marketing/'];
128
- monitoring: readonly ['app/Actions/Monitoring/', 'app/Actions/TestErrorAction.ts', 'app/Models/Error.ts', 'functions/monitoring/', 'resources/views/dashboard/monitoring/', 'resources/views/dashboard/errors/'];
129
- realtime: readonly ['app/Actions/Realtime/', 'app/Actions/Dashboard/Realtime/', 'app/Models/realtime/', 'app/Broadcasts/', 'functions/realtime/', 'resources/views/dashboard/realtime/'];
130
- queue: readonly ['app/Actions/Queue/', 'app/Actions/Dashboard/Jobs/', 'app/Jobs/', 'app/Models/Job.ts', 'app/Models/FailedJob.ts', 'functions/jobs.ts', 'resources/views/dashboard/queue/', 'resources/views/dashboard/jobs/']
131
- };
135
+ export declare const FEATURE_FILES: Record<FeatureName, readonly string[]>;
132
136
  /**
133
137
  * Per-feature database table ownership (stacksjs/stacks#1854).
134
138
  *
@@ -148,16 +152,20 @@ export declare const FEATURE_FILES: {
148
152
  * across features (none today, but `categories` could end up here)
149
153
  * should stay out of the manifest until that's resolved — the runner
150
154
  * defaults to "run unless owned by a disabled feature".
155
+ * @defaultValue
156
+ * ```ts
157
+ * {
158
+ * cms: ['posts', 'pages', 'comments', 'tags', 'authors', 'categories'],
159
+ * commerce: [ 'products', 'product_variants', 'product_units', 'manufacturers', 'orders', 'order_items', 'carts', 'cart_items', 'payments', 'payment_methods', 'payment_products', 'payment_transactions', 'customers', 'subscribers', 'subscriber_emails', 'subscriptions', 'gift_cards', 'coupons', 'transactions', 'reviews', 'drivers', 'delivery_routes', 'digital_deliveries', 'shipping_methods', 'shipping_rates', 'shipping_zones', 'license_keys', 'loyalty_points', 'loyalty_rewards', 'print_devices', 'receipts', 'tax_rates', 'waitlist_products', 'waitlist_restaurants', ],
160
+ * dashboard: [ 'boards', 'board_columns', 'cards', 'card_labels', 'card_assignees', 'card_comments', 'labels', 'ci_run_states', 'ci_runner_samples', 'ci_runner_alert_states', 'requests', 'logs', ],
161
+ * marketing: [],
162
+ * monitoring: ['errors'],
163
+ * realtime: ['websockets'],
164
+ * queue: ['jobs', 'failed_jobs']
165
+ * }
166
+ * ```
151
167
  */
152
- export declare const FEATURE_TABLES: {
153
- cms: readonly ['posts', 'pages', 'comments', 'tags', 'authors', 'categories'];
154
- commerce: readonly ['products', 'product_variants', 'product_units', 'manufacturers', 'orders', 'order_items', 'carts', 'cart_items', 'payments', 'payment_methods', 'payment_products', 'payment_transactions', 'customers', 'subscribers', 'subscriber_emails', 'subscriptions', 'gift_cards', 'coupons', 'transactions', 'reviews', 'drivers', 'delivery_routes', 'digital_deliveries', 'shipping_methods', 'shipping_rates', 'shipping_zones', 'license_keys', 'loyalty_points', 'loyalty_rewards', 'print_devices', 'receipts', 'tax_rates', 'waitlist_products', 'waitlist_restaurants'];
155
- dashboard: readonly ['boards', 'board_columns', 'cards', 'card_labels', 'card_assignees', 'card_comments', 'labels', 'ci_run_states', 'ci_runner_samples', 'ci_runner_alert_states', 'requests', 'logs'];
156
- marketing: never[];
157
- monitoring: readonly ['errors'];
158
- realtime: readonly ['websockets'];
159
- queue: readonly ['jobs', 'failed_jobs']
160
- };
168
+ export declare const FEATURE_TABLES: Record<FeatureName, readonly string[]>;
161
169
  export declare interface CopyFeatureFilesOptions {
162
170
  force?: boolean
163
171
  source?: string
@@ -131,13 +131,13 @@ export const FEATURE_NAMES = [
131
131
  export function migrationTable(filename) {
132
132
  const inMatch = filename.match(/-in-([a-z0-9_]+)\.sql$/i);
133
133
  if (inMatch)
134
- return inMatch[1];
134
+ return inMatch[1] ?? null;
135
135
  const createMatch = filename.match(/-create-([a-z0-9_]+)-table\.sql$/i);
136
136
  if (createMatch)
137
- return createMatch[1];
137
+ return createMatch[1] ?? null;
138
138
  const alterMatch = filename.match(/-alter-([a-z0-9_]+)-/i);
139
139
  if (alterMatch)
140
- return alterMatch[1];
140
+ return alterMatch[1] ?? null;
141
141
  return null;
142
142
  }
143
143
  export function migrationFeature(filename) {
@@ -11,7 +11,10 @@ export function http(buddy) {
11
11
  };
12
12
  buddy.command("http [domain]", descriptions.http).option("-p, --project [project]", descriptions.project, { default: !1 }).option("-v, --verbose", descriptions.verbose, { default: !1 }).action(async (domain, options) => {
13
13
  log.debug("Running `buddy http [domain]` ...", options);
14
- const url = domain || config.app.url, client = new HttxClient({ verbose: options.verbose });
14
+ const url = domain || config.app.url;
15
+ if (!url)
16
+ throw Error("No domain configured. Pass a domain or set config.app.url.");
17
+ const client = new HttxClient({ verbose: options.verbose });
15
18
  log.info(`GET ${url}`);
16
19
  (await client.request(url.startsWith("http") ? url : `https://${url}`, {
17
20
  method: "GET"
@@ -10,11 +10,10 @@ export function install(buddy) {
10
10
  };
11
11
  buddy.command("install", descriptions.install).option("-p, --project [project]", descriptions.project, { default: !1 }).option("-v, --verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
12
12
  log.debug("Running `buddy install` ...", options);
13
- const result = await runCommand("bun install", {
13
+ if ((await runCommand("bun install", {
14
14
  ...options,
15
15
  cwd: p.projectPath()
16
- });
17
- if (result && typeof result === "object" && "isErr" in result && result.isErr?.()) {
16
+ })).isErr) {
18
17
  log.error("bun install failed");
19
18
  process.exit(ExitCode.FatalError);
20
19
  }
@@ -55,7 +55,7 @@ export function list(buddy) {
55
55
  continue;
56
56
  let group = "General";
57
57
  if (name.includes(":"))
58
- group = name.split(":")[0];
58
+ group = name.split(":")[0] ?? "General";
59
59
  else if (["dev", "build", "test", "lint"].includes(name))
60
60
  group = "Development";
61
61
  else if (["deploy", "release", "publish"].includes(name))
@@ -59,7 +59,7 @@ export function make(buddy) {
59
59
  }
60
60
  setDryRun(Boolean(options.dryRun || options["dry-run"]));
61
61
  if (make) {
62
- options.name = buddy.args[1];
62
+ options.name = buddy.args[1] ?? make;
63
63
  switch (make) {
64
64
  case "action":
65
65
  await makeAction(options);
@@ -157,7 +157,10 @@ async function publishResource(ctx) {
157
157
  for (const m of matches)
158
158
  log.info(` ${m}`);
159
159
  }
160
- const sourcePath = matches[0], targetPath = `${userDir.replace(/\/$/, "")}/${fileName}`;
160
+ const sourcePath = matches[0];
161
+ if (!sourcePath)
162
+ throw Error(`Could not resolve default ${kind}: ${fileName}`);
163
+ const targetPath = `${userDir.replace(/\/$/, "")}/${fileName}`;
161
164
  if (existsSync(targetPath) && !force) {
162
165
  log.error(`Already exists: ${italic(targetPath)}`);
163
166
  log.info("Pass --force to overwrite.");
@@ -51,7 +51,8 @@ export function serve(buddy) {
51
51
  break;
52
52
  }
53
53
  } catch {}
54
- ({ serve: stxServe } = await import("bun-plugin-stx/serve"));
54
+ if (!stxServe)
55
+ ({ serve: stxServe } = await import("bun-plugin-stx/serve"));
55
56
  const stxModule = await resolveVendoredStxModule(), { site: siteConfig, i18n: i18nConfig } = await loadStxSiteConfig(), userViewsPath = "resources/views", defaultsResources = resolveDefaultsResources(), defaultViewsPath = join(defaultsResources, "views"), userLayoutsPath = existsSync("resources/views/layouts") ? "resources/views/layouts" : "resources/layouts", userComponentsPath = existsSync("resources/views/components") ? "resources/views/components" : "resources/components", apiBase = process.env.API_URL || `http://127.0.0.1:${Number(process.env.PORT_API) || config.ports?.api || 3008}`;
56
57
  log.info(`Starting production server on port ${port}...`);
57
58
  await stxServe({
@@ -119,12 +119,16 @@ export function share(buddy) {
119
119
  companion.runner({ verbose: options.verbose ?? !1 }).catch(() => {});
120
120
  const results = await Promise.allSettled(companions.map((c) => waitForPort(c.port, "localhost", 60000)));
121
121
  unmuteOutput();
122
- for (let i = 0;i < companions.length; i++)
123
- if (results[i].status === "fulfilled") {
124
- startedCompanions.push(companions[i]);
125
- s.succeed(`${bold(companions[i].label)} ready ${dim(`on :${companions[i].port}`)}`);
122
+ for (let i = 0;i < companions.length; i++) {
123
+ const result = results[i], companion = companions[i];
124
+ if (!result || !companion)
125
+ continue;
126
+ if (result.status === "fulfilled") {
127
+ startedCompanions.push(companion);
128
+ s.succeed(`${bold(companion.label)} ready ${dim(`on :${companion.port}`)}`);
126
129
  } else
127
- s.fail(`${companions[i].label} failed to start ${dim(`on :${companions[i].port}`)}`);
130
+ s.fail(`${companion.label} failed to start ${dim(`on :${companion.port}`)}`);
131
+ }
128
132
  }
129
133
  console.log();
130
134
  s.start("Creating tunnel...");
@@ -193,13 +197,14 @@ export function share(buddy) {
193
197
  } catch (error) {
194
198
  unmuteOutput();
195
199
  s.fail(getErrorMessage(error));
196
- if (error.message?.includes("timeout") || error.message?.includes("ECONNREFUSED")) {
200
+ const caught = error instanceof Error ? error : Error(String(error));
201
+ if (caught.message.includes("timeout") || caught.message.includes("ECONNREFUSED")) {
197
202
  log.error(`Could not reach tunnel server at ${server}`);
198
203
  log.info(`Verify with: curl -sk https://${server}/status`);
199
204
  } else
200
- log.error(`Failed to create tunnel: ${error.message}`);
205
+ log.error(`Failed to create tunnel: ${caught.message}`);
201
206
  if (options.verbose)
202
- log.error(error.stack);
207
+ log.error(caught.stack);
203
208
  for (const t of tunnels)
204
209
  t.close();
205
210
  await outro("Share failed", { startTime: perf, useSeconds: !0 });
package/dist/config.js CHANGED
@@ -153,7 +153,7 @@ export async function loadBuddyConfig() {
153
153
  throw Error("Configuration validation failed");
154
154
  }
155
155
  cachedConfig = config;
156
- return cachedConfig;
156
+ return config;
157
157
  } catch (error) {
158
158
  log.warn(`Failed to load buddy config from ${configPath}:`, error);
159
159
  }
@@ -1 +1 @@
1
-
1
+ export {};
@@ -6,7 +6,4 @@ export declare function runMigrator(req: MigrateProjectRequest): Promise<Migrati
6
6
  * writing to `MIGRATION_REPORT.md` in the target project.
7
7
  */
8
8
  export declare function renderReport(report: MigrationReport): string;
9
- export declare const DRIVERS: {
10
- laravel: unknown;
11
- rails: unknown
12
- };
9
+ export declare const DRIVERS: Record<string, Driver>;
@@ -1,6 +1,6 @@
1
1
  export function parseLaravelMigration(source) {
2
2
  const createMatch = source.match(/Schema::create\(\s*['"]([a-z0-9_]+)['"]\s*,\s*function\s*\([^)]*\)\s*\{([\s\S]*?)\}\s*\)\s*;/i);
3
- if (!createMatch)
3
+ if (!createMatch?.[1] || createMatch[2] === void 0)
4
4
  return null;
5
5
  const table = createMatch[1], body = createMatch[2], skipped = [], columns = [], indexes = [], lines = body.split(`
6
6
  `).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("//") && !line.startsWith("*") && !line.startsWith("/*"));
@@ -29,7 +29,7 @@ export function parseLaravelMigration(source) {
29
29
  }
30
30
  function parseColumnLine(line) {
31
31
  const methodMatch = line.match(/^\$table->([a-zA-Z_]+)\s*\(([^)]*)\)(.*)$/);
32
- if (!methodMatch)
32
+ if (!methodMatch?.[1] || methodMatch[2] === void 0 || methodMatch[3] === void 0)
33
33
  return null;
34
34
  const method = methodMatch[1], args = methodMatch[2].trim(), rest = methodMatch[3];
35
35
  if (method === "timestamps")
@@ -98,7 +98,9 @@ function applyModifiers(rest, column) {
98
98
  const modRegex = /->([a-zA-Z_]+)\s*\(([^)]*)\)/g;
99
99
  let match;
100
100
  while ((match = modRegex.exec(rest)) !== null) {
101
- const mod = match[1], args = match[2].trim();
101
+ const mod = match[1], args = match[2]?.trim() ?? "";
102
+ if (!mod)
103
+ continue;
102
104
  switch (mod) {
103
105
  case "nullable":
104
106
  column.nullable = !0;
@@ -129,21 +131,20 @@ function applyModifiers(rest, column) {
129
131
  }
130
132
  }
131
133
  function parseStringArg(args) {
132
- const m = args.match(/^['"]([^'"]+)['"]/);
133
- return m ? m[1] : null;
134
+ return args.match(/^['"]([^'"]+)['"]/)?.[1] ?? null;
134
135
  }
135
136
  function parseIndexArg(args) {
136
137
  const single = parseStringArg(args);
137
138
  if (single)
138
139
  return [single];
139
140
  const arr = args.match(/\[([^\]]*)\]/);
140
- if (!arr)
141
+ if (!arr?.[1])
141
142
  return [];
142
143
  return arr[1].split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
143
144
  }
144
145
  function parseDefaultArg(args) {
145
146
  const stringMatch = args.match(/^['"]([^'"]*)['"]/);
146
- if (stringMatch)
147
+ if (stringMatch?.[1] !== void 0)
147
148
  return `'${stringMatch[1].replace(/'/g, "''")}'`;
148
149
  const trimmed = args.trim();
149
150
  if (trimmed === "true")
@@ -1,7 +1,7 @@
1
1
  const RELATION_KINDS = ["belongsTo", "hasMany", "hasOne", "belongsToMany"];
2
2
  export function parseLaravelModel(source) {
3
3
  const classMatch = source.match(/\bclass\s+([A-Z][A-Za-z0-9_]*)\b/);
4
- if (!classMatch)
4
+ if (!classMatch?.[1])
5
5
  return null;
6
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
7
  if (/protected\s+\$appends\s*=/.test(source))
@@ -16,23 +16,23 @@ export function parseLaravelModel(source) {
16
16
  return { className, table, fillable, hidden, casts, relationships, tsSource, notes };
17
17
  }
18
18
  function extractStringProperty(source, name) {
19
- const re = new RegExp(`protected\\s+\\$${name}\\s*=\\s*['"]([^'"]+)['"]`), m = source.match(re);
20
- return m ? m[1] : null;
19
+ const re = new RegExp(`protected\\s+\\$${name}\\s*=\\s*['"]([^'"]+)['"]`);
20
+ return source.match(re)?.[1] ?? null;
21
21
  }
22
22
  function extractArrayProperty(source, name) {
23
23
  const re = new RegExp(`protected\\s+\\$${name}\\s*=\\s*\\[([^\\]]*)\\]`, "s"), m = source.match(re);
24
- if (!m)
24
+ if (!m?.[1])
25
25
  return [];
26
26
  return m[1].split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
27
27
  }
28
28
  function extractKeyedArrayProperty(source, name) {
29
29
  const re = new RegExp(`protected\\s+\\$${name}\\s*=\\s*\\[([^\\]]*)\\]`, "s"), m = source.match(re);
30
- if (!m)
30
+ if (!m?.[1])
31
31
  return {};
32
32
  const out = {}, pairs = m[1].split(",");
33
33
  for (const pair of pairs) {
34
34
  const kv = pair.match(/['"]([^'"]+)['"]\s*=>\s*['"]([^'"]+)['"]/);
35
- if (kv)
35
+ if (kv?.[1] && kv[2] !== void 0)
36
36
  out[kv[1]] = kv[2];
37
37
  }
38
38
  return out;
@@ -42,6 +42,8 @@ function extractRelationships(source) {
42
42
  let m;
43
43
  while ((m = methodRegex.exec(source)) !== null) {
44
44
  const name = m[1], kind = m[2], target = m[3];
45
+ if (!name || !kind || !target)
46
+ continue;
45
47
  found.push({ name, kind, target });
46
48
  }
47
49
  return found;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/buddy",
3
3
  "type": "module",
4
- "version": "0.70.73",
4
+ "version": "0.70.74",
5
5
  "description": "Meet Buddy. The Stacks runtime.",
6
6
  "author": "Chris Breuer",
7
7
  "contributors": [
@@ -83,11 +83,11 @@
83
83
  "compile:darwin-arm64": "bun build ./src/cli.ts --compile --minify --target=bun-darwin-arm64 --external=localtunnels/cloud --external=bun-queue --outfile bin/buddy-darwin-arm64",
84
84
  "typecheck": "bun tsc --noEmit",
85
85
  "zip:all": "bun run zip:linux-x64 && bun run zip:linux-arm64 && bun run zip:windows-x64 && bun run zip:darwin-x64 && bun run zip:darwin-arm64",
86
- "zip:linux-x64": "zip -r bin/buddy-linux-x64.zip bin/buddy-linux-x64",
87
- "zip:linux-arm64": "zip -r bin/buddy-linux-arm64.zip bin/buddy-linux-arm64",
88
- "zip:windows-x64": "zip -r bin/buddy-windows-x64.zip bin/buddy-windows-x64.exe",
89
- "zip:darwin-x64": "zip -r bin/buddy-darwin-x64.zip bin/buddy-darwin-x64",
90
- "zip:darwin-arm64": "zip -r bin/buddy-darwin-arm64.zip bin/buddy-darwin-arm64",
86
+ "zip:linux-x64": "zip -j bin/buddy-linux-x64.zip bin/buddy-linux-x64",
87
+ "zip:linux-arm64": "zip -j bin/buddy-linux-arm64.zip bin/buddy-linux-arm64",
88
+ "zip:windows-x64": "zip -j bin/buddy-windows-x64.zip bin/buddy-windows-x64.exe",
89
+ "zip:darwin-x64": "zip -j bin/buddy-darwin-x64.zip bin/buddy-darwin-x64",
90
+ "zip:darwin-arm64": "zip -j bin/buddy-darwin-arm64.zip bin/buddy-darwin-arm64",
91
91
  "prepublishOnly": "bun run build"
92
92
  },
93
93
  "dependencies": {
@@ -135,10 +135,10 @@
135
135
  "@stacksjs/ui": "^0.70.23",
136
136
  "@stacksjs/utils": "^0.70.23",
137
137
  "@stacksjs/validation": "^0.70.23",
138
- "@stacksjs/ts-cloud": "^0.7.12"
138
+ "@stacksjs/ts-cloud": "^0.7.14"
139
139
  },
140
140
  "devDependencies": {
141
- "better-dx": "^0.2.12"
141
+ "better-dx": "^0.2.16"
142
142
  },
143
143
  "web-types": "./web-types.json"
144
144
  }