@flisk-sh/flisk 1.3.205 → 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 (48) hide show
  1. package/cli/dist/cli.js +230 -92
  2. package/cli/dist/commands/doctor.js +29 -15
  3. package/cli/dist/commands/project.js +104 -58
  4. package/cli/dist/commands/up.js +8 -5
  5. package/cli/dist/generator/template-engine.js +17 -9
  6. package/cli/dist/utils/cli-command.js +3 -1
  7. package/cli/dist/utils/demo-stage.js +315 -0
  8. package/cli/dist/utils/native-windows.js +20 -0
  9. package/cli/dist/utils/one-command.js +15 -8
  10. package/cli/dist-bundle/chunk-3QQGL3NQ.js +35 -0
  11. package/cli/dist-bundle/chunk-4W6OXDLK.js +17 -0
  12. package/cli/dist-bundle/chunk-53M5D4KE.js +2 -0
  13. package/cli/dist-bundle/{chunk-4TMLNLYG.js → chunk-5CFVN3W3.js} +1 -1
  14. package/cli/dist-bundle/{chunk-P2MFLNY2.js → chunk-6Y6G7PSD.js} +1 -1
  15. package/cli/dist-bundle/chunk-7C5VA6CW.js +57 -0
  16. package/cli/dist-bundle/chunk-D2BXXJDM.js +7 -0
  17. package/cli/dist-bundle/chunk-GANRXKBM.js +2 -0
  18. package/cli/dist-bundle/chunk-GOEBYLFN.js +2 -0
  19. package/cli/dist-bundle/chunk-LI3BXAX5.js +2 -0
  20. package/cli/dist-bundle/{chunk-BHQDQQYQ.js → chunk-NCEK46NX.js} +10 -10
  21. package/cli/dist-bundle/chunk-ODVBKN6S.js +2 -0
  22. package/cli/dist-bundle/{chunk-5G7E6CO2.js → chunk-PHI5AT3H.js} +2 -2
  23. package/cli/dist-bundle/chunk-XSPMLWAE.js +10 -0
  24. package/cli/dist-bundle/cli.js +265 -263
  25. package/cli/dist-bundle/cold-preflight-4I24DQLW.js +2 -0
  26. package/cli/dist-bundle/demo-stage-EOMANLAD.js +7 -0
  27. package/cli/dist-bundle/native-windows-X5XVLFJS.js +2 -0
  28. package/cli/dist-bundle/one-command-P6XVAXWV.js +5 -0
  29. package/cli/dist-bundle/open-6374RNL6.js +2 -0
  30. package/cli/dist-bundle/project-IRWFRXWA.js +2 -0
  31. package/cli/dist-bundle/services-VIIBMWD4.js +2 -0
  32. package/cli/dist-bundle/tilt-port-owner-AN3ITRTR.js +2 -0
  33. package/cli/dist-bundle/url-actions-D6ROTHZG.js +2 -0
  34. package/cli/package.json +1 -1
  35. package/package.json +1 -1
  36. package/cli/dist-bundle/chunk-67M3DX6J.js +0 -34
  37. package/cli/dist-bundle/chunk-CF46CD6R.js +0 -6
  38. package/cli/dist-bundle/chunk-CLQSJP6M.js +0 -21
  39. package/cli/dist-bundle/chunk-DTIHDMED.js +0 -5
  40. package/cli/dist-bundle/chunk-IQNUVQH5.js +0 -52
  41. package/cli/dist-bundle/chunk-WVSI4XL5.js +0 -4
  42. package/cli/dist-bundle/chunk-YNJTA77W.js +0 -2
  43. package/cli/dist-bundle/cold-preflight-R53YXYOC.js +0 -2
  44. package/cli/dist-bundle/one-command-YP6L6MMM.js +0 -5
  45. package/cli/dist-bundle/open-PKNOQQXA.js +0 -2
  46. package/cli/dist-bundle/project-I5QVEGNB.js +0 -2
  47. package/cli/dist-bundle/spinner-YLB6FYQH.js +0 -2
  48. package/cli/dist-bundle/tilt-port-owner-Y5LHXWPS.js +0 -2
@@ -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}"`));
@@ -107,10 +115,12 @@ export async function cloneProjectTemplate(templateName, targetDir, runtimeNextS
107
115
  if (existsSync(destination) && readdirSync(destination).length > 0) {
108
116
  errorFactories.directoryExists(destination).exit();
109
117
  }
110
- showCommandHeader(`Cloning template: ${templateName}`);
111
- showDetail(`${template.description}\n`, 0);
112
- showDetail(`Source: ${template.bundledPath ? "bundled Flisk example" : template.repo}`, 0);
113
- 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
+ }
114
124
  try {
115
125
  if (template.bundledPath) {
116
126
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../../");
@@ -129,15 +139,28 @@ export async function cloneProjectTemplate(templateName, targetDir, runtimeNextS
129
139
  }
130
140
  }
131
141
  else {
132
- 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
+ });
133
148
  }
134
149
  }
