everything-dev 1.16.1 → 1.16.3

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 (75) 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/infra.cjs +127 -0
  6. package/dist/cli/infra.cjs.map +1 -0
  7. package/dist/cli/infra.mjs +124 -0
  8. package/dist/cli/infra.mjs.map +1 -0
  9. package/dist/cli/init.cjs +9 -0
  10. package/dist/cli/init.cjs.map +1 -1
  11. package/dist/cli/init.d.cts +2 -1
  12. package/dist/cli/init.d.cts.map +1 -1
  13. package/dist/cli/init.d.mts +2 -1
  14. package/dist/cli/init.d.mts.map +1 -1
  15. package/dist/cli/init.mjs +9 -1
  16. package/dist/cli/init.mjs.map +1 -1
  17. package/dist/cli/sync.cjs +6 -0
  18. package/dist/cli/sync.cjs.map +1 -1
  19. package/dist/cli/sync.mjs +6 -0
  20. package/dist/cli/sync.mjs.map +1 -1
  21. package/dist/cli/timing.cjs +30 -0
  22. package/dist/cli/timing.cjs.map +1 -0
  23. package/dist/cli/timing.mjs +27 -0
  24. package/dist/cli/timing.mjs.map +1 -0
  25. package/dist/cli/upgrade.cjs +66 -47
  26. package/dist/cli/upgrade.cjs.map +1 -1
  27. package/dist/cli/upgrade.mjs +66 -47
  28. package/dist/cli/upgrade.mjs.map +1 -1
  29. package/dist/cli.cjs +9 -0
  30. package/dist/cli.cjs.map +1 -1
  31. package/dist/cli.mjs +9 -0
  32. package/dist/cli.mjs.map +1 -1
  33. package/dist/components/dev-view.cjs +1 -1
  34. package/dist/components/dev-view.mjs +1 -1
  35. package/dist/components/streaming-view.cjs +1 -1
  36. package/dist/components/streaming-view.mjs +1 -1
  37. package/dist/contract.cjs +7 -0
  38. package/dist/contract.cjs.map +1 -1
  39. package/dist/contract.d.cts +22 -1
  40. package/dist/contract.d.cts.map +1 -1
  41. package/dist/contract.d.mts +22 -1
  42. package/dist/contract.d.mts.map +1 -1
  43. package/dist/contract.mjs +7 -1
  44. package/dist/contract.mjs.map +1 -1
  45. package/dist/dev-session.cjs +5 -3
  46. package/dist/dev-session.cjs.map +1 -1
  47. package/dist/dev-session.mjs +3 -3
  48. package/dist/dev-session.mjs.map +1 -1
  49. package/dist/index.cjs +1 -0
  50. package/dist/index.d.cts +2 -2
  51. package/dist/index.d.mts +2 -2
  52. package/dist/index.mjs +2 -2
  53. package/dist/near-cli.cjs +1 -1
  54. package/dist/near-cli.mjs +1 -1
  55. package/dist/orchestrator.cjs +1 -1
  56. package/dist/orchestrator.mjs +1 -1
  57. package/dist/plugin.cjs +56 -39
  58. package/dist/plugin.cjs.map +1 -1
  59. package/dist/plugin.d.cts +8 -0
  60. package/dist/plugin.d.cts.map +1 -1
  61. package/dist/plugin.d.mts +8 -0
  62. package/dist/plugin.d.mts.map +1 -1
  63. package/dist/plugin.mjs +54 -37
  64. package/dist/plugin.mjs.map +1 -1
  65. package/package.json +1 -1
  66. package/src/api-contract.ts +21 -3
  67. package/src/cli/infra.ts +190 -0
  68. package/src/cli/init.ts +4 -0
  69. package/src/cli/sync.ts +9 -0
  70. package/src/cli/timing.ts +36 -0
  71. package/src/cli/upgrade.ts +82 -53
  72. package/src/cli.ts +15 -0
  73. package/src/contract.ts +8 -0
  74. package/src/dev-session.ts +1 -1
  75. package/src/plugin.ts +98 -67
