everything-dev 1.16.0 → 1.16.2

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 (86) hide show
  1. package/dist/cli/infra.cjs +127 -0
  2. package/dist/cli/infra.cjs.map +1 -0
  3. package/dist/cli/infra.mjs +124 -0
  4. package/dist/cli/infra.mjs.map +1 -0
  5. package/dist/cli/init.cjs +9 -0
  6. package/dist/cli/init.cjs.map +1 -1
  7. package/dist/cli/init.d.cts +2 -1
  8. package/dist/cli/init.d.cts.map +1 -1
  9. package/dist/cli/init.d.mts +2 -1
  10. package/dist/cli/init.d.mts.map +1 -1
  11. package/dist/cli/init.mjs +9 -1
  12. package/dist/cli/init.mjs.map +1 -1
  13. package/dist/cli/sync.cjs +6 -0
  14. package/dist/cli/sync.cjs.map +1 -1
  15. package/dist/cli/sync.mjs +6 -0
  16. package/dist/cli/sync.mjs.map +1 -1
  17. package/dist/cli/upgrade.cjs +75 -7
  18. package/dist/cli/upgrade.cjs.map +1 -1
  19. package/dist/cli/upgrade.mjs +74 -8
  20. package/dist/cli/upgrade.mjs.map +1 -1
  21. package/dist/cli.cjs +2 -0
  22. package/dist/cli.cjs.map +1 -1
  23. package/dist/cli.mjs +2 -0
  24. package/dist/cli.mjs.map +1 -1
  25. package/dist/config.cjs +2 -24
  26. package/dist/config.cjs.map +1 -1
  27. package/dist/config.d.cts.map +1 -1
  28. package/dist/config.d.mts.map +1 -1
  29. package/dist/config.mjs +3 -25
  30. package/dist/config.mjs.map +1 -1
  31. package/dist/contract.cjs +2 -0
  32. package/dist/contract.cjs.map +1 -1
  33. package/dist/contract.d.cts +6 -2
  34. package/dist/contract.d.cts.map +1 -1
  35. package/dist/contract.d.mts +6 -2
  36. package/dist/contract.d.mts.map +1 -1
  37. package/dist/contract.meta.cjs +2 -2
  38. package/dist/contract.meta.cjs.map +1 -1
  39. package/dist/contract.meta.d.cts +2 -2
  40. package/dist/contract.meta.d.mts +2 -2
  41. package/dist/contract.meta.mjs +2 -2
  42. package/dist/contract.meta.mjs.map +1 -1
  43. package/dist/contract.mjs +2 -0
  44. package/dist/contract.mjs.map +1 -1
  45. package/dist/fastkv.cjs +0 -45
  46. package/dist/fastkv.cjs.map +1 -1
  47. package/dist/fastkv.d.cts +1 -19
  48. package/dist/fastkv.d.cts.map +1 -1
  49. package/dist/fastkv.d.mts +1 -19
  50. package/dist/fastkv.d.mts.map +1 -1
  51. package/dist/fastkv.mjs +1 -44
  52. package/dist/fastkv.mjs.map +1 -1
  53. package/dist/index.cjs +0 -2
  54. package/dist/index.d.cts +2 -2
  55. package/dist/index.d.mts +2 -2
  56. package/dist/index.mjs +2 -2
  57. package/dist/merge.cjs +4 -11
  58. package/dist/merge.cjs.map +1 -1
  59. package/dist/merge.d.cts.map +1 -1
  60. package/dist/merge.d.mts.map +1 -1
  61. package/dist/merge.mjs +4 -11
  62. package/dist/merge.mjs.map +1 -1
  63. package/dist/near-cli.cjs +1 -1
  64. package/dist/near-cli.mjs +1 -1
  65. package/dist/plugin.cjs +51 -58
  66. package/dist/plugin.cjs.map +1 -1
  67. package/dist/plugin.d.cts +4 -2
  68. package/dist/plugin.d.cts.map +1 -1
  69. package/dist/plugin.d.mts +4 -2
  70. package/dist/plugin.d.mts.map +1 -1
  71. package/dist/plugin.mjs +50 -57
  72. package/dist/plugin.mjs.map +1 -1
  73. package/dist/types.d.cts +2 -2
  74. package/dist/types.d.mts +2 -2
  75. package/package.json +1 -1
  76. package/src/cli/infra.ts +190 -0
  77. package/src/cli/init.ts +4 -0
  78. package/src/cli/sync.ts +9 -0
  79. package/src/cli/upgrade.ts +117 -6
  80. package/src/cli.ts +6 -0
  81. package/src/config.ts +5 -34
  82. package/src/contract.meta.ts +2 -2
  83. package/src/contract.ts +2 -0
  84. package/src/fastkv.ts +0 -72
  85. package/src/merge.ts +6 -19
  86. package/src/plugin.ts +51 -70
