everything-dev 1.16.2 → 1.17.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 (95) hide show
  1. package/dist/api-contract.cjs +16 -5
  2. package/dist/api-contract.cjs.map +1 -1
  3. package/dist/api-contract.mjs +16 -5
  4. package/dist/api-contract.mjs.map +1 -1
  5. package/dist/cli/init.cjs +50 -51
  6. package/dist/cli/init.cjs.map +1 -1
  7. package/dist/cli/init.d.cts.map +1 -1
  8. package/dist/cli/init.d.mts.map +1 -1
  9. package/dist/cli/init.mjs +50 -51
  10. package/dist/cli/init.mjs.map +1 -1
  11. package/dist/cli/sync.cjs +3 -5
  12. package/dist/cli/sync.cjs.map +1 -1
  13. package/dist/cli/sync.mjs +3 -5
  14. package/dist/cli/sync.mjs.map +1 -1
  15. package/dist/cli/timing.cjs +30 -0
  16. package/dist/cli/timing.cjs.map +1 -0
  17. package/dist/cli/timing.mjs +27 -0
  18. package/dist/cli/timing.mjs.map +1 -0
  19. package/dist/cli/upgrade.cjs +214 -48
  20. package/dist/cli/upgrade.cjs.map +1 -1
  21. package/dist/cli/upgrade.mjs +214 -48
  22. package/dist/cli/upgrade.mjs.map +1 -1
  23. package/dist/cli.cjs +9 -0
  24. package/dist/cli.cjs.map +1 -1
  25. package/dist/cli.mjs +9 -0
  26. package/dist/cli.mjs.map +1 -1
  27. package/dist/components/dev-view.cjs +1 -1
  28. package/dist/components/dev-view.mjs +1 -1
  29. package/dist/components/streaming-view.cjs +1 -1
  30. package/dist/components/streaming-view.mjs +1 -1
  31. package/dist/config.cjs +125 -74
  32. package/dist/config.cjs.map +1 -1
  33. package/dist/config.d.cts +9 -2
  34. package/dist/config.d.cts.map +1 -1
  35. package/dist/config.d.mts +9 -2
  36. package/dist/config.d.mts.map +1 -1
  37. package/dist/config.mjs +126 -76
  38. package/dist/config.mjs.map +1 -1
  39. package/dist/contract.cjs +7 -0
  40. package/dist/contract.cjs.map +1 -1
  41. package/dist/contract.d.cts +58 -13
  42. package/dist/contract.d.cts.map +1 -1
  43. package/dist/contract.d.mts +58 -13
  44. package/dist/contract.d.mts.map +1 -1
  45. package/dist/contract.mjs +7 -1
  46. package/dist/contract.mjs.map +1 -1
  47. package/dist/dev-session.cjs +5 -3
  48. package/dist/dev-session.cjs.map +1 -1
  49. package/dist/dev-session.mjs +3 -3
  50. package/dist/dev-session.mjs.map +1 -1
  51. package/dist/index.cjs +3 -0
  52. package/dist/index.d.cts +4 -4
  53. package/dist/index.d.mts +4 -4
  54. package/dist/index.mjs +4 -4
  55. package/dist/merge.cjs +1 -0
  56. package/dist/merge.mjs +1 -1
  57. package/dist/orchestrator.cjs +1 -1
  58. package/dist/orchestrator.mjs +1 -1
  59. package/dist/plugin.cjs +49 -37
  60. package/dist/plugin.cjs.map +1 -1
  61. package/dist/plugin.d.cts +44 -12
  62. package/dist/plugin.d.cts.map +1 -1
  63. package/dist/plugin.d.mts +44 -12
  64. package/dist/plugin.d.mts.map +1 -1
  65. package/dist/plugin.mjs +48 -36
  66. package/dist/plugin.mjs.map +1 -1
  67. package/dist/sidebar.cjs +6 -14
  68. package/dist/sidebar.cjs.map +1 -1
  69. package/dist/sidebar.d.cts +3 -3
  70. package/dist/sidebar.d.cts.map +1 -1
  71. package/dist/sidebar.d.mts +3 -3
  72. package/dist/sidebar.d.mts.map +1 -1
  73. package/dist/sidebar.mjs +6 -14
  74. package/dist/sidebar.mjs.map +1 -1
  75. package/dist/types.cjs +10 -16
  76. package/dist/types.cjs.map +1 -1
  77. package/dist/types.d.cts +56 -12
  78. package/dist/types.d.cts.map +1 -1
  79. package/dist/types.d.mts +56 -12
  80. package/dist/types.d.mts.map +1 -1
  81. package/dist/types.mjs +10 -17
  82. package/dist/types.mjs.map +1 -1
  83. package/package.json +1 -1
  84. package/src/api-contract.ts +21 -3
  85. package/src/cli/init.ts +95 -63
  86. package/src/cli/sync.ts +5 -8
  87. package/src/cli/timing.ts +36 -0
  88. package/src/cli/upgrade.ts +292 -56
  89. package/src/cli.ts +15 -0
  90. package/src/config.ts +250 -107
  91. package/src/contract.ts +8 -0
  92. package/src/dev-session.ts +1 -1
  93. package/src/plugin.ts +97 -54
  94. package/src/sidebar.ts +9 -31
  95. package/src/types.ts +10 -15
