everything-dev 1.17.0 → 1.20.0

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 (52) hide show
  1. package/dist/cli/init.cjs +165 -84
  2. package/dist/cli/init.cjs.map +1 -1
  3. package/dist/cli/init.d.cts +14 -2
  4. package/dist/cli/init.d.cts.map +1 -1
  5. package/dist/cli/init.d.mts +14 -2
  6. package/dist/cli/init.d.mts.map +1 -1
  7. package/dist/cli/init.mjs +164 -85
  8. package/dist/cli/init.mjs.map +1 -1
  9. package/dist/cli/prompts.cjs +12 -14
  10. package/dist/cli/prompts.cjs.map +1 -1
  11. package/dist/cli/prompts.mjs +12 -14
  12. package/dist/cli/prompts.mjs.map +1 -1
  13. package/dist/cli/timing.cjs +21 -1
  14. package/dist/cli/timing.cjs.map +1 -1
  15. package/dist/cli/timing.mjs +21 -1
  16. package/dist/cli/timing.mjs.map +1 -1
  17. package/dist/cli/upgrade.cjs +114 -49
  18. package/dist/cli/upgrade.cjs.map +1 -1
  19. package/dist/cli/upgrade.mjs +114 -49
  20. package/dist/cli/upgrade.mjs.map +1 -1
  21. package/dist/contract.cjs +1 -4
  22. package/dist/contract.cjs.map +1 -1
  23. package/dist/contract.d.cts +4 -10
  24. package/dist/contract.d.cts.map +1 -1
  25. package/dist/contract.d.mts +4 -10
  26. package/dist/contract.d.mts.map +1 -1
  27. package/dist/contract.meta.cjs +5 -5
  28. package/dist/contract.meta.cjs.map +1 -1
  29. package/dist/contract.meta.d.cts +9 -9
  30. package/dist/contract.meta.d.mts +9 -9
  31. package/dist/contract.meta.mjs +5 -5
  32. package/dist/contract.meta.mjs.map +1 -1
  33. package/dist/contract.mjs +1 -4
  34. package/dist/contract.mjs.map +1 -1
  35. package/dist/plugin.cjs +83 -119
  36. package/dist/plugin.cjs.map +1 -1
  37. package/dist/plugin.d.cts +3 -6
  38. package/dist/plugin.d.cts.map +1 -1
  39. package/dist/plugin.d.mts +3 -6
  40. package/dist/plugin.d.mts.map +1 -1
  41. package/dist/plugin.mjs +84 -120
  42. package/dist/plugin.mjs.map +1 -1
  43. package/dist/types.d.cts +2 -2
  44. package/dist/types.d.mts +2 -2
  45. package/package.json +5 -5
  46. package/src/cli/init.ts +224 -162
  47. package/src/cli/prompts.ts +17 -22
  48. package/src/cli/timing.ts +27 -0
  49. package/src/cli/upgrade.ts +173 -56
  50. package/src/contract.meta.ts +6 -8
  51. package/src/contract.ts +1 -4
  52. package/src/plugin.ts +189 -209
package/src/cli/init.ts CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  loadManifestNormalizationSpec,
21
21
  normalizePackageManifestsInTree,
22
22
  } from "../internal/manifest-normalizer";
23
- import type { BosConfig } from "../types";
23
+ import type { BosConfig, BosConfigInput } from "../types";
24
24
  import { isPathExcluded } from "../utils/path-match";
25
25
  import { saveBosConfig } from "../utils/save-config";
26
26
  import { writeSnapshot } from "./snapshot";