@@ -0,0 +1,190 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import * as p from "@clack/prompts";
5
+ import type { RuntimeConfig } from "../types";
6
+
7
+ const POSTGRES_USER = "everythingdev";
8
+ const POSTGRES_PASSWORD = "everythingdev";
9
+ const API_DATABASE_SECRET = "API_DATABASE_URL";
10
+ const AUTH_DATABASE_SECRET = "AUTH_DATABASE_URL";
11
+ const BASE_DATABASE_PORT = 5434;
12
+
13
+ interface DatabaseSecretConfig {
14
+ secret: string;
15
+ slug: string;
16
+ port: number;
17
+ serviceName: string;
18
+ databaseName: string;
19
+ volumeName: string;
20
+ url: string;
21
+ }
22
+
23
+ function uniqueSecrets(values: Array<string | undefined>): string[] {
24
+ const secrets: string[] = [];
25
+ const seen = new Set<string>();
26
+
27
+ for (const value of values) {
28
+ if (!value || seen.has(value)) continue;
29
+ seen.add(value);
30
+ secrets.push(value);
31
+ }
32
+
33
+ return secrets;
34
+ }
35
+
36
+ function getRuntimeSecrets(runtimeConfig: RuntimeConfig): string[] {
37
+ const pluginSecrets = Object.values(runtimeConfig.plugins ?? {}).flatMap(
38
+ (plugin) => plugin.secrets ?? [],
39
+ );
40
+
41
+ return uniqueSecrets([
42
+ API_DATABASE_SECRET,
43
+ AUTH_DATABASE_SECRET,
44
+ ...(runtimeConfig.api.secrets ?? []),
45
+ ...(runtimeConfig.auth?.secrets ?? []),
46
+ ...pluginSecrets,
47
+ ]);
48
+ }
49
+
50
+ function normalizeDatabaseSlug(secret: string): string {
51
+ return secret.replace(/_DATABASE_URL$/, "").toLowerCase();
52
+ }
53
+
54
+ function buildDatabaseConfigs(secrets: string[]): DatabaseSecretConfig[] {
55
+ const databaseSecrets = uniqueSecrets(
56
+ secrets.filter((secret) => secret.endsWith("_DATABASE_URL")),
57
+ );
58
+
59
+ const additionalSecrets = databaseSecrets
60
+ .filter((secret) => secret !== API_DATABASE_SECRET && secret !== AUTH_DATABASE_SECRET)
61
+ .sort((a, b) => a.localeCompare(b));
62
+
63
+ const orderedSecrets = [API_DATABASE_SECRET, AUTH_DATABASE_SECRET, ...additionalSecrets];
64
+
65
+ return orderedSecrets.map((secret, index) => {
66
+ const slug = normalizeDatabaseSlug(secret);
67
+ const port =
68
+ secret === API_DATABASE_SECRET
69
+ ? 5432
70
+ : secret === AUTH_DATABASE_SECRET
71
+ ? 5433
72
+ : BASE_DATABASE_PORT + index - 2;
73
+
74
+ return {
75
+ secret,
76
+ slug,
77
+ port,
78
+ serviceName: `postgres-${slug.replace(/_/g, "-")}`,
79
+ databaseName: `${slug}_db`,
80
+ volumeName: `postgres_${slug}_data`,
81
+ url: `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${port}/${slug}_db`,
82
+ };
83
+ });
84
+ }
85
+
86
+ function defaultSecretValue(
87
+ secret: string,
88
+ databases: Map<string, DatabaseSecretConfig>,
89
+ options: { forExample: boolean },
90
+ ): string {
91
+ if (secret === "BETTER_AUTH_SECRET") {
92
+ return options.forExample ? "" : randomBytes(32).toString("base64url");
93
+ }
94
+
95
+ if (secret === "CORS_ORIGIN") {
96
+ return "http://localhost:3000";
97
+ }
98
+
99
+ return databases.get(secret)?.url ?? "";
100
+ }
101
+
102
+ function renderEnvFile(
103
+ secrets: string[],
104
+ databases: DatabaseSecretConfig[],
105
+ options: { forExample: boolean },
106
+ ): string {
107
+ const databaseMap = new Map(databases.map((entry) => [entry.secret, entry]));
108
+ const lines = [
109
+ "# Generated from configured bos secrets",
110
+ "# Update values as needed for your local environment",
111
+ "",
112
+ ];
113
+
114
+ for (const secret of secrets) {
115
+ lines.push(`${secret}=${defaultSecretValue(secret, databaseMap, options)}`);
116
+ }
117
+
118
+ return `${lines.join("\n")}\n`;
119
+ }
120
+
121
+ function renderDockerCompose(databases: DatabaseSecretConfig[]): string {
122
+ const lines = [
123
+ "x-pg-common: &pg-common",
124
+ " image: postgres:17-alpine",
125
+ " environment: &pg-env",
126
+ ` POSTGRES_USER: ${POSTGRES_USER}`,
127
+ ` POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}`,
128
+ " healthcheck:",
129
+ ' test: ["CMD-SHELL", "pg_isready -U everythingdev"]',
130
+ " interval: 3s",
131
+ " timeout: 3s",
132
+ " retries: 5",
133
+ "",
134
+ "services:",
135
+ ];
136
+
137
+ for (const database of databases) {
138
+ lines.push(` ${database.serviceName}:`);
139
+ lines.push(" <<: *pg-common");
140
+ lines.push(" environment:");
141
+ lines.push(" <<: *pg-env");
142
+ lines.push(` POSTGRES_DB: ${database.databaseName}`);
143
+ lines.push(" ports:");
144
+ lines.push(` - "${database.port}:5432"`);
145
+ lines.push(" volumes:");
146
+ lines.push(` - ${database.volumeName}:/var/lib/postgresql/data`);
147
+ lines.push("");
148
+ }
149
+
150
+ lines.push("volumes:");
151
+ for (const database of databases) {
152
+ lines.push(` ${database.volumeName}:`);
153
+ }
154
+
155
+ return `${lines.join("\n")}\n`;
156
+ }
157
+
158
+ export function writeGeneratedInfra(configDir: string, runtimeConfig: RuntimeConfig): string[] {
159
+ const secrets = getRuntimeSecrets(runtimeConfig);
160
+ const databases = buildDatabaseConfigs(secrets);
161
+ const envExamplePath = join(configDir, ".env.example");
162
+ const dockerComposePath = join(configDir, "docker-compose.yml");
163
+
164
+ writeFileSync(envExamplePath, renderEnvFile(secrets, databases, { forExample: true }));
165
+ writeFileSync(dockerComposePath, renderDockerCompose(databases));
166
+
167
+ return secrets;
168
+ }
169
+
170
+ export function ensureEnvFile(configDir: string): void {
171
+ const envPath = join(configDir, ".env");
172
+ const examplePath = join(configDir, ".env.example");
173
+
174
+ if (existsSync(envPath) || !existsSync(examplePath)) return;
175
+
176
+ const content = readFileSync(examplePath, "utf-8");
177
+ const lines = content.split("\n");
178
+ const secret = randomBytes(32).toString("base64url");
179
+ const updated = lines
180
+ .map((line) => {
181
+ if (/^BETTER_AUTH_SECRET=/.test(line)) {
182
+ return `BETTER_AUTH_SECRET=${secret}`;
183
+ }
184
+ return line;
185
+ })
186
+ .join("\n");
187
+
188
+ writeFileSync(envPath, updated);
189
+ p.log.info("Created .env from generated .env.example with generated BETTER_AUTH_SECRET");
190
+ }
package/src/cli/init.ts CHANGED
@@ -609,6 +609,10 @@ export async function runTypesGen(destination: string): Promise<void> {
609
609
  await execCommand("node_modules/.bin/bos", ["types", "gen"], destination);
610
610
  }