@@ -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);
@@ -1,9 +1,13 @@
1
1
  import { existsSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import process from "node:process";
4
+ import * as p from "@clack/prompts";
3
5
  import { glob } from "glob";
4
6
  import type { UpgradeOptions, UpgradeResult } from "../contract";
7
+ import { resolveExtendsRef } from "../merge";
8
+ import { saveBosConfig } from "../utils/save-config";
5
9
  import { readInstalledFrameworkVersion } from "./framework-version";
6
- import { runBunInstall, runTypesGen } from "./init";
10
+ import { fetchParentConfig, runBunInstall, runTypesGen } from "./init";
7
11
  import { syncTemplate } from "./sync";
8
12
 
9
13
  const FRAMEWORK_PACKAGES = ["everything-dev", "every-plugin"];
@@ -47,6 +51,105 @@ interface NpmPackageInfo {
47
51
  version: string;
48
52
  }
49
53
 
54
+ function getExtendsRef(config: Record<string, unknown>): string | undefined {
55
+ if (typeof config.extends === "string") {
56
+ return config.extends;
57
+ }
58
+
59
+ if (config.extends && typeof config.extends === "object") {
60
+ return resolveExtendsRef(config.extends as Record<string, string>, "production");
61
+ }
62
+
63
+ return undefined;
64
+ }
65
+
66
+ function parseBosRef(ref: string): { account: string; gateway: string } | null {
67
+ const match = ref.match(/^bos:\/\/([^/]+)\/(.+)$/);
68
+ if (!match?.[1] || !match[2]) return null;
69
+ return { account: match[1], gateway: match[2] };
70
+ }
71
+
72
+ async function loadParentPluginOptions(projectDir: string): Promise<{
73
+ localConfig: Record<string, unknown>;
74
+ parentPlugins: Record<string, unknown>;
75
+ newPluginKeys: string[];
76
+ } | null> {
77
+ const configPath = join(projectDir, "bos.config.json");
78
+ if (!existsSync(configPath)) {
79
+ return null;
80
+ }
81
+
82
+ const localConfig = JSON.parse(readFileSync(configPath, "utf-8")) as Record<string, unknown>;
83
+ const extendsRef = getExtendsRef(localConfig);
84
+ if (!extendsRef?.startsWith("bos://")) {
85
+ return null;
86
+ }
87
+
88
+ const parsed = parseBosRef(extendsRef);
89
+ if (!parsed) {
90
+ return null;
91
+ }
92
+
93
+ let parentConfig: Record<string, unknown>;
94
+ try {
95
+ parentConfig = await fetchParentConfig(parsed.account, parsed.gateway);
96
+ } catch {
97
+ return null;
98
+ }
99
+
100
+ const parentPlugins =
101
+ parentConfig.plugins && typeof parentConfig.plugins === "object"
102
+ ? (parentConfig.plugins as Record<string, unknown>)
103
+ : {};
104
+ const localPlugins =
105
+ localConfig.plugins && typeof localConfig.plugins === "object"
106
+ ? (localConfig.plugins as Record<string, unknown>)
107
+ : {};
108
+
109
+ const newPluginKeys = Object.keys(parentPlugins).filter((key) => !(key in localPlugins));
110
+ return { localConfig, parentPlugins, newPluginKeys };
111
+ }
112
+
113
+ async function addSelectedParentPlugins(projectDir: string): Promise<string[]> {
114
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
115
+ return [];
116
+ }
117
+
118
+ const pluginOptions = await loadParentPluginOptions(projectDir);
119
+ if (!pluginOptions || pluginOptions.newPluginKeys.length === 0) {
120
+ return [];
121
+ }
122
+
123
+ const selectedValue = await p.multiselect({
124
+ message: "Select new plugins from parent:",
125
+ options: pluginOptions.newPluginKeys.map((key) => ({ value: key, label: key })),
126
+ required: false,
127
+ });
128
+
129
+ if (p.isCancel(selectedValue)) {
130
+ process.exit(0);
131
+ }
132
+
133
+ const selected = selectedValue as string[];
134
+ if (selected.length === 0) {
135
+ return [];
136
+ }
137
+
138
+ const localPlugins =
139
+ pluginOptions.localConfig.plugins && typeof pluginOptions.localConfig.plugins === "object"
140
+ ? (pluginOptions.localConfig.plugins as Record<string, unknown>)
141
+ : {};
142
+ const nextPlugins = { ...localPlugins };
143
+ for (const key of selected) {
144
+ nextPlugins[key] = pluginOptions.parentPlugins[key];
145
+ }
146
+
147
+ pluginOptions.localConfig.plugins = nextPlugins;
148
+ await saveBosConfig(projectDir, pluginOptions.localConfig);
149
+
150
+ return selected;
151
+ }
152
+
50
153
  async function fetchLatestNpmVersion(packageName: string): Promise<string | null> {
51
154
  try {
52
155
  const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
@@ -269,6 +372,7 @@ export async function upgradeTemplate(
269
372
 
270
373
  if (options.dryRun) {
271
374
  let changelogUrl: string | undefined;
375
+ const pluginOptions = options.noSync ? null : await loadParentPluginOptions(projectDir);
272
376
  if (hasUpdates) {
273
377
  const configPath = join(projectDir, "bos.config.json");
274
378
  let parentConfig: Record<string, unknown> | null = null;
@@ -289,6 +393,7 @@ export async function upgradeTemplate(
289
393
  ...packages,
290
394
  ...catalogVersionUpdates.map((u) => ({ name: u.name, from: u.from, to: u.to })),
291
395
  ],
396
+ availablePlugins: pluginOptions?.newPluginKeys,
292
397
  changelogUrl,
293
398
  };
294
399
  }
@@ -315,13 +420,13 @@ export async function upgradeTemplate(
315
420
  }
316
421
  }
317
422
 
318
- if (hasUpdates && !options.noInstall) {
319
- await runBunInstall(projectDir);
320
- await runTypesGen(projectDir);
321
- }
322
-
323
423
  let syncResult: UpgradeResult["sync"];
424
+ let addedPlugins: string[] = [];
324
425
  if (!options.noSync) {
426
+ if (!options.dryRun) {
427
+ addedPlugins = await addSelectedParentPlugins(projectDir);
428
+ }
429
+
325
430
  syncResult = await syncTemplate(projectDir, {
326
431
  dryRun: false,
327
432
  force: options.force,
@@ -329,6 +434,11 @@ export async function upgradeTemplate(
329
434
  });
330
435
  }
331
436
 
437
+ if ((hasUpdates || addedPlugins.length > 0) && !options.noInstall) {
438
+ await runBunInstall(projectDir);
439
+ await runTypesGen(projectDir);
440
+ }
441
+
332
442
  const migratedFiles = await rewriteLegacyUiImports(projectDir);
333
443
  for (const file of OBSOLETE_FILES) {
334
444
  const filePath = join(projectDir, file);
@@ -359,6 +469,7 @@ export async function upgradeTemplate(
359
469
  ],
360
470
  sync: syncResult,
361
471
  migrated: migratedFiles.length > 0 ? migratedFiles : undefined,
472
+ selectedPlugins: addedPlugins.length > 0 ? addedPlugins : undefined,
362
473
  changelogUrl,
363
474
  };
364
475
  }
package/src/cli.ts CHANGED
@@ -240,6 +240,12 @@ async function main() {
240
240
  if (result.changelogUrl) {
241
241
  console.log(` ${colors.dim("Changelog:")} ${result.changelogUrl}`);
242
242
  }
243
+ if (result.availablePlugins && result.availablePlugins.length > 0) {
244
+ console.log(` ${colors.dim("New parent plugins:")} ${result.availablePlugins.join(", ")}`);
245
+ }
246
+ if (result.selectedPlugins && result.selectedPlugins.length > 0) {
247
+ console.log(` ${colors.dim("Added plugins:")} ${result.selectedPlugins.join(", ")}`);
248
+ }
243
249
  if (result.sync) {
244
250
  const sync = result.sync;
245
251
  if (sync.updated.length > 0) {
package/src/config.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, isAbsolute, join, resolve } from "node:path";
3
- import { fetchBosConfigFromFastKv, fetchPluginFromRegistry, parsePluginBosUrl } from "./fastkv";
3
+ import { fetchBosConfigFromFastKv } from "./fastkv";
4
4
  import {
5
5
  type BosEnv,
6
6
  isPlainObject,
@@ -546,31 +546,6 @@ async function resolveRemotePluginRuntimeName(baseUrl: string, fallback: string)
546
546
  }
547
547
  }
548
548
 
549
- interface ResolvedBosPlugin {
550
- url: string;
551
- integrity?: string;
552
- }
553
-
554
- async function resolveBosPluginUrl(bosUrl: string): Promise<ResolvedBosPlugin | null> {
555
- const parsed = parsePluginBosUrl(bosUrl);
556
- if (!parsed) return null;
557
-
558
- try {
559
- const entry = await fetchPluginFromRegistry(parsed.accountId, parsed.pluginName);
560
- if (!entry) return null;
561
-
562
- const cdnUrl = entry.metadata.cdnUrl;
563
- if (!cdnUrl) return null;
564
-
565
- return {
566
- url: cdnUrl,
567
- integrity: entry.metadata.integrity ?? undefined,
568
- };
569
- } catch {
570
- return null;
571
- }
572
- }
573
-
574
549
  async function buildRuntimePluginConfig(
575
550
  pluginId: string,
576
551
  config: BosConfigInput,
@@ -586,16 +561,12 @@ async function buildRuntimePluginConfig(
586
561
  const sourceProduction = typeof source.production === "string" ? source.production : undefined;
587
562
  const proxy = typeof apiConfig.proxy === "string" ? apiConfig.proxy : undefined;
588
563
  const development = apiDevelopment ?? sourceDevelopment;
589
- let production = apiProduction ?? sourceProduction;
564
+ const production = apiProduction ?? sourceProduction;
590
565
 
591
566
  if (production?.startsWith("bos://")) {
592
- const resolved = await resolveBosPluginUrl(production);
593
- if (resolved) {
594
- production = resolved.url;
595
- if (resolved.integrity && env === "production") {
596
- source.integrity = resolved.integrity;
597
- }
598
- }
567
+ throw new Error(
568
+ `Plugin "${pluginId}" has unsupported production target "${production}". Use extends: "bos://account/domain" for plugin configs or a CDN URL for production.`,
569
+ );
599
570
  }
600
571
 
601
572
  const runtimeTarget =
@@ -44,7 +44,7 @@ export const cliCommandMeta = {
44
44
  fields: {
45
45
  source: {
46
46
  positional: true,
47
- description: "Plugin source (local:path, bos://account/plugins/name, or URL)",
47
+ description: "Plugin source (local:path, bos://account/domain, or URL)",
48
48
  },
49
49
  as: { description: "Plugin alias" },
50
50
  production: { description: "Production URL override" },
@@ -113,7 +113,7 @@ export const cliCommandMeta = {
113
113
  upgrade: {
114
114
  commandPath: ["upgrade"],
115
115
  summary: "Upgrade framework packages and sync template files",
116
- interactive: false,
116
+ interactive: true,
117
117
  fields: {
118
118
  dryRun: { description: "Preview changes without writing" },
119
119
  force: { description: "Overwrite user-modified files during sync" },
package/src/contract.ts CHANGED
@@ -201,6 +201,8 @@ export const UpgradeResultSchema = z.object({
201
201
  ),
202
202
  sync: SyncResultSchema.optional(),
203
203
  migrated: z.array(z.string()).optional(),
204
+ availablePlugins: z.array(z.string()).optional(),
205
+ selectedPlugins: z.array(z.string()).optional(),
204
206
  changelogUrl: z.string().optional(),
205
207
  error: z.string().optional(),
206
208
  });
package/src/fastkv.ts CHANGED
@@ -139,78 +139,6 @@ export interface PluginManifest {
139
139
  additionalExports?: Array<{ path: string; exports: string[]; sha256: string }>;
140
140
  }
141
141
 
142
- export interface PluginMetadata {
143
- title: string | null;
144
- description: string | null;
145
- repoUrl: string | null;
146
- version: string;
147
- publishedAt: string;
148
- cdnUrl: string;
149
- integrity: string | null;
150
- }
151
-
152
- export interface PluginRegistryEntry {
153
- manifest: PluginManifest;
154
- metadata: PluginMetadata;
155
- }
156
-
157
- export function parsePluginBosUrl(
158
- source: string,
159
- ): { accountId: string; pluginName: string } | null {
160
- if (!source.startsWith("bos://")) return null;
161
- const match = source.match(/^bos:\/\/([^/]+)\/plugins\/([^/]+)$/);
162
- if (!match?.[1] || !match[2]) return null;
163
- return { accountId: match[1], pluginName: match[2] };
164
- }
165
-
166
- async function fetchKvValue(accountId: string, key: string): Promise<unknown | null> {
167
- const payload = await fetchJson<FastKvListResponse>(
168
- `${getFastKvBaseUrlForAccount(accountId)}/v0/latest/${encodeURIComponent(getRegistryNamespaceForAccount(accountId))}/${encodeURIComponent(accountId)}`,
169
- {
170
- method: "POST",
171
- body: JSON.stringify({ key, limit: 1 }),
172
- },
173
- );
174
- const value = payload?.entries?.find(Boolean)?.value;
175
- if (value == null) return null;
176
- if (typeof value === "string") {
177
- try {
178
- return JSON.parse(value);
179
- } catch {
180
- return null;
181
- }
182
- }
183
- return value;
184
- }
185
-
186
- export async function fetchPluginFromRegistry(
187
- accountId: string,
188
- pluginName: string,
189
- ): Promise<PluginRegistryEntry | null> {
190
- const manifestKey = `plugins/${accountId}/${pluginName}/manifest.json`;
191
- const metadataKey = `plugins/${accountId}/${pluginName}/metadata`;
192
-
193
- const [rawManifest, rawMetadata] = await Promise.all([
194
- fetchKvValue(accountId, manifestKey),
195
- fetchKvValue(accountId, metadataKey),
196
- ]);
197
-
198
- if (!rawManifest || typeof rawManifest !== "object") return null;
199
-
200
- return {
201
- manifest: rawManifest as PluginManifest,
202
- metadata: (rawMetadata ?? {
203
- title: null,
204
- description: null,
205
- repoUrl: null,
206
- version: "",
207
- publishedAt: "",
208
- cdnUrl: "",
209
- integrity: null,
210
- }) as PluginMetadata,
211
- };
212
- }
213
-
214
142
  export async function fetchRemotePluginManifest(cdnUrl: string): Promise<PluginManifest | null> {
215
143
  try {
216
144
  const baseUrl = cdnUrl.replace(/\/$/, "");
package/src/merge.ts CHANGED
@@ -95,28 +95,15 @@ export function mergeBosConfigWithExtends(
95
95
  parent: BosConfigInput,
96
96
  child: BosConfigInput,
97
97
  ): BosConfigInput {
98
- const merged = bosConfigMerger(child, parent) as BosConfigInput;
99
-
100
- if (isPlainObject(parent.plugins) && isPlainObject(child.plugins)) {
101
- const plugins: Record<string, unknown> = { ...parent.plugins };
102
- for (const [key, rawValue] of Object.entries(child.plugins)) {
103
- const value = rawValue as unknown;
104
- if (value === null || value === false) {
105
- delete plugins[key];
106
- } else if (isPlainObject(plugins[key]) && isPlainObject(value)) {
107
- plugins[key] = bosConfigMerger(
108
- value as Record<string, unknown>,
109
- plugins[key] as Record<string, unknown>,
110
- );
111
- } else {
112
- plugins[key] = value;
113
- }
114
- }
115
- (merged as Record<string, unknown>).plugins = plugins;
116
- } else if (child.plugins !== undefined) {
98
+ const { plugins: _ignoredParentPlugins, ...parentWithoutPlugins } = parent;
99
+ const merged = bosConfigMerger(child, parentWithoutPlugins) as BosConfigInput;
100
+
101
+ if (child.plugins !== undefined && isPlainObject(child.plugins)) {
117
102
  (merged as Record<string, unknown>).plugins = cleanNullSentinels(
118
103
  child.plugins as Record<string, unknown>,
119
104
  );
105
+ } else {
106
+ delete (merged as Record<string, unknown>).plugins;
120
107
  }
121
108
 
122
109
  const mergedRecord = merged as Record<string, unknown>;