@absolutejs/absolute 0.20.0-beta.77 → 0.20.0-beta.78

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.
@@ -186,20 +186,30 @@ var collectBareImportsFromFile = async (entryPath, transpiler, maxDepth = 8) =>
186
186
  }
187
187
  return Array.from(bareOut);
188
188
  };
189
+ var MAX_DISCOVERY_FILES = 2000;
189
190
  var collectTransitiveImports = async (specs, alreadyVendored, alreadyScanned) => {
191
+ const { dirname } = await import("path");
190
192
  const transpiler = new Bun.Transpiler({ loader: "js" });
191
193
  const newSpecs = new Set;
192
- for (const spec of specs) {
193
- if (alreadyScanned.has(spec))
194
+ const queue = [...specs].map((spec) => ({ from: process.cwd(), spec }));
195
+ let visited = 0;
196
+ while (queue.length > 0 && visited < MAX_DISCOVERY_FILES) {
197
+ const next = queue.shift();
198
+ if (!next)
199
+ break;
200
+ const key = `${next.from}\x00${next.spec}`;
201
+ if (alreadyScanned.has(key))
194
202
  continue;
195
- alreadyScanned.add(spec);
203
+ alreadyScanned.add(key);
196
204
  let resolved;
197
205
  try {
198
- resolved = Bun.resolveSync(spec, process.cwd());
206
+ resolved = Bun.resolveSync(next.spec, next.from);
199
207
  } catch {
200
208
  continue;
201
209
  }
210
+ visited += 1;
202
211
  const bareImports = await collectBareImportsFromFile(resolved, transpiler);
212
+ const importerDirectory = dirname(resolved);
203
213
  for (const child of bareImports) {
204
214
  if (!isBareSpecifier(child))
205
215
  continue;
@@ -209,11 +219,9 @@ var collectTransitiveImports = async (specs, alreadyVendored, alreadyScanned) =>
209
219
  continue;
210
220
  if (isAbsolutePackageSpecifier(child))
211
221
  continue;
212
- if (alreadyVendored.has(child))
213
- continue;
214
- if (!isResolvable(child))
215
- continue;
216
- newSpecs.add(child);
222
+ if (!alreadyVendored.has(child) && isResolvable(child))
223
+ newSpecs.add(child);
224
+ queue.push({ from: importerDirectory, spec: child });
217
225
  }
218
226
  }
219
227
  return newSpecs;
@@ -383,5 +391,6 @@ var computeDepVendorPaths = async (directories, files = []) => {
383
391
  };
384
392
  export {
385
393
  buildDepVendor,
394
+ collectTransitiveImports,
386
395
  computeDepVendorPaths
387
396
  };
@@ -1,14 +1,14 @@
1
1
  // @bun
2
- import {
3
- acquireBuildDirectoryLock,
4
- updateLockMetadata
5
- } from "./index-4rme31py.js";
6
2
  import {
7
3
  TUNNEL_CONTROL_PATH,
8
4
  TUNNEL_FORWARDED_HOST_HEADER,
9
5
  decodeTunnelMessage,
10
6
  encodeTunnelMessage
11
7
  } from "./index-6h54jqdq.js";
8
+ import {
9
+ acquireBuildDirectoryLock,
10
+ updateLockMetadata
11
+ } from "./index-4rme31py.js";
12
12
  import {
13
13
  scanListeners
14
14
  } from "./index-fhaf3tdk.js";
@@ -731,7 +731,7 @@ var serializeDependencies = (graph) => {
731
731
  return dependencies;
732
732
  };
733
733
  var computePayload = async (config, vendorSourceDirs) => {
734
- const { computeDepVendorPaths } = await import("./buildDepVendor-521faxfs.js");
734
+ const { computeDepVendorPaths } = await import("./buildDepVendor-rz5w5cp0.js");
735
735
  const vendors = computeDepVendorPaths(vendorSourceDirs);
736
736
  const graph = {
737
737
  dependencies: new Map,
package/dist/cli/index.js CHANGED
@@ -58,7 +58,7 @@ if (command === "dev") {
58
58
  }
59
59
  const positionalArgs = stripNamedArgs("--config", "--android-device", "--ios-device").filter((arg) => arg !== "--no-mobile" && arg !== "--eager");
60
60
  const serverEntry = positionalArgs[0] ?? DEFAULT_SERVER_ENTRY;
61
- const { dev } = await import("./dev-p8403w4q.js");
61
+ const { dev } = await import("./dev-3xz7366n.js");
62
62
  await dev(serverEntry, configPath, {
63
63
  androidDevice,
64
64
  eager: args.includes("--eager"),
@@ -94,7 +94,7 @@ if (command === "dev") {
94
94
  command: `workspace:${workspaceCommand ?? "unknown"}`
95
95
  });
96
96
  const configPath = parseNamedArg("--config");
97
- const { workspace } = await import("./workspace-weacygfv.js");
97
+ const { workspace } = await import("./workspace-h7czpaqh.js");
98
98
  await workspace(workspaceCommand, {
99
99
  configPath,
100
100
  noTui: args.includes("--no-tui")
@@ -215,6 +215,10 @@ if (command === "dev") {
215
215
  const configPath = parseNamedArg("--config");
216
216
  const { typecheck } = await import("./typecheck-7n5599tp.js");
217
217
  await typecheck(configPath);
218
+ } else if (command === "migrate") {
219
+ sendTelemetryEvent("cli:command", { command });
220
+ const { migrate } = await import("./migrate-p4rp5r6b.js");
221
+ migrate(parseNamedArg("--path"));
218
222
  } else if (command === "mkcert") {
219
223
  sendTelemetryEvent("cli:command", { command });
220
224
  const { setupMkcert } = await import("./devCert-avr89zf3.js");
@@ -257,6 +261,7 @@ if (command === "dev") {
257
261
  console.error(" prettier Run Prettier check (cached)");
258
262
  console.error(" routes [--json] List every route (pages + API) of a running dev server");
259
263
  console.error(" typecheck Run type checkers for all frameworks");
264
+ console.error(" migrate Report what moving this project to AbsoluteJS involves");
260
265
  console.error(" telemetry Manage anonymous telemetry");
261
266
  console.error(" tunnel-relay Run the public reverse-tunnel relay (for webhook dev)");
262
267
  process.exit(1);
@@ -0,0 +1,521 @@
1
+ // @bun
2
+ import {
3
+ colors
4
+ } from "./index-8nym68py.js";
5
+ import"./index-czwrxgdj.js";
6
+ import"./index-mywsk201.js";
7
+
8
+ // src/migrate/detect.ts
9
+ var CONFIDENT = 0.7;
10
+ var DEPENDENCY_SIGNALS = [
11
+ ["@absolutejs/absolute", "absolutejs"],
12
+ ["next", "nextjs"],
13
+ ["nuxt", "nuxt"],
14
+ ["@remix-run/react", "remix"],
15
+ ["@remix-run/node", "remix"],
16
+ ["@sveltejs/kit", "sveltekit"],
17
+ ["astro", "astro"],
18
+ ["react-scripts", "create-react-app"],
19
+ ["vite", "vite"]
20
+ ];
21
+ var FILE_SIGNALS = [
22
+ ["absolute.config.ts", "absolutejs"],
23
+ ["absolute.config.js", "absolutejs"],
24
+ ["next.config.js", "nextjs"],
25
+ ["next.config.mjs", "nextjs"],
26
+ ["next.config.ts", "nextjs"],
27
+ ["nuxt.config.ts", "nuxt"],
28
+ ["remix.config.js", "remix"],
29
+ ["svelte.config.js", "sveltekit"],
30
+ ["astro.config.mjs", "astro"],
31
+ ["vite.config.ts", "vite"],
32
+ ["vite.config.js", "vite"]
33
+ ];
34
+ var DEPENDENCY_WEIGHT = 0.75;
35
+ var FILE_WEIGHT = 0.3;
36
+ var CERTAIN = 1;
37
+ var STACK_PRECEDENCE = [
38
+ "absolutejs",
39
+ "nextjs",
40
+ "nuxt",
41
+ "remix",
42
+ "sveltekit",
43
+ "astro",
44
+ "create-react-app",
45
+ "vite",
46
+ "unknown"
47
+ ];
48
+ var AMBIGUITY_PENALTY = 0.3;
49
+ var dependencyNames = (manifest) => {
50
+ if (!manifest)
51
+ return new Set;
52
+ return new Set([
53
+ ...Object.keys(manifest.dependencies ?? {}),
54
+ ...Object.keys(manifest.devDependencies ?? {}),
55
+ ...Object.keys(manifest.peerDependencies ?? {})
56
+ ]);
57
+ };
58
+ var SUBSUMED_BY_OTHERS = new Set(["vite"]);
59
+ var scoreSignals = (present, signals, weight, label) => {
60
+ const scores = new Map;
61
+ const evidence = [];
62
+ for (const [name, kind] of signals) {
63
+ if (!present(name))
64
+ continue;
65
+ scores.set(kind, (scores.get(kind) ?? 0) + weight);
66
+ evidence.push(label(name));
67
+ }
68
+ return { evidence, scores };
69
+ };
70
+ var merge = (into, from) => {
71
+ for (const [kind, score] of from) {
72
+ into.set(kind, (into.get(kind) ?? 0) + score);
73
+ }
74
+ return into;
75
+ };
76
+ var detectStack = ({
77
+ manifest,
78
+ files
79
+ }) => {
80
+ const dependencies = dependencyNames(manifest);
81
+ const names = new Set(files.map((file) => file.split("/").pop() ?? file));
82
+ const fromDependencies = scoreSignals((name) => dependencies.has(name), DEPENDENCY_SIGNALS, DEPENDENCY_WEIGHT, (name) => ({
83
+ detail: `depends on ${name}`,
84
+ source: "package.json"
85
+ }));
86
+ const fromFiles = scoreSignals((name) => names.has(name), FILE_SIGNALS, FILE_WEIGHT, (name) => ({ detail: "config file present", source: name }));
87
+ const scores = merge(new Map(fromDependencies.scores), fromFiles.scores);
88
+ const decisive = [...scores.keys()].some((kind2) => !SUBSUMED_BY_OTHERS.has(kind2));
89
+ if (decisive) {
90
+ for (const kind2 of SUBSUMED_BY_OTHERS)
91
+ scores.delete(kind2);
92
+ }
93
+ const rank = (kind2) => {
94
+ const index = STACK_PRECEDENCE.indexOf(kind2);
95
+ return index === -1 ? STACK_PRECEDENCE.length : index;
96
+ };
97
+ const ranked = [...scores.entries()].sort(([leftKind, left], [rightKind, right]) => right - left || rank(leftKind) - rank(rightKind));
98
+ const [top] = ranked;
99
+ if (!top) {
100
+ return {
101
+ alternates: [],
102
+ confidence: 0,
103
+ evidence: [],
104
+ kind: "unknown"
105
+ };
106
+ }
107
+ const [kind, score] = top;
108
+ const [, runnerUpScore] = ranked[1] ?? [];
109
+ const ambiguous = runnerUpScore === score;
110
+ return {
111
+ alternates: ranked.slice(1).map(([alternate]) => alternate),
112
+ confidence: Math.max(0, Math.min(score, CERTAIN) - (ambiguous ? AMBIGUITY_PENALTY : 0)),
113
+ evidence: [...fromDependencies.evidence, ...fromFiles.evidence],
114
+ kind
115
+ };
116
+ };
117
+
118
+ // src/migrate/scan.ts
119
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
120
+ import { join, relative } from "path";
121
+ // src/migrate/catalogSnapshot.json
122
+ var catalogSnapshot_default = {
123
+ comment: "Snapshot of first-party package names, used to validate migration suggestions offline. Studio passes its live catalog instead; regenerate with scripts/generateCatalog.ts in the Studio repo.",
124
+ generatedFrom: "@absolutejs/studio packageCatalog.json contract 2",
125
+ names: [
126
+ "@absolutejs/a2a",
127
+ "@absolutejs/agency",
128
+ "@absolutejs/agent",
129
+ "@absolutejs/agent-conformance",
130
+ "@absolutejs/agent-control",
131
+ "@absolutejs/agent-discovery",
132
+ "@absolutejs/agent-inbox",
133
+ "@absolutejs/agent-memory",
134
+ "@absolutejs/agent-reputation",
135
+ "@absolutejs/agent-runtime",
136
+ "@absolutejs/agent-sandbox",
137
+ "@absolutejs/agent-trust",
138
+ "@absolutejs/agents-mcp",
139
+ "@absolutejs/arazzo",
140
+ "@absolutejs/artifacts",
141
+ "@absolutejs/audience",
142
+ "@absolutejs/audit",
143
+ "@absolutejs/audit-elysia",
144
+ "@absolutejs/audit-postgres",
145
+ "@absolutejs/audit-s3",
146
+ "@absolutejs/auth",
147
+ "@absolutejs/beacon",
148
+ "@absolutejs/billing",
149
+ "@absolutejs/blob",
150
+ "@absolutejs/collectibles",
151
+ "@absolutejs/commerce",
152
+ "@absolutejs/commerce-customcat",
153
+ "@absolutejs/commerce-easypost",
154
+ "@absolutejs/commerce-resend",
155
+ "@absolutejs/commerce-stripe",
156
+ "@absolutejs/compliance",
157
+ "@absolutejs/crm",
158
+ "@absolutejs/discover",
159
+ "@absolutejs/dispatch",
160
+ "@absolutejs/dispatch-apns",
161
+ "@absolutejs/dispatch-fcm",
162
+ "@absolutejs/dispatch-postmark",
163
+ "@absolutejs/dispatch-resend",
164
+ "@absolutejs/dispatch-telnyx",
165
+ "@absolutejs/dispatch-twilio",
166
+ "@absolutejs/egress",
167
+ "@absolutejs/email",
168
+ "@absolutejs/engagement",
169
+ "@absolutejs/engagement-apollo",
170
+ "@absolutejs/enrich",
171
+ "@absolutejs/errors",
172
+ "@absolutejs/errors-postgres",
173
+ "@absolutejs/execution",
174
+ "@absolutejs/git",
175
+ "@absolutejs/health",
176
+ "@absolutejs/incidents",
177
+ "@absolutejs/logs",
178
+ "@absolutejs/marketplace",
179
+ "@absolutejs/mcp",
180
+ "@absolutejs/media",
181
+ "@absolutejs/meeting",
182
+ "@absolutejs/meeting-discord",
183
+ "@absolutejs/meeting-recall",
184
+ "@absolutejs/metering",
185
+ "@absolutejs/metrics",
186
+ "@absolutejs/observability",
187
+ "@absolutejs/onchain",
188
+ "@absolutejs/onchain-base",
189
+ "@absolutejs/outcomes",
190
+ "@absolutejs/partnership",
191
+ "@absolutejs/policy",
192
+ "@absolutejs/pwa",
193
+ "@absolutejs/queue",
194
+ "@absolutejs/queue-postgres",
195
+ "@absolutejs/queue-redis",
196
+ "@absolutejs/rag",
197
+ "@absolutejs/rag-pinecone",
198
+ "@absolutejs/rag-postgres",
199
+ "@absolutejs/rag-sqlite",
200
+ "@absolutejs/rate-limit",
201
+ "@absolutejs/reliability",
202
+ "@absolutejs/renown",
203
+ "@absolutejs/replay",
204
+ "@absolutejs/rules",
205
+ "@absolutejs/scoped-state",
206
+ "@absolutejs/secrets",
207
+ "@absolutejs/site-admin",
208
+ "@absolutejs/sync",
209
+ "@absolutejs/sync-automerge",
210
+ "@absolutejs/sync-bus-pg",
211
+ "@absolutejs/sync-bus-redis",
212
+ "@absolutejs/sync-loro",
213
+ "@absolutejs/sync-pack-comments",
214
+ "@absolutejs/sync-pack-counters",
215
+ "@absolutejs/sync-pack-digest",
216
+ "@absolutejs/sync-pack-favorites",
217
+ "@absolutejs/sync-pack-mentions",
218
+ "@absolutejs/sync-pack-notifications",
219
+ "@absolutejs/sync-pack-presence",
220
+ "@absolutejs/sync-pack-utils",
221
+ "@absolutejs/sync-yjs",
222
+ "@absolutejs/tour",
223
+ "@absolutejs/voice",
224
+ "@absolutejs/voice-assemblyai",
225
+ "@absolutejs/voice-azure",
226
+ "@absolutejs/voice-cartesia",
227
+ "@absolutejs/voice-deepgram",
228
+ "@absolutejs/voice-elevenlabs",
229
+ "@absolutejs/voice-gemini",
230
+ "@absolutejs/voice-gladia",
231
+ "@absolutejs/voice-google-speech",
232
+ "@absolutejs/voice-lmnt",
233
+ "@absolutejs/voice-neets",
234
+ "@absolutejs/voice-openai",
235
+ "@absolutejs/voice-openai-whisper",
236
+ "@absolutejs/voice-playht",
237
+ "@absolutejs/voice-rime",
238
+ "@absolutejs/voice-smallest",
239
+ "@absolutejs/voice-soniox",
240
+ "@absolutejs/voice-speechmatics",
241
+ "@absolutejs/vulnerabilities",
242
+ "@absolutejs/vulnerabilities-epss",
243
+ "@absolutejs/vulnerabilities-kev",
244
+ "@absolutejs/vulnerabilities-osv",
245
+ "@absolutejs/vulnerabilities-postgres",
246
+ "@absolutejs/vulnerabilities-report",
247
+ "@absolutejs/vulnerabilities-ubuntu",
248
+ "@absolutejs/vulnerabilities-witness",
249
+ "@absolutejs/vulnerabilities-worker",
250
+ "@absolutejs/wallet",
251
+ "@absolutejs/wallet-stripe",
252
+ "@absolutejs/webmcp"
253
+ ]
254
+ };
255
+
256
+ // src/migrate/substitutions.ts
257
+ var DEFAULT_SUBSTITUTION_RULES = [
258
+ {
259
+ from: "next-auth",
260
+ rationale: "Sessions, OAuth providers, MFA and passkeys, wired to the same request context.",
261
+ to: "@absolutejs/auth"
262
+ },
263
+ {
264
+ from: "bullmq",
265
+ rationale: "Durable job queue with a Postgres or Redis adapter instead of a required Redis.",
266
+ to: "@absolutejs/queue"
267
+ },
268
+ {
269
+ from: "bull",
270
+ rationale: "Durable job queue with first-party adapters.",
271
+ to: "@absolutejs/queue"
272
+ },
273
+ {
274
+ from: "@sentry/node",
275
+ rationale: "Error capture, session replay and symbolication without a third-party account.",
276
+ to: "@absolutejs/errors"
277
+ },
278
+ {
279
+ from: "@sentry/browser",
280
+ rationale: "Browser error capture reported through the same pipeline.",
281
+ to: "@absolutejs/beacon"
282
+ },
283
+ {
284
+ from: "web-vitals",
285
+ rationale: "Real-user vitals already collected by the framework client.",
286
+ to: "@absolutejs/beacon"
287
+ },
288
+ {
289
+ from: "nodemailer",
290
+ rationale: "Transactional send with swappable Resend, Postmark or SMTP adapters.",
291
+ to: "@absolutejs/dispatch"
292
+ },
293
+ {
294
+ from: "resend",
295
+ rationale: "Same provider, behind an adapter you can change without touching call sites.",
296
+ to: "@absolutejs/dispatch-resend"
297
+ },
298
+ {
299
+ from: "stripe",
300
+ rationale: "Checkout, subscriptions and webhooks with the ledger already modelled.",
301
+ to: "@absolutejs/commerce-stripe"
302
+ },
303
+ {
304
+ from: "@aws-sdk/client-s3",
305
+ rationale: "Object storage with presigning and lifecycle handled for you.",
306
+ to: "@absolutejs/blob"
307
+ },
308
+ {
309
+ from: "yjs",
310
+ rationale: "Collaborative state with presence, comments and history packs included.",
311
+ to: "@absolutejs/sync"
312
+ },
313
+ {
314
+ from: "zustand",
315
+ rationale: "Scoped client state that survives island boundaries.",
316
+ to: "@absolutejs/scoped-state"
317
+ },
318
+ {
319
+ from: "express-rate-limit",
320
+ rationale: "Rate limiting applied at the route contract.",
321
+ to: "@absolutejs/rate-limit"
322
+ },
323
+ {
324
+ from: "pino",
325
+ rationale: "Structured logging that joins the observability pipeline.",
326
+ to: "@absolutejs/logs"
327
+ },
328
+ {
329
+ from: "winston",
330
+ rationale: "Structured logging that joins the observability pipeline.",
331
+ to: "@absolutejs/logs"
332
+ }
333
+ ];
334
+ var planSubstitutions = ({
335
+ dependencies,
336
+ importsByPackage = {},
337
+ catalog,
338
+ rules = DEFAULT_SUBSTITUTION_RULES
339
+ }) => {
340
+ const declared = new Set(dependencies);
341
+ const available = new Set(catalog.map((entry) => entry.name));
342
+ return rules.filter((rule) => declared.has(rule.from) && available.has(rule.to)).map((rule) => ({
343
+ from: rule.from,
344
+ rationale: rule.rationale,
345
+ to: rule.to,
346
+ usedIn: [...importsByPackage[rule.from] ?? []]
347
+ })).sort((left, right) => right.usedIn.length - left.usedIn.length || left.from.localeCompare(right.from));
348
+ };
349
+
350
+ // src/migrate/scan.ts
351
+ var SKIP_DIRECTORIES = new Set([
352
+ ".git",
353
+ ".next",
354
+ ".turbo",
355
+ "build",
356
+ "coverage",
357
+ "dist",
358
+ "node_modules",
359
+ "out"
360
+ ]);
361
+ var SOURCE_EXTENSIONS = [
362
+ ".ts",
363
+ ".tsx",
364
+ ".js",
365
+ ".jsx",
366
+ ".mjs",
367
+ ".vue",
368
+ ".svelte"
369
+ ];
370
+ var MAX_FILES = 5000;
371
+ var isSource = (file) => SOURCE_EXTENSIONS.some((extension) => file.endsWith(extension));
372
+ var entriesOf = (directory) => {
373
+ try {
374
+ return readdirSync(directory);
375
+ } catch {
376
+ return [];
377
+ }
378
+ };
379
+ var isDirectory = (path) => {
380
+ try {
381
+ return statSync(path).isDirectory();
382
+ } catch {
383
+ return false;
384
+ }
385
+ };
386
+ var textOf = (path) => {
387
+ try {
388
+ return readFileSync(path, "utf-8");
389
+ } catch {
390
+ return "";
391
+ }
392
+ };
393
+ var walkOne = (root, directory) => entriesOf(directory).reduce((carry, entry) => {
394
+ if (SKIP_DIRECTORIES.has(entry))
395
+ return carry;
396
+ const absolute = join(directory, entry);
397
+ if (isDirectory(absolute)) {
398
+ carry.directories.push(absolute);
399
+ return carry;
400
+ }
401
+ carry.files.push(relative(root, absolute));
402
+ return carry;
403
+ }, { directories: [], files: [] });
404
+ var listProjectFiles = (root, limit = MAX_FILES) => {
405
+ const found = [];
406
+ const queue = [root];
407
+ while (queue.length > 0 && found.length < limit) {
408
+ const directory = queue.shift();
409
+ if (directory === undefined)
410
+ break;
411
+ const walk = walkOne(root, directory);
412
+ queue.push(...walk.directories);
413
+ found.push(...walk.files.slice(0, limit - found.length));
414
+ }
415
+ return found;
416
+ };
417
+ var isRecord = (value) => typeof value === "object" && value !== null;
418
+ var sectionNames = (value) => isRecord(value) ? Object.keys(value) : [];
419
+ var readManifest = (root) => {
420
+ const path = join(root, "package.json");
421
+ if (!existsSync(path))
422
+ return null;
423
+ const parsed = JSON.parse(textOf(path) || "null");
424
+ if (!isRecord(parsed))
425
+ return null;
426
+ return parsed;
427
+ };
428
+ var declaredDependencies = (manifest) => {
429
+ if (!manifest)
430
+ return [];
431
+ const source = manifest;
432
+ return ["dependencies", "devDependencies", "peerDependencies"].flatMap((section) => sectionNames(source[section]));
433
+ };
434
+ var importsInFile = (text, packages) => packages.filter((name) => text.includes(`'${name}`) || text.includes(`"${name}`));
435
+ var findImporters = (root, files, packages) => files.filter(isSource).reduce((carry, file) => {
436
+ for (const name of importsInFile(textOf(join(root, file)), packages)) {
437
+ carry[name] = [...carry[name] ?? [], file];
438
+ }
439
+ return carry;
440
+ }, {});
441
+ var scanProject = (root, { catalog, rules } = {}) => {
442
+ const manifest = readManifest(root);
443
+ const files = listProjectFiles(root);
444
+ const dependencies = declaredDependencies(manifest);
445
+ const resolvedCatalog = catalog ?? catalogSnapshot_default.names.map((name) => ({ name }));
446
+ return {
447
+ detection: detectStack({ files, manifest }),
448
+ substitutions: planSubstitutions({
449
+ catalog: resolvedCatalog,
450
+ dependencies,
451
+ importsByPackage: findImporters(root, files, dependencies),
452
+ rules
453
+ })
454
+ };
455
+ };
456
+
457
+ // src/cli/scripts/migrate.ts
458
+ var PERCENT = 100;
459
+ var paint = (code, text) => `${code}${text}${colors.reset}`;
460
+ var bold = (text) => paint(colors.bold, text);
461
+ var dim = (text) => paint(colors.dim, text);
462
+ var warn = (text) => paint(colors.yellow, text);
463
+ var stackLabel = {
464
+ absolutejs: "AbsoluteJS",
465
+ astro: "Astro",
466
+ "create-react-app": "Create React App",
467
+ nextjs: "Next.js",
468
+ nuxt: "Nuxt",
469
+ remix: "Remix",
470
+ sveltekit: "SvelteKit",
471
+ unknown: "not recognised",
472
+ vite: "Vite"
473
+ };
474
+ var renderDetection = (plan) => {
475
+ const { detection } = plan;
476
+ const percent = Math.round(detection.confidence * PERCENT);
477
+ const label = stackLabel[detection.kind] ?? detection.kind;
478
+ const lines = [`
479
+ Stack: ${bold(label)} (${percent}% confident)`];
480
+ for (const item of detection.evidence.slice(0, 4)) {
481
+ lines.push(dim(` ${item.source} \u2014 ${item.detail}`));
482
+ }
483
+ if (detection.alternates.length > 0) {
484
+ lines.push(dim(` also matched: ${detection.alternates.join(", ")}`));
485
+ }
486
+ if (detection.confidence < CONFIDENT && detection.kind !== "unknown") {
487
+ lines.push(warn(" Low confidence \u2014 confirm the stack before migrating."));
488
+ }
489
+ return lines.join(`
490
+ `);
491
+ };
492
+ var renderSubstitutions = (plan) => {
493
+ const { substitutions } = plan;
494
+ if (substitutions.length === 0) {
495
+ return `
496
+ ${dim("No first-party substitutions found.")}`;
497
+ }
498
+ const lines = [
499
+ `
500
+ ${bold(String(substitutions.length))} dependencies have a first-party equivalent:`
501
+ ];
502
+ for (const item of substitutions) {
503
+ const where = item.usedIn.length > 0 ? `${item.usedIn.length} file${item.usedIn.length === 1 ? "" : "s"}` : "declared, never imported";
504
+ lines.push(` ${item.from} \u2192 ${bold(item.to)} ${dim(`(${where})`)}`);
505
+ lines.push(dim(` ${item.rationale}`));
506
+ }
507
+ lines.push(dim(`
508
+ Nothing was changed. This command only reports; apply the swaps yourself
509
+ or let Studio propose them as a reviewable diff.`));
510
+ return lines.join(`
511
+ `);
512
+ };
513
+ var migrate = (root = process.cwd()) => {
514
+ const plan = scanProject(root);
515
+ console.log(renderDetection(plan));
516
+ console.log(renderSubstitutions(plan));
517
+ console.log("");
518
+ };
519
+ export {
520
+ migrate
521
+ };
@@ -1104,6 +1104,20 @@ var warnForSharedBuildDirectories = (services, addLog) => {
1104
1104
  addLog("workspace", `Services ${names.map((name) => `"${name}"`).join(" and ")} resolve to the same buildDirectory: ${buildDirectory}. Builds will be serialized and the last completed build will own that directory.`, "warn");
1105
1105
  }
1106
1106
  };
1107
+ var startWorkspaceOrShutDown = async ({
1108
+ onFailure,
1109
+ shutdown,
1110
+ start
1111
+ }) => {
1112
+ try {
1113
+ await start();
1114
+ return true;
1115
+ } catch (error) {
1116
+ onFailure(error instanceof Error ? error.message : String(error));
1117
+ await shutdown(1);
1118
+ return false;
1119
+ }
1120
+ };
1107
1121
  var workspace = async (subcommand, options) => {
1108
1122
  if (subcommand !== "dev") {
1109
1123
  throw new Error(subcommand ? `Unknown workspace command: ${subcommand}` : "No workspace subcommand specified. Use `absolute workspace dev`.");
@@ -1337,7 +1351,13 @@ var workspace = async (subcommand, options) => {
1337
1351
  await killProcesses();
1338
1352
  restarting = false;
1339
1353
  workspaceBootStartedAt = performance.now();
1340
- await startServices();
1354
+ const restarted = await startWorkspaceOrShutDown({
1355
+ shutdown,
1356
+ start: startServices,
1357
+ onFailure: (message) => addLog("workspace", `Workspace failed to restart: ${message}`, "error")
1358
+ });
1359
+ if (!restarted)
1360
+ return;
1341
1361
  tui.setReadyDuration(performance.now() - workspaceBootStartedAt);
1342
1362
  };
1343
1363
  const togglePause = () => {
@@ -1402,12 +1422,19 @@ var workspace = async (subcommand, options) => {
1402
1422
  shutdown(0);
1403
1423
  });
1404
1424
  tui.start();
1405
- await startServices();
1425
+ const started = await startWorkspaceOrShutDown({
1426
+ shutdown,
1427
+ start: startServices,
1428
+ onFailure: (message) => addLog("workspace", `Workspace failed to start: ${message}`, "error")
1429
+ });
1430
+ if (!started)
1431
+ return;
1406
1432
  tui.setReadyDuration(performance.now() - workspaceBootStartedAt);
1407
1433
  await Promise.withResolvers().promise;
1408
1434
  };
1409
1435
  export {
1410
1436
  findSharedWorkspaceBuildDirectories,
1411
1437
  resolveWorkspaceServicePort,
1438
+ startWorkspaceOrShutDown,
1412
1439
  workspace
1413
1440
  };