135
- catch {
136
- showErrorAndExit(template.bundledPath
137
- ? "Bundled example copy failed. Reinstall the Flisk CLI package and try again."
138
- : "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);
139
160
  }
140
161
  clearDiscoveryCache();
162
+ if (options.quiet)
163
+ return;
141
164
  console.log(chalk.green(`\n✅ Cloned "${templateName}" into ${dirName}/`));
142
165
  showDetail("\nNext steps:", 0);
143
166
  showDetail(`1. cd ${dirName}`);
@@ -197,22 +220,26 @@ export const projectCommand = addYesOption(new Command("project"))
197
220
  .option("--check", "Check if master configs exist and are in sync")
198
221
  .option("--force", "Overwrite existing configuration (dangerous)")
199
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)
200
224
  .action(async (template, options) => {
201
225
  const { assertMachineReadyOrExit, detectRuntimePrerequisites, formatRuntimeNextSteps } = await import("../utils/cold-preflight.js");
202
226
  // Project setup only writes files: it must not need a running Docker daemon
203
227
  // or an installed Tilt. The runtime is checked on `flisk up`.
204
228
  await assertMachineReadyOrExit({ scope: "scaffold" });
205
229
  const runtimeNextSteps = formatRuntimeNextSteps(detectRuntimePrerequisites());
230
+ const quiet = Boolean(options.quiet);
206
231
  if (template) {
207
- await runCommand(async () => cloneProjectTemplate(template, options.path, runtimeNextSteps));
232
+ await runCommand(async () => cloneProjectTemplate(template, options.path, runtimeNextSteps, { quiet }));
208
233
  return;
209
234
  }
210
235
  await runCommand(async () => {
211
236
  let projectRoot = findProjectRoot();
212
237
  if (!projectRoot) {
213
238
  projectRoot = cwd();
214
- showStep("🚀 Initializing new Flisk project...\n");
215
- showDetail(`Location: ${projectRoot}\n`, 0);
239
+ if (!quiet) {
240
+ showStep("🚀 Initializing new Flisk project...\n");
241
+ showDetail(`Location: ${projectRoot}\n`, 0);
242
+ }
216
243
  }
217
244
  const tdkDir = join(projectRoot, ".tdk");
218
245
  const projectJsonPath = join(tdkDir, PROJECT_JSON);
@@ -239,15 +266,19 @@ export const projectCommand = addYesOption(new Command("project"))
239
266
  process.exit(1);
240
267
  }
241
268
  }
242
- showCommandHeader("Project Configuration");
243
- showDetail(`Project root: ${projectRoot}\n`, 0);
269
+ if (!quiet) {
270
+ showCommandHeader("Project Configuration");
271
+ showDetail(`Project root: ${projectRoot}\n`, 0);
272
+ }
244
273
  if (!existsSync(tdkDir)) {
245
274
  ensureDirectory(tdkDir);
246
- showSuccess("Created: .tdk/ directory");
275
+ if (!quiet)
276
+ showSuccess("Created: .tdk/ directory");
247
277
  }
248
278
  const projectJsonExists = existsSync(projectJsonPath);
249
279
  if (projectJsonExists && !options.force) {
250
- showSuccess(".tdk/project.json exists");
280
+ if (!quiet)
281
+ showSuccess(".tdk/project.json exists");
251
282
  const rawProjectConfig = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
252
283
  const projectConfig = readProjectConfig(projectRoot);
253
284
  const discoveredStacks = discoverExistingServiceStacks(projectRoot);
@@ -256,32 +287,36 @@ export const projectCommand = addYesOption(new Command("project"))
256
287
  if (shouldRewriteProjectConfig) {
257
288
  writeJsonFile(projectJsonPath, projectConfig);
258
289
  }
259
- if (autoEnabledStacks.length > 0) {
290
+ if (autoEnabledStacks.length > 0 && !quiet) {
260
291
  showDetail(`Auto-enabled discovered service stacks: ${autoEnabledStacks.join(", ")}`, 0);
261
292
  }
262
- showStep("\n📋 Regenerating master configuration files...\n");
263
- await generateMasterConfigs(projectRoot);
293
+ if (!quiet)
294
+ showStep("\n📋 Regenerating master configuration files...\n");
295
+ await generateMasterConfigs(projectRoot, { quiet });
264
296
  if (ensureEnvFile(projectRoot)) {
265
- showSuccess("Created: .env (the defaults work as-is)");
297
+ if (!quiet)
298
+ showSuccess("Created: .env (the defaults work as-is)");
266
299
  }
267
300
  else {
268
301
  const added = completeEnvFile(projectRoot);
269
- if (added.length > 0) {
302
+ if (added.length > 0 && !quiet) {
270
303
  showSuccess(`Updated: .env (added ${added.join(", ")}; existing values unchanged)`);
271
304
  }
272
305
  }
273
306
  const ignored = ensureGitignore(projectRoot);
274
- if (ignored.length > 0) {
307
+ if (ignored.length > 0 && !quiet) {
275
308
  showSuccess(`Added to .gitignore: ${ignored.join(", ")}`);
276
309
  }
277
- console.log(chalk.green("\n✅ Project configuration regenerated!"));
278
- showDetail("\nGenerated in .tdk/.tdk-out/:", 0);
279
- for (const file of MASTER_CONFIG_FILES) {
280
- 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);
281
319
  }
282
- showDetail("\nRuntime readiness:", 0);
283
- for (const line of runtimeNextSteps)
284
- showDetail(line, 0);
285
320
  return;
286
321
  }
287
322
  if (projectJsonExists && options.force && !options.yes) {
@@ -304,12 +339,14 @@ export const projectCommand = addYesOption(new Command("project"))
304
339
  }
305
340
  const configContent = readFileSync(configFilePath, "utf-8");
306
341
  projectConfig = JSON.parse(configContent);
307
- showSuccess(`Loaded config from: ${configFilePath}`);
342
+ if (!quiet)
343
+ showSuccess(`Loaded config from: ${configFilePath}`);
308
344
  }
309
345
  else if (options.yes) {
310
346
  projectConfig = JSON.parse(JSON.stringify(DEFAULT_PROJECT_JSON));
311
347
  projectConfig.project.name = projectRoot.split("/").pop() || "my-project";
312
- console.log(chalk.gray("Using default configuration (non-interactive mode)"));
348
+ if (!quiet)
349
+ console.log(chalk.gray("Using default configuration (non-interactive mode)"));
313
350
  }
314
351
  else {
315
352
  showStep("📝 Project Setup Wizard\n");
@@ -401,27 +438,34 @@ export const projectCommand = addYesOption(new Command("project"))
401
438
  : DEFAULT_PROJECT_JSON.discovery.paths;
402
439
  }
403
440
  const autoEnabledStacks = syncDiscoveredStacksToPreAlpha(projectConfig, discoveredStacks);
404
- if (autoEnabledStacks.length > 0) {
441
+ if (autoEnabledStacks.length > 0 && !quiet) {
405
442
  showDetail(`Auto-enabled discovered service stacks: ${autoEnabledStacks.join(", ")}`, 0);
406
443
  }
407
- showStep("\n📋 Creating project configuration...\n");
444
+ if (!quiet)
445
+ showStep("\n📋 Creating project configuration...\n");
408
446
  writeJsonFile(projectJsonPath, projectConfig);
409
- showSuccess("Created: .tdk/project.json");
410
- showDetail(`→ Project: ${projectConfig.project.name}`);
411
- showStep("\n📋 Generating master configuration files...\n");
412
- await generateMasterConfigs(projectRoot);
413
- 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");
414
455
  const envCreated = ensureEnvFile(projectRoot);
415
456
  if (envCreated) {
416
- showSuccess("Created: .env (the defaults work as-is)");
457
+ if (!quiet)
458
+ showSuccess("Created: .env (the defaults work as-is)");
417
459
  }
418
460
  else {
419
461
  // An .env from an older CLI lacks keys a newer one expects; add them without changing anything already there.
420
462
  const added = completeEnvFile(projectRoot);
421
463
  if (added.length > 0) {
422
- showSuccess(`Updated: .env (added ${added.join(", ")}; existing values unchanged)`);
464
+ if (!quiet) {
465
+ showSuccess(`Updated: .env (added ${added.join(", ")}; existing values unchanged)`);
466
+ }
423
467
  }
424
- else {
468
+ else if (!quiet) {
425
469
  showSuccess("Found: .env (environment already configured)");
426
470
  }
427
471
  }
@@ -434,24 +478,26 @@ export const projectCommand = addYesOption(new Command("project"))
434
478
  showDetail("→ Edit .env and fill in these values before running `flisk up`", 1);
435
479
  }
436
480
  const ignored = ensureGitignore(projectRoot);
437
- if (ignored.length > 0) {
481
+ if (ignored.length > 0 && !quiet) {
438
482
  showSuccess(`Added to .gitignore: ${ignored.join(", ")}`);
439
483
  }
440
- console.log(chalk.green("\n✅ Project configuration complete!"));
441
- showDetail("\nGenerated files in .tdk/.tdk-out/:", 0);
442
- for (const file of MASTER_CONFIG_FILES) {
443
- 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);
444
501
  }
445
- showDetail("\nEnvironment configuration:", 0);
446
- showDetail("- .env (contains all required variables)");
447
- showDetail("\nSource file:", 0);
448
- showDetail("- .tdk/project.json (edit this to change project structure)");
449
- showDetail("\nNext steps:", 0);
450
- showDetail("1. flisk resource api --type backend --stack app # scaffold a service");
451
- showDetail("2. flisk up app # start it");
452
- showDetail("\nRuntime readiness:", 0);
453
- for (const line of runtimeNextSteps)
454
- showDetail(line, 0);
455
- showDetail("\nAfter editing .tdk/project.json, run `flisk config regenerate`.", 0);
456
502
  });
457
503
  });
@@ -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 {