@@ -3,12 +3,13 @@ import { join } from "node:path";
3
3
  import process from "node:process";
4
4
  import * as p from "@clack/prompts";
5
5
  import { glob } from "glob";
6
- import type { UpgradeOptions, UpgradeResult } from "../contract";
6
+ import type { PhaseTiming, UpgradeOptions, UpgradeResult } from "../contract";
7
7
  import { resolveExtendsRef } from "../merge";
8
8
  import { saveBosConfig } from "../utils/save-config";
9
9
  import { readInstalledFrameworkVersion } from "./framework-version";
10
10
  import { fetchParentConfig, runBunInstall, runTypesGen } from "./init";
11
11
  import { syncTemplate } from "./sync";
12
+ import { timePhase } from "./timing";
12
13
 
13
14
  const FRAMEWORK_PACKAGES = ["everything-dev", "every-plugin"];
14
15
 
@@ -69,6 +70,197 @@ function parseBosRef(ref: string): { account: string; gateway: string } | null {
69
70
  return { account: match[1], gateway: match[2] };
70
71
  }
71
72
 
73
+ function parseTargetedRef(ref: string): { configRef: string; targetPath?: string } {
74
+ const hashIndex = ref.indexOf("#");
75
+ if (hashIndex === -1) {
76
+ return { configRef: ref };
77
+ }
78
+ return {
79
+ configRef: ref.slice(0, hashIndex),
80
+ targetPath: ref.slice(hashIndex + 1) || undefined,
81
+ };
82
+ }
83
+
84
+ function ensureTargetedRef(ref: string, targetPath: string): string {
85
+ const parsed = parseTargetedRef(ref);
86
+ if (parsed.targetPath) return ref;
87
+ return `${parsed.configRef}#${targetPath}`;
88
+ }
89
+
90
+ function rewriteExtendsTarget(
91
+ entry: Record<string, unknown> | undefined,
92
+ targetPath: string,
93
+ ): boolean {
94
+ if (!entry?.extends) return false;
95
+
96
+ if (typeof entry.extends === "string") {
97
+ const next = ensureTargetedRef(entry.extends, targetPath);
98
+ if (next === entry.extends) return false;
99
+ entry.extends = next;
100
+ return true;
101
+ }
102
+
103
+ if (typeof entry.extends === "object") {
104
+ let changed = false;
105
+ for (const [key, value] of Object.entries(entry.extends as Record<string, unknown>)) {
106
+ if (typeof value !== "string") continue;
107
+ const next = ensureTargetedRef(value, targetPath);
108
+ if (next !== value) {
109
+ (entry.extends as Record<string, unknown>)[key] = next;
110
+ changed = true;
111
+ }
112
+ }
113
+ return changed;
114
+ }
115
+
116
+ return false;
117
+ }
118
+
119
+ function migrateRootConfigTargets(config: Record<string, unknown>): boolean {
120
+ let changed = false;
121
+ const app =
122
+ config.app && typeof config.app === "object"
123
+ ? (config.app as Record<string, unknown>)
124
+ : undefined;
125
+
126
+ if (app?.api && typeof app.api === "object") {
127
+ changed = rewriteExtendsTarget(app.api as Record<string, unknown>, "app.api") || changed;
128
+ }
129
+ if (app?.auth && typeof app.auth === "object") {
130
+ changed = rewriteExtendsTarget(app.auth as Record<string, unknown>, "app.auth") || changed;
131
+ }
132
+
133
+ if (config.plugins && typeof config.plugins === "object") {
134
+ for (const [pluginKey, pluginValue] of Object.entries(
135
+ config.plugins as Record<string, unknown>,
136
+ )) {
137
+ if (typeof pluginValue === "string") {
138
+ const next = ensureTargetedRef(pluginValue, `plugins.${pluginKey}`);
139
+ if (next !== pluginValue) {
140
+ (config.plugins as Record<string, unknown>)[pluginKey] = next;
141
+ changed = true;
142
+ }
143
+ continue;
144
+ }
145
+ if (!pluginValue || typeof pluginValue !== "object") continue;
146
+ changed =
147
+ rewriteExtendsTarget(pluginValue as Record<string, unknown>, `plugins.${pluginKey}`) ||
148
+ changed;
149
+ }
150
+ }
151
+
152
+ return changed;
153
+ }
154
+
155
+ function migratePluginProviderConfig(config: Record<string, unknown>, pluginKey: string): boolean {
156
+ let changed = false;
157
+ if (!config.plugins || typeof config.plugins !== "object") {
158
+ config.plugins = {};
159
+ changed = true;
160
+ }
161
+
162
+ const plugins = config.plugins as Record<string, unknown>;
163
+ if (!plugins[pluginKey] || typeof plugins[pluginKey] !== "object") {
164
+ plugins[pluginKey] = { name: pluginKey };
165
+ changed = true;
166
+ }
167
+
168
+ const pluginEntry = plugins[pluginKey] as Record<string, unknown>;
169
+ if (typeof pluginEntry.name !== "string" || pluginEntry.name.length === 0) {
170
+ pluginEntry.name = pluginKey;
171
+ changed = true;
172
+ }
173
+
174
+ const app =
175
+ config.app && typeof config.app === "object"
176
+ ? (config.app as Record<string, unknown>)
177
+ : undefined;
178
+ const apiEntry =
179
+ app?.api && typeof app.api === "object" ? (app.api as Record<string, unknown>) : undefined;
180
+
181
+ if (apiEntry) {
182
+ for (const key of [
183
+ "extends",
184
+ "name",
185
+ "development",
186
+ "production",
187
+ "integrity",
188
+ "proxy",
189
+ "variables",
190
+ "secrets",
191
+ "sidebar",
192
+ "routes",
193
+ ] as const) {
194
+ if (pluginEntry[key] === undefined && apiEntry[key] !== undefined) {
195
+ pluginEntry[key] = apiEntry[key];
196
+ changed = true;
197
+ }
198
+ }
199
+
200
+ delete app!.api;
201
+ changed = true;
202
+ if (Object.keys(app!).length === 0) {
203
+ delete config.app;
204
+ }
205
+ }
206
+
207
+ if (config.sidebar !== undefined && pluginEntry.sidebar === undefined) {
208
+ pluginEntry.sidebar = config.sidebar;
209
+ changed = true;
210
+ }
211
+ if (config.routes !== undefined && pluginEntry.routes === undefined) {
212
+ pluginEntry.routes = config.routes;
213
+ changed = true;
214
+ }
215
+ if (config.sidebar !== undefined) {
216
+ delete config.sidebar;
217
+ changed = true;
218
+ }
219
+ if (config.routes !== undefined) {
220
+ delete config.routes;
221
+ changed = true;
222
+ }
223
+
224
+ changed = rewriteExtendsTarget(pluginEntry, `plugins.${pluginKey}`) || changed;
225
+
226
+ return changed;
227
+ }
228
+
229
+ export async function migrateBosConfigFiles(projectDir: string): Promise<string[]> {
230
+ const migrated: string[] = [];
231
+ const rootConfigPath = join(projectDir, "bos.config.json");
232
+
233
+ if (existsSync(rootConfigPath)) {
234
+ const rootConfig = JSON.parse(readFileSync(rootConfigPath, "utf-8")) as Record<string, unknown>;
235
+ if (migrateRootConfigTargets(rootConfig)) {
236
+ await saveBosConfig(projectDir, rootConfig);
237
+ migrated.push("bos.config.json");
238
+ }
239
+ }
240
+
241
+ const pluginConfigPaths = await glob("plugins/*/bos.config.json", {
242
+ cwd: projectDir,
243
+ nodir: true,
244
+ dot: false,
245
+ absolute: false,
246
+ });
247
+
248
+ for (const relativePath of pluginConfigPaths) {
249
+ const match = relativePath.match(/^plugins\/([^/]+)\/bos\.config\.json$/);
250
+ const pluginKey = match?.[1];
251
+ if (!pluginKey) continue;
252
+
253
+ const filePath = join(projectDir, relativePath);
254
+ const pluginConfig = JSON.parse(readFileSync(filePath, "utf-8")) as Record<string, unknown>;
255
+ if (!migratePluginProviderConfig(pluginConfig, pluginKey)) continue;
256
+
257
+ writeFileSync(filePath, `${JSON.stringify(pluginConfig, null, 2)}\n`);
258
+ migrated.push(relativePath);
259
+ }
260
+
261
+ return migrated;
262
+ }
263
+
72
264
  async function loadParentPluginOptions(projectDir: string): Promise<{
73
265
  localConfig: Record<string, unknown>;
74
266
  parentPlugins: Record<string, unknown>;
@@ -141,7 +333,16 @@ async function addSelectedParentPlugins(projectDir: string): Promise<string[]> {
141
333
  : {};
142
334
  const nextPlugins = { ...localPlugins };
143
335
  for (const key of selected) {
144
- nextPlugins[key] = pluginOptions.parentPlugins[key];
336
+ const parentPlugin = pluginOptions.parentPlugins[key];
337
+ if (parentPlugin && typeof parentPlugin === "object") {
338
+ const nextPlugin = structuredClone(parentPlugin as Record<string, unknown>);
339
+ rewriteExtendsTarget(nextPlugin, `plugins.${key}`);
340
+ nextPlugins[key] = nextPlugin;
341
+ } else if (typeof parentPlugin === "string") {
342
+ nextPlugins[key] = ensureTargetedRef(parentPlugin, `plugins.${key}`);
343
+ } else {
344
+ nextPlugins[key] = parentPlugin;
345
+ }
145
346
  }
146
347
 
147
348
  pluginOptions.localConfig.plugins = nextPlugins;
@@ -333,38 +534,52 @@ export async function upgradeTemplate(
333
534
  projectDir: string,
334
535
  options: UpgradeOptions,
335
536
  ): Promise<UpgradeResult> {
537
+ const timings: PhaseTiming[] = [];
336
538
  const pkgPath = join(projectDir, "package.json");
337
539
  if (!existsSync(pkgPath)) {
338
540
  return {
339
541
  status: "error",
340
542
  packages: [],
543
+ timings,
341
544
  error: "No package.json found in current directory",
342
545
  };
343
546
  }
344
547
 
345
- const packages: UpgradeResult["packages"] = [];
548
+ const { packages, catalogVersionUpdates } = await timePhase(
549
+ timings,
550
+ "check package versions",
551
+ async () => {
552
+ const nextPackages: UpgradeResult["packages"] = [];
346
553
 
347
- for (const name of FRAMEWORK_PACKAGES) {
348
- const installed = readInstalledVersion(projectDir, name);
349
- const latest = await fetchLatestNpmVersion(name);
554
+ for (const name of FRAMEWORK_PACKAGES) {
555
+ const installed = readInstalledVersion(projectDir, name);
556
+ const latest = await fetchLatestNpmVersion(name);
350
557
 
351
- if (!latest) {
352
- packages.push({ name, from: installed, to: installed ?? "unknown" });
353
- continue;
354
- }
558
+ if (!latest) {
559
+ nextPackages.push({ name, from: installed, to: installed ?? "unknown" });
560
+ continue;
561
+ }
355
562
 
356
- packages.push({ name, from: installed, to: latest });
357
- }
563
+ nextPackages.push({ name, from: installed, to: latest });
564
+ }
358
565
 
359
- const catalogVersionUpdates: Array<{ name: string; from: string | undefined; to: string }> = [];
360
- for (const name of CATALOG_TOOL_PACKAGES) {
361
- const installed = readInstalledVersion(projectDir, name);
362
- if (!installed) continue;
363
- const latest = await fetchLatestNpmVersion(name);
364
- if (!latest) continue;
365
- if (installed === latest) continue;
366
- catalogVersionUpdates.push({ name, from: installed, to: latest });
367
- }
566
+ const nextCatalogVersionUpdates: Array<{
567
+ name: string;
568
+ from: string | undefined;
569
+ to: string;
570
+ }> = [];
571
+ for (const name of CATALOG_TOOL_PACKAGES) {
572
+ const installed = readInstalledVersion(projectDir, name);
573
+ if (!installed) continue;
574
+ const latest = await fetchLatestNpmVersion(name);
575
+ if (!latest) continue;
576
+ if (installed === latest) continue;
577
+ nextCatalogVersionUpdates.push({ name, from: installed, to: latest });
578
+ }
579
+
580
+ return { packages: nextPackages, catalogVersionUpdates: nextCatalogVersionUpdates };
581
+ },
582
+ );
368
583
 
369
584
  const hasFrameworkUpdates = packages.some((p) => p.from !== p.to && p.from !== undefined);
370
585
  const hasCatalogUpdates = catalogVersionUpdates.length > 0;
@@ -372,7 +587,11 @@ export async function upgradeTemplate(
372
587
 
373
588
  if (options.dryRun) {
374
589
  let changelogUrl: string | undefined;
375
- const pluginOptions = options.noSync ? null : await loadParentPluginOptions(projectDir);
590
+ const pluginOptions = options.noSync
591
+ ? null
592
+ : await timePhase(timings, "discover parent plugins", () =>
593
+ loadParentPluginOptions(projectDir),
594
+ );
376
595
  if (hasUpdates) {
377
596
  const configPath = join(projectDir, "bos.config.json");
378
597
  let parentConfig: Record<string, unknown> | null = null;
@@ -394,59 +613,75 @@ export async function upgradeTemplate(
394
613
  ...catalogVersionUpdates.map((u) => ({ name: u.name, from: u.from, to: u.to })),
395
614
  ],
396
615
  availablePlugins: pluginOptions?.newPluginKeys,
616
+ timings,
397
617
  changelogUrl,
398
618
  };
399
619
  }
400
620
 
401
- for (const pkg of packages) {
402
- if (pkg.from !== undefined && pkg.from !== pkg.to) {
403
- updateRootPackageVersion(projectDir, pkg.name, pkg.to);
404
- }
405
- }
406
-
407
- for (const update of catalogVersionUpdates) {
408
- updateRootCatalogVersion(projectDir, update.name, update.to);
409
- }
410
-
411
- const workspacePkgPaths = await findWorkspacePackageJsons(projectDir);
412
- for (const pkgPath of workspacePkgPaths) {
621
+ await timePhase(timings, "apply package updates", async () => {
413
622
  for (const pkg of packages) {
414
623
  if (pkg.from !== undefined && pkg.from !== pkg.to) {
415
- updateWorkspacePackageRefInFile(pkgPath, pkg.name);
624
+ updateRootPackageVersion(projectDir, pkg.name, pkg.to);
416
625
  }
417
626
  }
627
+
418
628
  for (const update of catalogVersionUpdates) {
419
- updateWorkspacePackageRefInFile(pkgPath, update.name);
629
+ updateRootCatalogVersion(projectDir, update.name, update.to);
420
630
  }
421
- }
631
+
632
+ const workspacePkgPaths = await findWorkspacePackageJsons(projectDir);
633
+ for (const pkgPath of workspacePkgPaths) {
634
+ for (const pkg of packages) {
635
+ if (pkg.from !== undefined && pkg.from !== pkg.to) {
636
+ updateWorkspacePackageRefInFile(pkgPath, pkg.name);
637
+ }
638
+ }
639
+ for (const update of catalogVersionUpdates) {
640
+ updateWorkspacePackageRefInFile(pkgPath, update.name);
641
+ }
642
+ }
643
+ });
644
+
645
+ const migratedBosConfigs = await timePhase(timings, "migrate bos configs", () =>
646
+ migrateBosConfigFiles(projectDir),
647
+ );
422
648
 
423
649
  let syncResult: UpgradeResult["sync"];
424
650
  let addedPlugins: string[] = [];
425
651
  if (!options.noSync) {
426
- if (!options.dryRun) {
427
- addedPlugins = await addSelectedParentPlugins(projectDir);
428
- }
429
-
430
- syncResult = await syncTemplate(projectDir, {
431
- dryRun: false,
432
- force: options.force,
433
- noInstall: true,
652
+ addedPlugins = await timePhase(timings, "discover parent plugins", async () => {
653
+ if (options.dryRun) return [];
654
+ return addSelectedParentPlugins(projectDir);
434
655
  });
435
- }
436
656
 
437
- if ((hasUpdates || addedPlugins.length > 0) && !options.noInstall) {
438
- await runBunInstall(projectDir);
439
- await runTypesGen(projectDir);
657
+ syncResult = await timePhase(timings, "sync template", () =>
658
+ syncTemplate(projectDir, {
659
+ dryRun: false,
660
+ force: options.force,
661
+ noInstall: true,
662
+ }),
663
+ );
440
664
  }
441
665
 
442
- const migratedFiles = await rewriteLegacyUiImports(projectDir);
443
- for (const file of OBSOLETE_FILES) {
444
- const filePath = join(projectDir, file);
445
- if (existsSync(filePath)) {
446
- rmSync(filePath);
447
- migratedFiles.push(file);
666
+ if ((hasUpdates || addedPlugins.length > 0) && !options.noInstall) {
667
+ await timePhase(timings, "install dependencies", () => runBunInstall(projectDir));
668
+ await timePhase(timings, "generate types", () => runTypesGen(projectDir));
669
+ }
670
+
671
+ const migratedFiles = await timePhase(timings, "clean obsolete files", async () => {
672
+ const nextMigratedFiles = [
673
+ ...migratedBosConfigs,
674
+ ...(await rewriteLegacyUiImports(projectDir)),
675
+ ];
676
+ for (const file of OBSOLETE_FILES) {
677
+ const filePath = join(projectDir, file);
678
+ if (existsSync(filePath)) {
679
+ rmSync(filePath);
680
+ nextMigratedFiles.push(file);
681
+ }
448
682
  }
449
- }
683
+ return nextMigratedFiles;
684
+ });
450
685
 
451
686
  let changelogUrl: string | undefined;
452
687
  const mainPkg = packages.find((p) => p.name === "everything-dev");
@@ -470,6 +705,7 @@ export async function upgradeTemplate(
470
705
  sync: syncResult,
471
706
  migrated: migratedFiles.length > 0 ? migratedFiles : undefined,
472
707
  selectedPlugins: addedPlugins.length > 0 ? addedPlugins : undefined,
708
+ timings,
473
709
  changelogUrl,
474
710
  };
475
711
  }
package/src/cli.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { findCommandDescriptor } from "./cli/catalog";
3
3
  import { printHelp } from "./cli/help";
4
4
  import { parseCommandInput } from "./cli/parse";
5
+ import { formatDuration, sumPhaseDurations } from "./cli/timing";
5
6
  import { findConfigPath } from "./config";
6
7
  import bosPlugin from "./plugin";
7
8
  import { createPluginRuntime } from "./sdk";
@@ -50,6 +51,18 @@ function normalizeVersion(v: string): string {
50
51
  return v.replace(/^[\^~>=v]+/, "").trim();
51
52
  }
52
53
 
54
+ function printTimingSummary(timings: Array<{ name: string; durationMs: number }> | undefined) {
55
+ if (!timings || timings.length === 0) return;
56
+
57
+ console.log(` ${colors.dim("Timings:")}`);
58
+ for (const timing of timings) {
59
+ console.log(` ${colors.dim(timing.name.padEnd(22))} ${formatDuration(timing.durationMs)}`);
60
+ }
61
+ console.log(
62
+ ` ${colors.dim("total".padEnd(22))} ${formatDuration(sumPhaseDurations(timings))}`,
63
+ );
64
+ }
65
+
53
66
  async function warnIfOutdated(client: any, command: string): Promise<void> {
54
67
  if (!["dev", "build", "start"].includes(command)) return;
55
68
 
@@ -152,6 +165,7 @@ async function main() {
152
165
  if (result.plugins && result.plugins.length > 0)
153
166
  console.log(` ${colors.dim("Plugins:")} ${result.plugins.join(", ")}`);
154
167
  console.log(` ${colors.dim("Files copied:")} ${result.filesCopied}`);
168
+ printTimingSummary(result.timings);
155
169
  console.log();
156
170
  console.log(colors.dim(" Next steps:"));
157
171
  console.log(colors.dim(` cd ${result.directory}`));
@@ -246,6 +260,7 @@ async function main() {
246
260
  if (result.selectedPlugins && result.selectedPlugins.length > 0) {
247
261
  console.log(` ${colors.dim("Added plugins:")} ${result.selectedPlugins.join(", ")}`);
248
262
  }
263
+ printTimingSummary(result.timings);
249
264
  if (result.sync) {
250
265
  const sync = result.sync;
251
266
  if (sync.updated.length > 0) {