@@ -51,12 +51,25 @@ export async function resolveSourceDir(opts: {
51
51
 
52
52
  const parentConfig = await fetchParentConfig(opts.extendsAccount, opts.extendsGateway);
53
53
 
54
- if (!parentConfig.repository) {
55
- throw new Error("Parent config has no repository field — cannot locate template source");
54
+ if (parentConfig.repository) {
55
+ const { dir: sourceDir, cleanup } = await downloadTarball(parentConfig.repository);
56
+ return { sourceDir, parentConfig, cleanup };
56
57
  }
57
58
 
58
- const { dir: sourceDir, cleanup } = await downloadTarball(parentConfig.repository);
59
- return { sourceDir, parentConfig, cleanup };
59
+ const chainResult = await resolveRepositoryViaExtendsChain(
60
+ opts.extendsAccount,
61
+ opts.extendsGateway,
62
+ );
63
+ if (chainResult?.repository) {
64
+ const { dir: sourceDir, cleanup } = await downloadTarball(chainResult.repository);
65
+ return { sourceDir, parentConfig: chainResult.config, cleanup };
66
+ }
67
+
68
+ return {
69
+ sourceDir: "",
70
+ parentConfig,
71
+ cleanup: async () => {},
72
+ };
60
73
  }
61
74
 
62
75
  export async function readTemplatekeep(sourceDir: string): Promise<string[]> {
@@ -80,6 +93,37 @@ export async function fetchParentConfig(
80
93
  return fetchBosConfigFromFastKv<BosConfig>(bosUrl);
81
94
  }
82
95
 
96
+ export async function resolveRepositoryViaExtendsChain(
97
+ extendsAccount: string,
98
+ extendsGateway: string,
99
+ visited = new Set<string>(),
100
+ ): Promise<{ repository: string; config: BosConfig } | null> {
101
+ const key = `bos://${extendsAccount}/${extendsGateway}`;
102
+ if (visited.has(key)) return null;
103
+ visited.add(key);
104
+
105
+ try {
106
+ const config = await fetchParentConfig(extendsAccount, extendsGateway);
107
+ if (config.repository) {
108
+ return { repository: config.repository, config };
109
+ }
110
+
111
+ const extendsRef = config.extends;
112
+ if (extendsRef && typeof extendsRef === "string") {
113
+ const normalized = extendsRef.startsWith("bos://") ? extendsRef : `bos://${extendsRef}`;
114
+ const match = normalized.match(/^bos:\/\/([^/]+)\/(.+)$/);
115
+ if (match) {
116
+ const result = await resolveRepositoryViaExtendsChain(match[1], match[2], visited);
117
+ if (result) return result;
118
+ }
119
+ }
120
+
121
+ return null;
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+
83
127
  export async function downloadTarball(
84
128
  repoUrl: string,
85
129
  ): Promise<{ dir: string; cleanup: () => Promise<void> }> {
@@ -286,89 +330,21 @@ export async function personalizeConfig(
286
330
  }
287
331
  }
288
332
 
289
- if (isInit) {
290
- const parentDomain = opts.extendsGateway;
333
+ for (const pluginKey of Object.keys(plugins)) {
334
+ const plugin = plugins[pluginKey];
335
+ let pluginObj: Record<string, unknown>;
291
336
 
292
- for (const pluginKey of Object.keys(plugins)) {
293
- const plugin = plugins[pluginKey];
294
- let pluginObj: Record<string, unknown>;
295
-
296
- if (typeof plugin === "string") {
297
- pluginObj = { extends: plugin };
298
- plugins[pluginKey] = pluginObj;
299
- } else if (plugin && typeof plugin === "object") {
300
- pluginObj = { ...(plugin as Record<string, unknown>) };
301
- } else {
302
- continue;
303
- }
304
-
305
- if (
306
- pluginObj.development &&
307
- typeof pluginObj.development === "string" &&
308
- pluginObj.development.startsWith("local:")
309
- ) {
310
- const pluginDir = join(destination, pluginObj.development.slice("local:".length));
311
- const pluginConfigPath = join(pluginDir, "bos.config.json");
312
-
313
- if (existsSync(pluginConfigPath)) {
314
- try {
315
- const pluginConfig = JSON.parse(readFileSync(pluginConfigPath, "utf-8")) as Record<
316
- string,
317
- unknown
318
- >;
319
- const normalizedConfig = ensureLocalPluginProviderConfig(
320
- pluginConfig,
321
- pluginKey,
322
- `${pluginKey}.${opts.domain ?? parentDomain}`,
323
- pluginObj,
324
- opts.pluginRoutes,
325
- );
326
- if (stripProviderProductionFields(normalizedConfig)) {
327
- writeFileSync(pluginConfigPath, `${JSON.stringify(normalizedConfig, null, 2)}\n`);
328
- }
329
- } catch {}
330
- } else if (existsSync(pluginDir)) {
331
- const pluginConfig = ensureLocalPluginProviderConfig(
332
- {},
333
- pluginKey,
334
- `${pluginKey}.${opts.domain ?? parentDomain}`,
335
- pluginObj,
336
- opts.pluginRoutes,
337
- );
338
-
339
- mkdirSync(pluginDir, { recursive: true });
340
- writeFileSync(pluginConfigPath, `${JSON.stringify(pluginConfig, null, 2)}\n`);
341
- }
342
-
343
- plugins[pluginKey] = buildCleanPluginEntry(pluginObj);
344
- } else {
345
- delete pluginObj.production;
346
- delete pluginObj.integrity;
347
- delete pluginObj.sidebar;
348
- delete pluginObj.routes;
349
- }
350
- }
351
- } else {
352
- for (const pluginKey of Object.keys(plugins)) {
353
- const pluginDir = resolve(
354
- destination,
355
- (plugins[pluginKey] as Record<string, unknown>)?.development
356
- ?.toString()
357
- ?.slice("local:".length) ?? "",
358
- );
359
- const pluginConfigPath = join(pluginDir, "bos.config.json");
360
- if (!existsSync(pluginConfigPath)) continue;
361
-
362
- try {
363
- const pluginConfig = JSON.parse(readFileSync(pluginConfigPath, "utf-8")) as Record<
364
- string,
365
- unknown
366
- >;
367
- if (stripProviderProductionFields(pluginConfig)) {
368
- writeFileSync(pluginConfigPath, `${JSON.stringify(pluginConfig, null, 2)}\n`);
369
- }
370
- } catch {}
337
+ if (typeof plugin === "string") {
338
+ pluginObj = { extends: plugin };
339
+ plugins[pluginKey] = pluginObj;
340
+ } else if (plugin && typeof plugin === "object") {
341
+ pluginObj = { ...(plugin as Record<string, unknown>) };
342
+ } else {
343
+ continue;
371
344
  }
345
+
346
+ delete pluginObj.production;
347
+ delete pluginObj.integrity;
372
348
  }
373
349
 
374
350
  if (Object.keys(plugins).length === 0) {
@@ -506,82 +482,6 @@ export async function personalizeConfig(
506
482
  }
507
483
  }
508
484
 
509
- function stripProviderProductionFields(config: Record<string, unknown>): boolean {
510
- let changed = false;
511
-
512
- if ("extends" in config) {
513
- delete config.extends;
514
- changed = true;
515
- }
516
-
517
- if (config.plugins && typeof config.plugins === "object") {
518
- const pluginEntries = config.plugins as Record<string, unknown>;
519
- for (const entryKey of Object.keys(pluginEntries)) {
520
- const entry = pluginEntries[entryKey];
521
- if (!entry || typeof entry !== "object") continue;
522
- const normalizedEntry = entry as Record<string, unknown>;
523
- if ("production" in normalizedEntry) {
524
- delete normalizedEntry.production;
525
- changed = true;
526
- }
527
- if ("integrity" in normalizedEntry) {
528
- delete normalizedEntry.integrity;
529
- changed = true;
530
- }
531
- }
532
- }
533
-
534
- return changed;
535
- }
536
-
537
- function ensureLocalPluginProviderConfig(
538
- pluginConfig: Record<string, unknown>,
539
- pluginKey: string,
540
- domain: string,
541
- pluginObj: Record<string, unknown>,
542
- pluginRoutes?: Record<string, string[]>,
543
- ): Record<string, unknown> {
544
- pluginConfig.domain = domain;
545
- if (!pluginConfig.plugins || typeof pluginConfig.plugins !== "object") {
546
- pluginConfig.plugins = {};
547
- }
548
-
549
- const plugins = pluginConfig.plugins as Record<string, unknown>;
550
- if (!plugins[pluginKey] || typeof plugins[pluginKey] !== "object") {
551
- plugins[pluginKey] = {};
552
- }
553
-
554
- const pluginEntry = plugins[pluginKey] as Record<string, unknown>;
555
- if (!pluginEntry.name) {
556
- pluginEntry.name = pluginKey;
557
- }
558
- if (!pluginEntry.development) {
559
- pluginEntry.development = "local:.";
560
- }
561
- if (pluginRoutes?.[pluginKey]) {
562
- pluginEntry.routes = pluginRoutes[pluginKey];
563
- }
564
- if (pluginObj.sidebar) {
565
- pluginEntry.sidebar = pluginObj.sidebar;
566
- }
567
-
568
- return pluginConfig;
569
- }
570
-
571
- function buildCleanPluginEntry(pluginObj: Record<string, unknown>): Record<string, unknown> {
572
- const cleanEntry: Record<string, unknown> = { development: pluginObj.development };
573
- if (pluginObj.extends) {
574
- cleanEntry.extends = pluginObj.extends;
575
- }
576
- if (pluginObj.secrets) {
577
- cleanEntry.secrets = pluginObj.secrets;
578
- }
579
- if (pluginObj.variables) {
580
- cleanEntry.variables = pluginObj.variables;
581
- }
582
- return cleanEntry;
583
- }
584
-
585
485
  function generateAuthTypesTemplate(): string {
586
486
  return `import type { Auth } from "better-auth";
587
487
  export type { Auth } from "better-auth";
@@ -650,6 +550,121 @@ const WORKSPACE_LOCAL_PATHS: Record<string, string> = {
650
550
  "every-plugin": "packages/every-plugin",
651
551
  };
652
552
 
553
+ export async function scaffoldMinimalProject(
554
+ destination: string,
555
+ parentConfig: BosConfigInput,
556
+ opts: {
557
+ extendsAccount: string;
558
+ extendsGateway: string;
559
+ account?: string;
560
+ domain?: string;
561
+ plugins?: string[];
562
+ withHost?: boolean;
563
+ },
564
+ ): Promise<number> {
565
+ mkdirSync(destination, { recursive: true });
566
+
567
+ const config: Record<string, unknown> = {
568
+ extends: `bos://${opts.extendsAccount}/${opts.extendsGateway}`,
569
+ account: opts.account || opts.extendsAccount,
570
+ ...(opts.domain ? { domain: opts.domain } : {}),
571
+ };
572
+
573
+ if (parentConfig.app && typeof parentConfig.app === "object") {
574
+ const app: Record<string, unknown> = {};
575
+ const parentApp = parentConfig.app as Record<string, Record<string, unknown>>;
576
+
577
+ if (parentApp.host) {
578
+ app.host = { ...parentApp.host };
579
+ const host = app.host as Record<string, unknown>;
580
+ delete host.production;
581
+ delete host.integrity;
582
+ }
583
+
584
+ if (parentApp.ui) {
585
+ app.ui = { ...parentApp.ui };
586
+ const ui = app.ui as Record<string, unknown>;
587
+ delete ui.production;
588
+ delete ui.integrity;
589
+ delete ui.ssr;
590
+ delete ui.ssrIntegrity;
591
+ }
592
+
593
+ if (parentApp.api) {
594
+ app.api = { ...parentApp.api };
595
+ const api = app.api as Record<string, unknown>;
596
+ delete api.production;
597
+ delete api.integrity;
598
+ }
599
+
600
+ if (parentApp.auth) {
601
+ app.auth = { ...parentApp.auth };
602
+ const auth = app.auth as Record<string, unknown>;
603
+ delete auth.production;
604
+ delete auth.integrity;
605
+ }
606
+
607
+ config.app = app;
608
+ }
609
+
610
+ if (opts.plugins && opts.plugins.length > 0 && parentConfig.plugins) {
611
+ const plugins: Record<string, unknown> = {};
612
+ for (const key of opts.plugins) {
613
+ const parentPlugin = (parentConfig.plugins as Record<string, unknown>)?.[key];
614
+ if (parentPlugin) {
615
+ if (typeof parentPlugin === "string") {
616
+ plugins[key] = { extends: parentPlugin };
617
+ } else {
618
+ const pluginCopy = { ...(parentPlugin as Record<string, unknown>) };
619
+ delete pluginCopy.production;
620
+ delete pluginCopy.integrity;
621
+ plugins[key] = pluginCopy;
622
+ }
623
+ }
624
+ }
625
+ config.plugins = plugins;
626
+ }
627
+
628
+ await saveBosConfig(destination, config);
629
+
630
+ const pkg: Record<string, unknown> = {
631
+ name: opts.domain || opts.extendsGateway,
632
+ private: true,
633
+ type: "module",
634
+ scripts: {
635
+ dev: "node_modules/.bin/bos dev --host remote",
636
+ "dev:ui": "node_modules/.bin/bos dev --ui local --api remote",
637
+ "dev:api": "node_modules/.bin/bos dev --ui remote --api local",
638
+ build: "node_modules/.bin/bos build",
639
+ deploy: "node_modules/.bin/bos build --deploy",
640
+ publish: "node_modules/.bin/bos publish",
641
+ start: "node_modules/.bin/bos start",
642
+ typecheck: "node_modules/.bin/bos types gen && tsc --noEmit",
643
+ postinstall: "node_modules/.bin/bos types gen || true",
644
+ "types:gen": "node_modules/.bin/bos types gen",
645
+ },
646
+ dependencies: {
647
+ "everything-dev": "catalog:",
648
+ "every-plugin": "catalog:",
649
+ },
650
+ devDependencies: {},
651
+ workspaces: {
652
+ packages: [],
653
+ catalog: {},
654
+ },
655
+ };
656
+ writeFileSync(join(destination, "package.json"), `${JSON.stringify(pkg, null, 2)}\n`);
657
+
658
+ const envExample = generateEnvExample(parentConfig);
659
+ if (envExample) {
660
+ writeFileSync(join(destination, ".env.example"), envExample);
661
+ }
662
+
663
+ writeFileSync(join(destination, ".gitignore"), generateGitignore());
664
+
665
+ return 4;
666
+ }
667
+
653
668
  async function resolveWorkspaceRefs(
654
669
  destination: string,
655
670
  options?: { localOverrides?: boolean; sourceDir?: string },
@@ -801,3 +816,50 @@ export async function generateDatabaseMigrations(destination: string): Promise<v
801
816
  export async function execCommand(command: string, args: string[], cwd?: string): Promise<void> {
802
817
  await execa(command, args, { cwd, stdio: "pipe" });
803
818
  }
819
+
820
+ function generateEnvExample(config: BosConfigInput): string {
821
+ const lines: string[] = ["# Environment variables"];
822
+ const collectSecrets = (obj: Record<string, unknown>, prefix = ""): void => {
823
+ for (const [key, value] of Object.entries(obj)) {
824
+ if (key === "secrets" && Array.isArray(value)) {
825
+ for (const secret of value) {
826
+ if (typeof secret === "string") {
827
+ lines.push(`${secret}=`);
828
+ }
829
+ }
830
+ } else if (key === "variables" && isPlainObject(value)) {
831
+ for (const [varKey, varVal] of Object.entries(value as Record<string, unknown>)) {
832
+ if (typeof varVal === "string") {
833
+ lines.push(`${varKey}=${varVal}`);
834
+ }
835
+ }
836
+ } else if (isPlainObject(value) && key !== "extends") {
837
+ collectSecrets(value as Record<string, unknown>, `${prefix}${key}.`);
838
+ }
839
+ }
840
+ };
841
+
842
+ if (config.app && typeof config.app === "object") {
843
+ collectSecrets(config.app as Record<string, unknown>);
844
+ }
845
+ if (config.plugins && typeof config.plugins === "object") {
846
+ collectSecrets(config.plugins as Record<string, unknown>);
847
+ }
848
+
849
+ lines.push("BETTER_AUTH_SECRET=generate-a-secret-here");
850
+ return `${lines.join("\n")}\n`;
851
+ }
852
+
853
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
854
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
855
+ }
856
+
857
+ function generateGitignore(): string {
858
+ return `node_modules/
859
+ dist/
860
+ .env
861
+ .bos/
862
+ *.gen.ts
863
+ *.gen.tsx
864
+ `;
865
+ }
@@ -2,12 +2,13 @@ import process from "node:process";
2
2
  import * as p from "@clack/prompts";
3
3
 
4
4
  function parseExtendsRef(ref: string): { account: string; gateway: string } | null {
5
- const match = ref.match(/^(?:bos:\/\/)?([^/]+)\/(.+)$/);
5
+ const normalized = ref.startsWith("bos://") ? ref : `bos://${ref}`;
6
+ const match = normalized.match(/^bos:\/\/([^/]+)\/(.+)$/);
6
7
  if (!match) return null;
7
8
  return { account: match[1], gateway: match[2] };
8
9
  }
9
10
 
10
- function deriveAccountFromDomain(domain: string, extendsAccount: string): string {
11
+ function deriveAccountFromExtends(domain: string, extendsAccount: string): string {
11
12
  const firstSegment = domain.split(".")[0];
12
13
  if (!firstSegment) return "";
13
14
  const suffix = extendsAccount.includes(".")
@@ -17,8 +18,6 @@ function deriveAccountFromDomain(domain: string, extendsAccount: string): string
17
18
  }
18
19
 
19
20
  export async function promptInitOptions(input: {
20
- extendsAccount?: string;
21
- extendsGateway?: string;
22
21
  extends?: string;
23
22
  directory?: string;
24
23
  account?: string;
@@ -37,40 +36,36 @@ export async function promptInitOptions(input: {
37
36
  }> {
38
37
  p.intro("Let's build an app...");
39
38
 
40
- const domain =
41
- input.domain ??
42
- ((await p.text({
43
- message: "Starting with a domain?",
44
- placeholder: "no",
45
- })) as string);
46
-
47
- if (p.isCancel(domain)) process.exit(0);
48
-
49
- const extendsPlaceholder = "bos://dev.everything.near/everything.dev";
50
39
  const extendsInput =
51
40
  input.extends ??
52
41
  ((await p.text({
53
42
  message: "Extending an existing app?",
54
- placeholder: extendsPlaceholder,
43
+ placeholder: "bos://dev.everything.near/everything.dev",
55
44
  })) as string);
56
45
 
57
46
  if (p.isCancel(extendsInput)) process.exit(0);
58
47
 
59
- let extendsAccount = input.extendsAccount || "";
60
- let extendsGateway = input.extendsGateway || "";
48
+ let extendsAccount = "dev.everything.near";
49
+ let extendsGateway = "everything.dev";
61
50
 
62
51
  if (extendsInput) {
63
52
  const parsed = parseExtendsRef(extendsInput);
64
53
  if (parsed) {
65
- extendsAccount = extendsAccount || parsed.account;
66
- extendsGateway = extendsGateway || parsed.gateway;
54
+ extendsAccount = parsed.account;
55
+ extendsGateway = parsed.gateway;
67
56
  }
68
57
  }
69
58
 
70
- extendsAccount = extendsAccount || "dev.everything.near";
71
- extendsGateway = extendsGateway || "everything.dev";
59
+ const domain =
60
+ input.domain ??
61
+ ((await p.text({
62
+ message: "Starting with a domain?",
63
+ placeholder: "no",
64
+ })) as string);
65
+
66
+ if (p.isCancel(domain)) process.exit(0);
72
67
 
73
- const accountDefault = domain ? deriveAccountFromDomain(domain, extendsAccount) : "";
68
+ const accountDefault = domain ? deriveAccountFromExtends(domain, extendsAccount) : "";
74
69
  const account =
75
70
  input.account ??
76
71
  ((await p.text({
package/src/cli/timing.ts CHANGED
@@ -1,13 +1,40 @@
1
+ import type { spinner as clackSpinner } from "@clack/prompts";
2
+
3
+ type Spinner = ReturnType<typeof clackSpinner>;
4
+
1
5
  export interface PhaseTiming {
2
6
  name: string;
3
7
  durationMs: number;
4
8
  }
5
9
 
10
+ const PHASE_LABELS: Record<string, string> = {
11
+ "parent config": "Fetching parent config...",
12
+ "template source": "Resolving template source...",
13
+ "scaffold project": "Creating project scaffold...",
14
+ "copy files": "Copying template files...",
15
+ "personalize config": "Personalizing config...",
16
+ "write snapshot": "Writing snapshot...",
17
+ "resolve config": "Resolving config...",
18
+ "generate env/docker": "Generating environment config...",
19
+ "create env file": "Creating .env file...",
20
+ "install dependencies": "Installing dependencies...",
21
+ "generate types": "Generating types...",
22
+ "generate migrations": "Generating database migrations...",
23
+ "generate code artifacts": "Generating code artifacts...",
24
+ "docker compose up": "Starting Docker services...",
25
+ };
26
+
27
+ function phaseLabel(name: string): string {
28
+ return PHASE_LABELS[name] ?? name;
29
+ }
30
+
6
31
  export async function timePhase<T>(
7
32
  timings: PhaseTiming[],
8
33
  name: string,
9
34
  fn: () => Promise<T>,
35
+ spinner?: Spinner,
10
36
  ): Promise<T> {
37
+ spinner?.message(phaseLabel(name));
11
38
  const startedAt = Date.now();
12
39
  try {
13
40
  return await fn();