611
611
 
612
+ export async function runDockerComposeUp(destination: string): Promise<void> {
613
+ await execCommand("docker", ["compose", "up", "-d", "--wait"], destination);
614
+ }
615
+
612
616
  const WORKSPACE_LOCAL_PATHS: Record<string, string> = {
613
617
  "everything-dev": "packages/everything-dev",
614
618
  "every-plugin": "packages/every-plugin",
package/src/cli/sync.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  } from "node:fs";
10
10
  import { dirname, join } from "node:path";
11
11
  import { glob } from "glob";
12
+ import { loadConfig } from "../config";
12
13
  import type { SyncOptions, SyncResult } from "../contract";
13
14
  import {
14
15
  isPlainObject as isPlainObjectFromMerge,
@@ -17,6 +18,7 @@ import {
17
18
  } from "../merge";
18
19
  import type { BosPluginRef } from "../types";
19
20
  import { isPathExcluded } from "../utils/path-match";
21
+ import { writeGeneratedInfra } from "./infra";
20
22
  import {
21
23
  personalizeConfig,
22
24
  readTemplatekeep,
@@ -27,10 +29,12 @@ import {
27
29
  import { readSnapshot, writeSnapshot } from "./snapshot";
28
30
 
29
31
  const FRAMEWORK_OWNED_SYNC_FILES = new Set([
32
+ ".env.example",
30
33
  ".gitignore",
31
34
  "biome.json",
32
35
  "bos.config.json",
33
36
  "package.json",
37
+ "docker-compose.yml",
34
38
  ".github/renovate.json",
35
39
  ".github/workflows/ci.yml",
36
40
  ".github/workflows/release-sync.yml",
@@ -494,6 +498,11 @@ export async function syncTemplate(projectDir: string, options: SyncOptions): Pr
494
498
  mode: "sync",
495
499
  });
496
500
 
501
+ const syncedConfig = await loadConfig({ cwd: projectDir });
502
+ if (syncedConfig?.runtime) {
503
+ writeGeneratedInfra(projectDir, syncedConfig.runtime);
504
+ }
505
+
497
506
  if (!options.noInstall) {
498
507
  await runBunInstall(projectDir);
499
508
  await runTypesGen(projectDir);
@@ -0,0 +1,36 @@
1
+ export interface PhaseTiming {
2
+ name: string;
3
+ durationMs: number;
4
+ }
5
+
6
+ export async function timePhase<T>(
7
+ timings: PhaseTiming[],
8
+ name: string,
9
+ fn: () => Promise<T>,
10
+ ): Promise<T> {
11
+ const startedAt = Date.now();
12
+ try {
13
+ return await fn();
14
+ } finally {
15
+ timings.push({ name, durationMs: Date.now() - startedAt });
16
+ }
17
+ }
18
+
19
+ export function sumPhaseDurations(timings: PhaseTiming[]): number {
20
+ return timings.reduce((total, timing) => total + timing.durationMs, 0);
21
+ }
22
+
23
+ export function formatDuration(durationMs: number): string {
24
+ if (durationMs < 1000) {
25
+ return `${durationMs}ms`;
26
+ }
27
+
28
+ if (durationMs < 60_000) {
29
+ const seconds = durationMs / 1000;
30
+ return `${seconds.toFixed(seconds >= 10 ? 0 : 1)}s`;
31
+ }
32
+
33
+ const minutes = Math.floor(durationMs / 60_000);
34
+ const seconds = Math.round((durationMs % 60_000) / 1000);
35
+ return `${minutes}m ${seconds}s`;
36
+ }
@@ -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
 
@@ -333,38 +334,52 @@ export async function upgradeTemplate(
333
334
  projectDir: string,
334
335
  options: UpgradeOptions,
335
336
  ): Promise<UpgradeResult> {
337
+ const timings: PhaseTiming[] = [];
336
338
  const pkgPath = join(projectDir, "package.json");
337
339
  if (!existsSync(pkgPath)) {
338
340
  return {
339
341
  status: "error",
340
342
  packages: [],
343
+ timings,
341
344
  error: "No package.json found in current directory",
342
345
  };
343
346
  }
344
347
 
345
- const packages: UpgradeResult["packages"] = [];
348
+ const { packages, catalogVersionUpdates } = await timePhase(
349
+ timings,
350
+ "check package versions",
351
+ async () => {
352
+ const nextPackages: UpgradeResult["packages"] = [];
346
353
 
347
- for (const name of FRAMEWORK_PACKAGES) {
348
- const installed = readInstalledVersion(projectDir, name);
349
- const latest = await fetchLatestNpmVersion(name);
354
+ for (const name of FRAMEWORK_PACKAGES) {
355
+ const installed = readInstalledVersion(projectDir, name);
356
+ const latest = await fetchLatestNpmVersion(name);
350
357
 
351
- if (!latest) {
352
- packages.push({ name, from: installed, to: installed ?? "unknown" });
353
- continue;
354
- }
358
+ if (!latest) {
359
+ nextPackages.push({ name, from: installed, to: installed ?? "unknown" });
360
+ continue;
361
+ }
355
362
 
356
- packages.push({ name, from: installed, to: latest });
357
- }
363
+ nextPackages.push({ name, from: installed, to: latest });
364
+ }
358
365
 
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
- }
366
+ const nextCatalogVersionUpdates: Array<{
367
+ name: string;
368
+ from: string | undefined;
369
+ to: string;
370
+ }> = [];
371
+ for (const name of CATALOG_TOOL_PACKAGES) {
372
+ const installed = readInstalledVersion(projectDir, name);
373
+ if (!installed) continue;
374
+ const latest = await fetchLatestNpmVersion(name);
375
+ if (!latest) continue;
376
+ if (installed === latest) continue;
377
+ nextCatalogVersionUpdates.push({ name, from: installed, to: latest });
378
+ }
379
+
380
+ return { packages: nextPackages, catalogVersionUpdates: nextCatalogVersionUpdates };
381
+ },
382
+ );
368
383
 
369
384
  const hasFrameworkUpdates = packages.some((p) => p.from !== p.to && p.from !== undefined);
370
385
  const hasCatalogUpdates = catalogVersionUpdates.length > 0;
@@ -372,7 +387,11 @@ export async function upgradeTemplate(
372
387
 
373
388
  if (options.dryRun) {
374
389
  let changelogUrl: string | undefined;
375
- const pluginOptions = options.noSync ? null : await loadParentPluginOptions(projectDir);
390
+ const pluginOptions = options.noSync
391
+ ? null
392
+ : await timePhase(timings, "discover parent plugins", () =>
393
+ loadParentPluginOptions(projectDir),
394
+ );
376
395
  if (hasUpdates) {
377
396
  const configPath = join(projectDir, "bos.config.json");
378
397
  let parentConfig: Record<string, unknown> | null = null;
@@ -394,59 +413,68 @@ export async function upgradeTemplate(
394
413
  ...catalogVersionUpdates.map((u) => ({ name: u.name, from: u.from, to: u.to })),
395
414
  ],
396
415
  availablePlugins: pluginOptions?.newPluginKeys,
416
+ timings,
397
417
  changelogUrl,
398
418
  };
399
419
  }
400
420
 
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) {
421
+ await timePhase(timings, "apply package updates", async () => {
413
422
  for (const pkg of packages) {
414
423
  if (pkg.from !== undefined && pkg.from !== pkg.to) {
415
- updateWorkspacePackageRefInFile(pkgPath, pkg.name);
424
+ updateRootPackageVersion(projectDir, pkg.name, pkg.to);
416
425
  }
417
426
  }
427
+
418
428
  for (const update of catalogVersionUpdates) {
419
- updateWorkspacePackageRefInFile(pkgPath, update.name);
429
+ updateRootCatalogVersion(projectDir, update.name, update.to);
420
430
  }
421
- }
431
+
432
+ const workspacePkgPaths = await findWorkspacePackageJsons(projectDir);
433
+ for (const pkgPath of workspacePkgPaths) {
434
+ for (const pkg of packages) {
435
+ if (pkg.from !== undefined && pkg.from !== pkg.to) {
436
+ updateWorkspacePackageRefInFile(pkgPath, pkg.name);
437
+ }
438
+ }
439
+ for (const update of catalogVersionUpdates) {
440
+ updateWorkspacePackageRefInFile(pkgPath, update.name);
441
+ }
442
+ }
443
+ });
422
444
 
423
445
  let syncResult: UpgradeResult["sync"];
424
446
  let addedPlugins: string[] = [];
425
447
  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,
448
+ addedPlugins = await timePhase(timings, "discover parent plugins", async () => {
449
+ if (options.dryRun) return [];
450
+ return addSelectedParentPlugins(projectDir);
434
451
  });
452
+
453
+ syncResult = await timePhase(timings, "sync template", () =>
454
+ syncTemplate(projectDir, {
455
+ dryRun: false,
456
+ force: options.force,
457
+ noInstall: true,
458
+ }),
459
+ );
435
460
  }
436
461
 
437
462
  if ((hasUpdates || addedPlugins.length > 0) && !options.noInstall) {
438
- await runBunInstall(projectDir);
439
- await runTypesGen(projectDir);
463
+ await timePhase(timings, "install dependencies", () => runBunInstall(projectDir));
464
+ await timePhase(timings, "generate types", () => runTypesGen(projectDir));
440
465
  }
441
466
 
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);
467
+ const migratedFiles = await timePhase(timings, "clean obsolete files", async () => {
468
+ const nextMigratedFiles = await rewriteLegacyUiImports(projectDir);
469
+ for (const file of OBSOLETE_FILES) {
470
+ const filePath = join(projectDir, file);
471
+ if (existsSync(filePath)) {
472
+ rmSync(filePath);
473
+ nextMigratedFiles.push(file);
474
+ }
448
475
  }
449
- }
476
+ return nextMigratedFiles;
477
+ });
450
478
 
451
479
  let changelogUrl: string | undefined;
452
480
  const mainPkg = packages.find((p) => p.name === "everything-dev");
@@ -470,6 +498,7 @@ export async function upgradeTemplate(
470
498
  sync: syncResult,
471
499
  migrated: migratedFiles.length > 0 ? migratedFiles : undefined,
472
500
  selectedPlugins: addedPlugins.length > 0 ? addedPlugins : undefined,
501
+ timings,
473
502
  changelogUrl,
474
503
  };
475
504
  }
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) {
package/src/contract.ts CHANGED
@@ -156,6 +156,11 @@ export const InitOptionsSchema = z.object({
156
156
  noInstall: z.boolean().default(false),
157
157
  });
158
158
 
159
+ export const PhaseTimingSchema = z.object({
160
+ name: z.string(),
161
+ durationMs: z.number(),
162
+ });
163
+
159
164
  export const InitResultSchema = z.object({
160
165
  status: z.enum(["initialized", "error"]),
161
166
  directory: z.string(),
@@ -166,6 +171,7 @@ export const InitResultSchema = z.object({
166
171
  extends: z.string(),
167
172
  plugins: z.array(z.string()).optional(),
168
173
  filesCopied: z.number(),
174
+ timings: z.array(PhaseTimingSchema).optional(),
169
175
  error: z.string().optional(),
170
176
  });
171
177
 
@@ -203,6 +209,7 @@ export const UpgradeResultSchema = z.object({
203
209
  migrated: z.array(z.string()).optional(),
204
210
  availablePlugins: z.array(z.string()).optional(),
205
211
  selectedPlugins: z.array(z.string()).optional(),
212
+ timings: z.array(PhaseTimingSchema).optional(),
206
213
  changelogUrl: z.string().optional(),
207
214
  error: z.string().optional(),
208
215
  });
@@ -307,6 +314,7 @@ export type KeyPublishOptions = z.infer<typeof KeyPublishOptionsSchema>;
307
314
  export type KeyPublishResult = z.infer<typeof KeyPublishResultSchema>;
308
315
  export type InitOptions = z.infer<typeof InitOptionsSchema>;
309
316
  export type InitResult = z.infer<typeof InitResultSchema>;
317
+ export type PhaseTiming = z.infer<typeof PhaseTimingSchema>;
310
318
  export type SyncOptions = z.infer<typeof SyncOptionsSchema>;
311
319
  export type SyncResult = z.infer<typeof SyncResultSchema>;
312
320
  export type UpgradeOptions = z.infer<typeof UpgradeOptionsSchema>;
@@ -1,4 +1,4 @@
1
- import { NodeContext } from "@effect/platform-node";
1
+ import * as NodeContext from "@effect/platform-node/NodeContext";
2
2
  import { Deferred, Effect, Exit } from "effect";
3
3
  import {
4
4
  type DevViewHandle,