@flisk-sh/flisk 1.3.204 → 1.3.206

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 (64) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/cli/dist/cli.js +251 -90
  3. package/cli/dist/commands/doctor.js +29 -15
  4. package/cli/dist/commands/project.js +108 -60
  5. package/cli/dist/commands/resource.js +3 -1
  6. package/cli/dist/commands/up.js +8 -5
  7. package/cli/dist/generator/template-engine.js +17 -9
  8. package/cli/dist/utils/cli-command.js +3 -1
  9. package/cli/dist/utils/colima.js +23 -4
  10. package/cli/dist/utils/demo-stage.js +315 -0
  11. package/cli/dist/utils/doctor-fix.js +35 -9
  12. package/cli/dist/utils/machine-output.js +27 -0
  13. package/cli/dist/utils/native-windows.js +20 -0
  14. package/cli/dist/utils/one-command.js +15 -8
  15. package/cli/dist/utils/project-templates.js +1 -0
  16. package/cli/dist-bundle/chunk-3QQGL3NQ.js +35 -0
  17. package/cli/dist-bundle/chunk-4W6OXDLK.js +17 -0
  18. package/cli/dist-bundle/chunk-53M5D4KE.js +2 -0
  19. package/cli/dist-bundle/{chunk-BS6G2SLM.js → chunk-5CFVN3W3.js} +1 -1
  20. package/cli/dist-bundle/{chunk-RFHMXJTK.js → chunk-6Y6G7PSD.js} +1 -1
  21. package/cli/dist-bundle/chunk-7C5VA6CW.js +57 -0
  22. package/cli/dist-bundle/chunk-D2BXXJDM.js +7 -0
  23. package/cli/dist-bundle/chunk-GANRXKBM.js +2 -0
  24. package/cli/dist-bundle/chunk-GIMHTZNR.js +2 -0
  25. package/cli/dist-bundle/chunk-GOEBYLFN.js +2 -0
  26. package/cli/dist-bundle/chunk-LI3BXAX5.js +2 -0
  27. package/cli/dist-bundle/{chunk-WMF4AFAE.js → chunk-NCEK46NX.js} +10 -10
  28. package/cli/dist-bundle/chunk-ODVBKN6S.js +2 -0
  29. package/cli/dist-bundle/{chunk-Z6UD7UZH.js → chunk-PHI5AT3H.js} +2 -2
  30. package/cli/dist-bundle/chunk-XSPMLWAE.js +10 -0
  31. package/cli/dist-bundle/chunk-YZRPX76N.js +2 -0
  32. package/cli/dist-bundle/cli.js +269 -267
  33. package/cli/dist-bundle/cold-preflight-4I24DQLW.js +2 -0
  34. package/cli/dist-bundle/{colima-4ID4BGHC.js → colima-SVZX2UML.js} +1 -1
  35. package/cli/dist-bundle/demo-stage-EOMANLAD.js +7 -0
  36. package/cli/dist-bundle/native-windows-X5XVLFJS.js +2 -0
  37. package/cli/dist-bundle/one-command-P6XVAXWV.js +5 -0
  38. package/cli/dist-bundle/open-6374RNL6.js +2 -0
  39. package/cli/dist-bundle/project-IRWFRXWA.js +2 -0
  40. package/cli/dist-bundle/project-templates-JDRE7AVL.js +2 -0
  41. package/cli/dist-bundle/services-VIIBMWD4.js +2 -0
  42. package/cli/dist-bundle/tilt-port-owner-AN3ITRTR.js +2 -0
  43. package/cli/dist-bundle/url-actions-D6ROTHZG.js +2 -0
  44. package/cli/package.json +4 -4
  45. package/engine/assets/docker/wake-gateway/Dockerfile +1 -1
  46. package/engine/topologies/platform/docker/compose/compose.star +20 -18
  47. package/engine/topologies/platform/docker/compose/unsafe_value.star +23 -0
  48. package/engine/topologies/platform/docker/networking/traefik.star +19 -0
  49. package/package.json +2 -2
  50. package/cli/dist-bundle/chunk-CBFVK3TF.js +0 -21
  51. package/cli/dist-bundle/chunk-CF46CD6R.js +0 -6
  52. package/cli/dist-bundle/chunk-DTIHDMED.js +0 -5
  53. package/cli/dist-bundle/chunk-EPBYRJE3.js +0 -2
  54. package/cli/dist-bundle/chunk-GD5676UE.js +0 -52
  55. package/cli/dist-bundle/chunk-MKJNQTIN.js +0 -2
  56. package/cli/dist-bundle/chunk-QSPFSZCS.js +0 -34
  57. package/cli/dist-bundle/chunk-ZVGY7COV.js +0 -4
  58. package/cli/dist-bundle/cold-preflight-FXOZ5SR5.js +0 -2
  59. package/cli/dist-bundle/one-command-YP6L6MMM.js +0 -5
  60. package/cli/dist-bundle/open-UQ6DF3XN.js +0 -2
  61. package/cli/dist-bundle/project-GTYHCCFG.js +0 -2
  62. package/cli/dist-bundle/spinner-YLB6FYQH.js +0 -2
  63. package/cli/dist-bundle/tilt-port-owner-KLNX2ZI2.js +0 -2
  64. /package/cli/dist-bundle/{devtools-ZUHXW33W.js → devtools-MGITLOWZ.js} +0 -0
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { execFileSync } from "node:child_process";
4
4
  import { cpSync, existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
5
- import { basename, dirname, join, resolve } from "node:path";
5
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
6
6
  import { cwd } from "node:process";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import chalk from "chalk";
@@ -86,7 +86,15 @@ export function templateTargetDir(templateName, targetDir) {
86
86
  const fromRepo = template?.repo.split("/").pop() ?? templateName;
87
87
  return fromRepo.replace(/\.git$/, "");
88
88
  }
89
- export async function cloneProjectTemplate(templateName, targetDir, runtimeNextSteps) {
89
+ /** The last non-empty line of a child process error, for a one-line failure message. */
90
+ function lastLine(text) {
91
+ const lines = text
92
+ .split("\n")
93
+ .map((line) => line.trim())
94
+ .filter(Boolean);
95
+ return lines.pop() ?? "unknown error";
96
+ }
97
+ export async function cloneProjectTemplate(templateName, targetDir, runtimeNextSteps, options = {}) {
90
98
  const template = PROJECT_TEMPLATES[templateName];
91
99
  if (!template) {
92
100
  console.log(chalk.red(`\n❌ Unknown template: "${templateName}"`));
@@ -101,14 +109,18 @@ export async function cloneProjectTemplate(templateName, targetDir, runtimeNextS
101
109
  if (!isPathSafe(dirName)) {
102
110
  errorFactories.invalidPath(dirName).exit();
103
111
  }
104
- const destination = resolve(cwd(), dirName);
112
+ // If targetDir is an absolute path, use it directly as the destination
113
+ // Otherwise resolve it relative to cwd()
114
+ const destination = isAbsolute(dirName) ? dirName : resolve(cwd(), dirName);
105
115
  if (existsSync(destination) && readdirSync(destination).length > 0) {
106
116
  errorFactories.directoryExists(destination).exit();
107
117
  }
108
- showCommandHeader(`Cloning template: ${templateName}`);
109
- showDetail(`${template.description}\n`, 0);
110
- showDetail(`Source: ${template.bundledPath ? "bundled Flisk example" : template.repo}`, 0);
111
- showDetail(`Destination: ${destination}\n`, 0);
118
+ if (!options.quiet) {
119
+ showCommandHeader(`Cloning template: ${templateName}`);
120
+ showDetail(`${template.description}\n`, 0);
121
+ showDetail(`Source: ${template.bundledPath ? "bundled Flisk example" : template.repo}`, 0);
122
+ showDetail(`Destination: ${destination}\n`, 0);
123
+ }
112
124
  try {
113
125
  if (template.bundledPath) {
114
126
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../../");
@@ -127,15 +139,28 @@ export async function cloneProjectTemplate(templateName, targetDir, runtimeNextS
127
139
  }
128
140
  }
129
141
  else {
130
- execFileSync("git", ["clone", template.repo, destination], { stdio: "inherit" });
142
+ // Piped, not inherited: a raw git progress dump is noise for the caller and, in the
143
+ // one-command flow, it interleaves with the frame that is already on screen.
144
+ execFileSync("git", ["clone", template.repo, destination], {
145
+ stdio: ["ignore", "pipe", "pipe"],
146
+ encoding: "utf-8",
147
+ });
131
148
  }
132
149
  }
133
- catch {
134
- showErrorAndExit(template.bundledPath
135
- ? "Bundled example copy failed. Reinstall the Flisk CLI package and try again."
136
- : "git clone failed. Check you have git installed and network access.");
150
+ catch (error) {
151
+ const detail = error instanceof Error ? error.message : String(error);
152
+ const message = template.bundledPath
153
+ ? `Bundled example copy failed (${lastLine(detail)}). Reinstall the Flisk CLI package and try again.`
154
+ : `git clone failed: ${lastLine(detail)}. Check you have git installed and network access.`;
155
+ // A caller that paints its own surface (the one-command flow) needs to report the
156
+ // failure itself instead of having the process die mid-paint.
157
+ if (options.quiet)
158
+ throw new Error(message);
159
+ showErrorAndExit(message);
137
160
  }
138
161
  clearDiscoveryCache();
162
+ if (options.quiet)
163
+ return;
139
164
  console.log(chalk.green(`\n✅ Cloned "${templateName}" into ${dirName}/`));
140
165
  showDetail("\nNext steps:", 0);
141
166
  showDetail(`1. cd ${dirName}`);
@@ -195,22 +220,26 @@ export const projectCommand = addYesOption(new Command("project"))
195
220
  .option("--check", "Check if master configs exist and are in sync")
196
221
  .option("--force", "Overwrite existing configuration (dangerous)")
197
222
  .option("--config-file <path>", "Load project config from existing JSON file")
223
+ .option("-q, --quiet", "Only print what fails; used by `flisk demo`", false)
198
224
  .action(async (template, options) => {
199
225
  const { assertMachineReadyOrExit, detectRuntimePrerequisites, formatRuntimeNextSteps } = await import("../utils/cold-preflight.js");
200
226
  // Project setup only writes files: it must not need a running Docker daemon
201
227
  // or an installed Tilt. The runtime is checked on `flisk up`.
202
228
  await assertMachineReadyOrExit({ scope: "scaffold" });
203
229
  const runtimeNextSteps = formatRuntimeNextSteps(detectRuntimePrerequisites());
230
+ const quiet = Boolean(options.quiet);
204
231
  if (template) {
205
- await runCommand(async () => cloneProjectTemplate(template, options.path, runtimeNextSteps));
232
+ await runCommand(async () => cloneProjectTemplate(template, options.path, runtimeNextSteps, { quiet }));
206
233
  return;
207
234
  }
208
235
  await runCommand(async () => {
209
236
  let projectRoot = findProjectRoot();
210
237
  if (!projectRoot) {
211
238
  projectRoot = cwd();
212
- showStep("🚀 Initializing new Flisk project...\n");
213
- showDetail(`Location: ${projectRoot}\n`, 0);
239
+ if (!quiet) {
240
+ showStep("🚀 Initializing new Flisk project...\n");
241
+ showDetail(`Location: ${projectRoot}\n`, 0);
242
+ }
214
243
  }
215
244
  const tdkDir = join(projectRoot, ".tdk");
216
245
  const projectJsonPath = join(tdkDir, PROJECT_JSON);
@@ -237,15 +266,19 @@ export const projectCommand = addYesOption(new Command("project"))
237
266
  process.exit(1);
238
267
  }
239
268
  }
240
- showCommandHeader("Project Configuration");
241
- showDetail(`Project root: ${projectRoot}\n`, 0);
269
+ if (!quiet) {
270
+ showCommandHeader("Project Configuration");
271
+ showDetail(`Project root: ${projectRoot}\n`, 0);
272
+ }
242
273
  if (!existsSync(tdkDir)) {
243
274
  ensureDirectory(tdkDir);
244
- showSuccess("Created: .tdk/ directory");
275
+ if (!quiet)
276
+ showSuccess("Created: .tdk/ directory");
245
277
  }
246
278
  const projectJsonExists = existsSync(projectJsonPath);
247
279
  if (projectJsonExists && !options.force) {
248
- showSuccess(".tdk/project.json exists");
280
+ if (!quiet)
281
+ showSuccess(".tdk/project.json exists");
249
282
  const rawProjectConfig = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
250
283
  const projectConfig = readProjectConfig(projectRoot);
251
284
  const discoveredStacks = discoverExistingServiceStacks(projectRoot);
@@ -254,32 +287,36 @@ export const projectCommand = addYesOption(new Command("project"))
254
287
  if (shouldRewriteProjectConfig) {
255
288
  writeJsonFile(projectJsonPath, projectConfig);
256
289
  }
257
- if (autoEnabledStacks.length > 0) {
290
+ if (autoEnabledStacks.length > 0 && !quiet) {
258
291
  showDetail(`Auto-enabled discovered service stacks: ${autoEnabledStacks.join(", ")}`, 0);
259
292
  }
260
- showStep("\n📋 Regenerating master configuration files...\n");
261
- await generateMasterConfigs(projectRoot);
293
+ if (!quiet)
294
+ showStep("\n📋 Regenerating master configuration files...\n");
295
+ await generateMasterConfigs(projectRoot, { quiet });
262
296
  if (ensureEnvFile(projectRoot)) {
263
- showSuccess("Created: .env (the defaults work as-is)");
297
+ if (!quiet)
298
+ showSuccess("Created: .env (the defaults work as-is)");
264
299
  }
265
300
  else {
266
301
  const added = completeEnvFile(projectRoot);
267
- if (added.length > 0) {
302
+ if (added.length > 0 && !quiet) {
268
303
  showSuccess(`Updated: .env (added ${added.join(", ")}; existing values unchanged)`);
269
304
  }
270
305
  }
271
306
  const ignored = ensureGitignore(projectRoot);
272
- if (ignored.length > 0) {
307
+ if (ignored.length > 0 && !quiet) {
273
308
  showSuccess(`Added to .gitignore: ${ignored.join(", ")}`);
274
309
  }
275
- console.log(chalk.green("\n✅ Project configuration regenerated!"));
276
- showDetail("\nGenerated in .tdk/.tdk-out/:", 0);
277
- for (const file of MASTER_CONFIG_FILES) {
278
- showDetail(`- ${file}`);
310
+ if (!quiet) {
311
+ console.log(chalk.green("\n✅ Project configuration regenerated!"));
312
+ showDetail("\nGenerated in .tdk/.tdk-out/:", 0);
313
+ for (const file of MASTER_CONFIG_FILES) {
314
+ showDetail(`- ${file}`);
315
+ }
316
+ showDetail("\nRuntime readiness:", 0);
317
+ for (const line of runtimeNextSteps)
318
+ showDetail(line, 0);
279
319
  }
280
- showDetail("\nRuntime readiness:", 0);
281
- for (const line of runtimeNextSteps)
282
- showDetail(line, 0);
283
320
  return;
284
321
  }
285
322
  if (projectJsonExists && options.force && !options.yes) {
@@ -302,12 +339,14 @@ export const projectCommand = addYesOption(new Command("project"))
302
339
  }
303
340
  const configContent = readFileSync(configFilePath, "utf-8");
304
341
  projectConfig = JSON.parse(configContent);
305
- showSuccess(`Loaded config from: ${configFilePath}`);
342
+ if (!quiet)
343
+ showSuccess(`Loaded config from: ${configFilePath}`);
306
344
  }
307
345
  else if (options.yes) {
308
346
  projectConfig = JSON.parse(JSON.stringify(DEFAULT_PROJECT_JSON));
309
347
  projectConfig.project.name = projectRoot.split("/").pop() || "my-project";
310
- console.log(chalk.gray("Using default configuration (non-interactive mode)"));
348
+ if (!quiet)
349
+ console.log(chalk.gray("Using default configuration (non-interactive mode)"));
311
350
  }
312
351
  else {
313
352
  showStep("📝 Project Setup Wizard\n");
@@ -399,27 +438,34 @@ export const projectCommand = addYesOption(new Command("project"))
399
438
  : DEFAULT_PROJECT_JSON.discovery.paths;
400
439
  }
401
440
  const autoEnabledStacks = syncDiscoveredStacksToPreAlpha(projectConfig, discoveredStacks);
402
- if (autoEnabledStacks.length > 0) {
441
+ if (autoEnabledStacks.length > 0 && !quiet) {
403
442
  showDetail(`Auto-enabled discovered service stacks: ${autoEnabledStacks.join(", ")}`, 0);
404
443
  }
405
- showStep("\n📋 Creating project configuration...\n");
444
+ if (!quiet)
445
+ showStep("\n📋 Creating project configuration...\n");
406
446
  writeJsonFile(projectJsonPath, projectConfig);
407
- showSuccess("Created: .tdk/project.json");
408
- showDetail(`→ Project: ${projectConfig.project.name}`);
409
- showStep("\n📋 Generating master configuration files...\n");
410
- await generateMasterConfigs(projectRoot);
411
- showStep("\n🔧 Setting up environment variables...\n");
447
+ if (!quiet) {
448
+ showSuccess("Created: .tdk/project.json");
449
+ showDetail(`→ Project: ${projectConfig.project.name}`);
450
+ showStep("\n📋 Generating master configuration files...\n");
451
+ }
452
+ await generateMasterConfigs(projectRoot, { quiet });
453
+ if (!quiet)
454
+ showStep("\n🔧 Setting up environment variables...\n");
412
455
  const envCreated = ensureEnvFile(projectRoot);
413
456
  if (envCreated) {
414
- showSuccess("Created: .env (the defaults work as-is)");
457
+ if (!quiet)
458
+ showSuccess("Created: .env (the defaults work as-is)");
415
459
  }
416
460
  else {
417
461
  // An .env from an older CLI lacks keys a newer one expects; add them without changing anything already there.
418
462
  const added = completeEnvFile(projectRoot);
419
463
  if (added.length > 0) {
420
- showSuccess(`Updated: .env (added ${added.join(", ")}; existing values unchanged)`);
464
+ if (!quiet) {
465
+ showSuccess(`Updated: .env (added ${added.join(", ")}; existing values unchanged)`);
466
+ }
421
467
  }
422
- else {
468
+ else if (!quiet) {
423
469
  showSuccess("Found: .env (environment already configured)");
424
470
  }
425
471
  }
@@ -432,24 +478,26 @@ export const projectCommand = addYesOption(new Command("project"))
432
478
  showDetail("→ Edit .env and fill in these values before running `flisk up`", 1);
433
479
  }
434
480
  const ignored = ensureGitignore(projectRoot);
435
- if (ignored.length > 0) {
481
+ if (ignored.length > 0 && !quiet) {
436
482
  showSuccess(`Added to .gitignore: ${ignored.join(", ")}`);
437
483
  }
438
- console.log(chalk.green("\n✅ Project configuration complete!"));
439
- showDetail("\nGenerated files in .tdk/.tdk-out/:", 0);
440
- for (const file of MASTER_CONFIG_FILES) {
441
- showDetail(`- ${file}`);
484
+ if (!quiet) {
485
+ console.log(chalk.green("\n✅ Project configuration complete!"));
486
+ showDetail("\nGenerated files in .tdk/.tdk-out/:", 0);
487
+ for (const file of MASTER_CONFIG_FILES) {
488
+ showDetail(`- ${file}`);
489
+ }
490
+ showDetail("\nEnvironment configuration:", 0);
491
+ showDetail("- .env (contains all required variables)");
492
+ showDetail("\nSource file:", 0);
493
+ showDetail("- .tdk/project.json (edit this to change project structure)");
494
+ showDetail("\nNext steps:", 0);
495
+ showDetail("1. flisk resource api --type backend --stack app # scaffold a service");
496
+ showDetail("2. flisk up app # start it");
497
+ showDetail("\nRuntime readiness:", 0);
498
+ for (const line of runtimeNextSteps)
499
+ showDetail(line, 0);
500
+ showDetail("\nAfter editing .tdk/project.json, run `flisk config regenerate`.", 0);
442
501
  }
443
- showDetail("\nEnvironment configuration:", 0);
444
- showDetail("- .env (contains all required variables)");
445
- showDetail("\nSource file:", 0);
446
- showDetail("- .tdk/project.json (edit this to change project structure)");
447
- showDetail("\nNext steps:", 0);
448
- showDetail("1. flisk resource api --type backend --stack app # scaffold a service");
449
- showDetail("2. flisk up app # start it");
450
- showDetail("\nRuntime readiness:", 0);
451
- for (const line of runtimeNextSteps)
452
- showDetail(line, 0);
453
- showDetail("\nAfter editing .tdk/project.json, run `flisk config regenerate`.", 0);
454
502
  });
455
503
  });
@@ -214,7 +214,9 @@ export function createPackageJson(name, type, frameworkId, prismaEnabled = false
214
214
  // image's `bun run build` fails with TS2688. ^20 matches the services that build.
215
215
  "@types/node": "^20.0.0",
216
216
  typescript: "^7.0.2",
217
- vitest: "^5.0.0",
217
+ // The same version the CLI tests itself with (cli/package.json devDependencies.vitest).
218
+ // Dependabot pins it exactly to keep CVE-2026-112950 fixed, and the scaffold follows.
219
+ vitest: "5.0.5",
218
220
  "@biomejs/biome": "^2.5.13",
219
221
  // vitest 5 peers on vite ^6.4.0 || ^7 || ^8; vite 5 makes `vitest run` fail at startup with
220
222
  // ERR_PACKAGE_PATH_NOT_EXPORTED ('./module-runner'), in every resource once it is hoisted.
@@ -19,6 +19,7 @@ import { exportHostPortPlan, getHostPortPlan, writeSavedHostPortPlan, } from "..
19
19
  import { formatHostPortPlan } from "../utils/host-port-plan.js";
20
20
  import { createJsonEmitter } from "../utils/json-output.js";
21
21
  import { toMachineError } from "../utils/machine-output.js";
22
+ import { nativeWindowsStackRefusal } from "../utils/native-windows.js";
22
23
  import { findProjectRoot, getPackageVersion } from "../utils/paths.js";
23
24
  import { findAvailablePort } from "../utils/port-assignment.js";
24
25
  import { formatPortFallbackNotice } from "../utils/port-fallback-notice.js";
@@ -48,11 +49,13 @@ export function formatUpSuccess(port, appUrls = []) {
48
49
  "Stop: flisk down",
49
50
  ];
50
51
  }
51
- export function nativeWindowsUpRefusal(platform, allowNativeWindows) {
52
- if (platform !== "win32" || allowNativeWindows === "1")
53
- return null;
54
- return "Landscape startup needs Ubuntu on WSL2. Native Windows is inspect-only.";
55
- }
52
+ /**
53
+ * The native Windows boundary for a command that starts a stack.
54
+ *
55
+ * It lives here because `flisk up` is where the boundary has always been enforced; the
56
+ * one-command demo flow refuses with the same message before it clones or installs anything.
57
+ */
58
+ export const nativeWindowsUpRefusal = nativeWindowsStackRefusal;
56
59
  /** Why `flisk up` cannot work on this host, or null when it may proceed. Docker reachability is only probed in container hosts. */
57
60
  export async function hostUpRefusal(host = detectHost(), dockerReachable = defaultDockerReachable) {
58
61
  if (host.kind === "webcontainer")
@@ -503,7 +503,8 @@ export async function generateMasterConfigs(projectRoot, options = {}) {
503
503
  if (fs.existsSync(tiltignoreSource)) {
504
504
  if (!fs.existsSync(tiltignoreTarget)) {
505
505
  fs.copyFileSync(tiltignoreSource, tiltignoreTarget);
506
- console.log(`✓ Copied: .tiltignore → project root`);
506
+ if (!options.quiet)
507
+ console.log(`✓ Copied: .tiltignore → project root`);
507
508
  }
508
509
  else if (options.discardHandEdits) {
509
510
  // Only a file Flisk wrote (it carries the header) may be restored; one without the
@@ -512,17 +513,23 @@ export async function generateMasterConfigs(projectRoot, options = {}) {
512
513
  if (current.includes(TDK_GENERATED_MARKER) &&
513
514
  current !== fs.readFileSync(tiltignoreSource, "utf-8")) {
514
515
  fs.copyFileSync(tiltignoreSource, tiltignoreTarget);
515
- console.log("✓ Restored: .tiltignore → project root (hand edits discarded)");
516
+ if (!options.quiet) {
517
+ console.log("✓ Restored: .tiltignore → project root (hand edits discarded)");
518
+ }
516
519
  }
517
520
  }
518
521
  }
519
522
  const copiedAssets = ensureProjectRuntimeAssets(projectRoot);
520
- for (const asset of copiedAssets) {
521
- console.log(`✓ Copied: ${asset} → project root`);
523
+ if (!options.quiet) {
524
+ for (const asset of copiedAssets) {
525
+ console.log(`✓ Copied: ${asset} → project root`);
526
+ }
527
+ }
528
+ await vendorTdkExtension(projectRoot, { quiet: options.quiet });
529
+ if (!options.quiet) {
530
+ console.log("");
531
+ console.log("💡 To start Tilt: flisk up");
522
532
  }
523
- await vendorTdkExtension(projectRoot);
524
- console.log("");
525
- console.log("💡 To start Tilt: flisk up");
526
533
  }
527
534
  // The generated Tiltfile loads the Flisk Tilt extension (engine, discovery,
528
535
  // specs, ext starlark modules). The extension repo may stay private, so we
@@ -618,7 +625,7 @@ export function ensureProjectRuntimeAssets(projectRoot) {
618
625
  }
619
626
  return copied;
620
627
  }
621
- async function vendorTdkExtension(projectRoot) {
628
+ async function vendorTdkExtension(projectRoot, options = {}) {
622
629
  const vendoredDir = assertTdkGeneratedPath(projectRoot, path.join(".tdk", ".tdk-out", "tdk-cli-ext"));
623
630
  // Source resolution order:
624
631
  // 1. $TDK_EXTENSION_SOURCE (explicit override)
@@ -669,7 +676,8 @@ async function vendorTdkExtension(projectRoot) {
669
676
  filter: (source) => !isTestFile(source),
670
677
  });
671
678
  }
672
- console.log(`✓ Vendored Flisk extension → .tdk/.tdk-out/tdk-cli-ext/`);
679
+ if (!options.quiet)
680
+ console.log(`✓ Vendored Flisk extension → .tdk/.tdk-out/tdk-cli-ext/`);
673
681
  // Best-effort: if TDK_LICENSE_KEY is set and grants any paid resource,
674
682
  // overlay the premium bundle over the stub files just vendored above.
675
683
  // No-ops silently (beyond a warning) if there's no key, the fetch
@@ -107,7 +107,9 @@ export function ensureGlobalInstall(env = process.env) {
107
107
  // Not installed under the old name, or the manager is missing.
108
108
  }
109
109
  try {
110
- execFileSync(manager, ["install", "-g", CLI_PACKAGE], { stdio: "inherit", timeout: 300000 });
110
+ // Silent on purpose: the caller owns the screen, and npm's install banner would paint
111
+ // over whatever it is showing. Progress is reported by the caller, not by npm.
112
+ execFileSync(manager, ["install", "-g", CLI_PACKAGE], { stdio: "ignore", timeout: 300000 });
111
113
  return "installed";
112
114
  }
113
115
  catch {
@@ -114,10 +114,13 @@ export function checkColima(deps = defaultDeps()) {
114
114
  export function planColimaRepair(status, dockerAnswers) {
115
115
  if (!status.isColima)
116
116
  return undefined;
117
+ // A Lima that is an x86_64 build on Apple Silicon, or a colima CLI that cannot see the VM because of it,
118
+ // needs a native limactl before anything else can start.
119
+ if (status.problem && /x86_64|rosetta|native arch|cannot see it/i.test(status.problem)) {
120
+ return "native-lima";
121
+ }
117
122
  if (!status.running)
118
123
  return "start";
119
- if (status.problem?.includes("cannot see it"))
120
- return "native-lima";
121
124
  if (!dockerAnswers)
122
125
  return "restart";
123
126
  return undefined;
@@ -125,15 +128,31 @@ export function planColimaRepair(status, dockerAnswers) {
125
128
  /** Runs a colima command with a native limactl first on PATH, when one is installed. */
126
129
  export function nativeLimaPath(env = process.env) {
127
130
  const home = env.HOME ?? "";
131
+ // Prefer the Apple Silicon Homebrew location first; an x86_64 limactl must never be treated as native.
128
132
  const candidates = [
133
+ "/opt/homebrew/bin",
129
134
  home ? `${home}/.local/bin` : "",
130
135
  home ? `${home}/bin` : "",
131
- "/opt/homebrew/bin",
132
136
  ].filter(Boolean);
133
137
  for (const dir of candidates) {
134
138
  const candidate = join(dir, "limactl");
135
139
  try {
136
- if (existsSync(candidate))
140
+ if (!existsSync(candidate))
141
+ continue;
142
+ // Verify the binary is actually a native (arm64/aarch64) build, not a Rosetta x86_64 one.
143
+ let archOutput = "";
144
+ try {
145
+ archOutput = execFileSync("file", ["-bL", candidate], {
146
+ encoding: "utf-8",
147
+ timeout: 5_000,
148
+ stdio: ["ignore", "pipe", "ignore"],
149
+ });
150
+ }
151
+ catch {
152
+ // `file` unavailable: fall back to accepting the candidate.
153
+ return dir;
154
+ }
155
+ if (/arm64|aarch64/i.test(archOutput))
137
156
  return dir;
138
157
  }
139
158
  catch {