everything-dev 1.4.1 → 1.6.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 (50) hide show
  1. package/dist/cli/init.cjs +78 -8
  2. package/dist/cli/init.cjs.map +1 -1
  3. package/dist/cli/init.d.cts +6 -0
  4. package/dist/cli/init.d.cts.map +1 -1
  5. package/dist/cli/init.d.mts +6 -0
  6. package/dist/cli/init.d.mts.map +1 -1
  7. package/dist/cli/init.mjs +78 -8
  8. package/dist/cli/init.mjs.map +1 -1
  9. package/dist/cli/prompts.cjs +64 -6
  10. package/dist/cli/prompts.cjs.map +1 -1
  11. package/dist/cli/prompts.mjs +64 -6
  12. package/dist/cli/prompts.mjs.map +1 -1
  13. package/dist/cli/sync.cjs +85 -18
  14. package/dist/cli/sync.cjs.map +1 -1
  15. package/dist/cli/sync.mjs +85 -18
  16. package/dist/cli/sync.mjs.map +1 -1
  17. package/dist/cli.cjs +41 -4
  18. package/dist/cli.cjs.map +1 -1
  19. package/dist/cli.mjs +41 -4
  20. package/dist/cli.mjs.map +1 -1
  21. package/dist/contract.cjs +3 -0
  22. package/dist/contract.cjs.map +1 -1
  23. package/dist/contract.d.cts +14 -6
  24. package/dist/contract.d.cts.map +1 -1
  25. package/dist/contract.d.mts +14 -6
  26. package/dist/contract.d.mts.map +1 -1
  27. package/dist/contract.mjs +3 -0
  28. package/dist/contract.mjs.map +1 -1
  29. package/dist/plugin.cjs +48 -17
  30. package/dist/plugin.cjs.map +1 -1
  31. package/dist/plugin.d.cts +9 -4
  32. package/dist/plugin.d.mts +9 -4
  33. package/dist/plugin.mjs +48 -17
  34. package/dist/plugin.mjs.map +1 -1
  35. package/dist/types.cjs +2 -1
  36. package/dist/types.cjs.map +1 -1
  37. package/dist/types.d.cts +4 -2
  38. package/dist/types.d.cts.map +1 -1
  39. package/dist/types.d.mts +4 -2
  40. package/dist/types.d.mts.map +1 -1
  41. package/dist/types.mjs +2 -1
  42. package/dist/types.mjs.map +1 -1
  43. package/package.json +2 -2
  44. package/src/cli/init.ts +122 -7
  45. package/src/cli/prompts.ts +84 -10
  46. package/src/cli/sync.ts +142 -17
  47. package/src/cli.ts +55 -4
  48. package/src/contract.ts +3 -0
  49. package/src/plugin.ts +51 -18
  50. package/src/types.ts +1 -0
package/src/cli/init.ts CHANGED
@@ -148,7 +148,7 @@ export async function copyFilteredFiles(
148
148
  sourceDir: string,
149
149
  destination: string,
150
150
  patterns: string[],
151
- options: { withHost: boolean },
151
+ options: { withHost: boolean; plugins?: string[]; pluginRoutes?: Record<string, string[]> },
152
152
  ): Promise<number> {
153
153
  if (patterns.length === 0) {
154
154
  return 0;
@@ -158,8 +158,24 @@ export async function copyFilteredFiles(
158
158
  ? [...patterns, "host/**"]
159
159
  : patterns.filter((p) => !p.startsWith("host/") && p !== "host/**");
160
160
 
161
+ const filteredPatterns = effectivePatterns.filter((p) => {
162
+ const pluginMatch = p.match(/^plugins\/([^/]+)/);
163
+ if (!pluginMatch) return true;
164
+ const pluginName = pluginMatch[1];
165
+ return options.plugins?.includes(pluginName) ?? true;
166
+ });
167
+
168
+ const excludedRoutePatterns: string[] = [];
169
+ if (options.pluginRoutes) {
170
+ for (const [pluginKey, routePatterns] of Object.entries(options.pluginRoutes)) {
171
+ if (!(options.plugins?.includes(pluginKey) ?? true)) {
172
+ excludedRoutePatterns.push(...routePatterns);
173
+ }
174
+ }
175
+ }
176
+
161
177
  const allFiles = new Set<string>();
162
- for (const pattern of effectivePatterns) {
178
+ for (const pattern of filteredPatterns) {
163
179
  const matches = await glob(pattern, {
164
180
  cwd: sourceDir,
165
181
  nodir: true,
@@ -167,10 +183,38 @@ export async function copyFilteredFiles(
167
183
  absolute: false,
168
184
  });
169
185
  for (const match of matches) {
186
+ const pluginMatch = match.match(/^plugins\/([^/]+)/);
187
+ if (pluginMatch) {
188
+ const pluginName = pluginMatch[1];
189
+ if (!(options.plugins?.includes(pluginName) ?? true)) continue;
190
+ }
191
+ if (isRouteExcluded(match, excludedRoutePatterns)) continue;
170
192
  allFiles.add(match);
171
193
  }
172
194
  }
173
195
 
196
+ const routeFiles = new Set<string>();
197
+ if (options.pluginRoutes) {
198
+ for (const [pluginKey, routePatterns] of Object.entries(options.pluginRoutes)) {
199
+ if (!(options.plugins?.includes(pluginKey) ?? true)) continue;
200
+ for (const rp of routePatterns) {
201
+ const matches = await glob(rp, {
202
+ cwd: sourceDir,
203
+ nodir: true,
204
+ dot: true,
205
+ absolute: false,
206
+ });
207
+ for (const match of matches) {
208
+ if (!isRouteExcluded(match, excludedRoutePatterns)) {
209
+ routeFiles.add(match);
210
+ }
211
+ }
212
+ }
213
+ }
214
+ }
215
+
216
+ for (const f of routeFiles) allFiles.add(f);
217
+
174
218
  mkdirSync(destination, { recursive: true });
175
219
 
176
220
  let count = 0;
@@ -179,7 +223,10 @@ export async function copyFilteredFiles(
179
223
  const stat = lstatSync(src);
180
224
  if (!stat.isFile()) continue;
181
225
 
182
- const dest = join(destination, filePath);
226
+ const destPath = filePath.startsWith(".templates/")
227
+ ? filePath.slice(".templates/".length)
228
+ : filePath;
229
+ const dest = join(destination, destPath);
183
230
  mkdirSync(dirname(dest), { recursive: true });
184
231
  const content = readFileSync(src);
185
232
  writeFileSync(dest, content);
@@ -189,6 +236,19 @@ export async function copyFilteredFiles(
189
236
  return count;
190
237
  }
191
238
 
239
+ function isRouteExcluded(filePath: string, excludedPatterns: string[]): boolean {
240
+ if (excludedPatterns.length === 0) return false;
241
+ for (const pattern of excludedPatterns) {
242
+ if (pattern.endsWith("/**")) {
243
+ const prefix = pattern.slice(0, -3);
244
+ if (filePath.startsWith(`${prefix}/`) || filePath === prefix) return true;
245
+ } else if (filePath === pattern || filePath.startsWith(`${pattern}/`)) {
246
+ return true;
247
+ }
248
+ }
249
+ return false;
250
+ }
251
+
192
252
  export async function personalizeConfig(
193
253
  destination: string,
194
254
  opts: {
@@ -196,6 +256,8 @@ export async function personalizeConfig(
196
256
  extendsGateway: string;
197
257
  account?: string;
198
258
  domain?: string;
259
+ plugins?: string[];
260
+ pluginRoutes?: Record<string, string[]>;
199
261
  workspaceOpts?: { localOverrides?: boolean; sourceDir?: string };
200
262
  },
201
263
  ): Promise<void> {
@@ -228,6 +290,15 @@ export async function personalizeConfig(
228
290
 
229
291
  if (config.plugins && typeof config.plugins === "object") {
230
292
  const plugins = config.plugins as Record<string, unknown>;
293
+
294
+ if (opts.plugins && opts.plugins.length > 0) {
295
+ for (const pluginKey of Object.keys(plugins)) {
296
+ if (!opts.plugins.includes(pluginKey)) {
297
+ delete plugins[pluginKey];
298
+ }
299
+ }
300
+ }
301
+
231
302
  for (const pluginKey of Object.keys(plugins)) {
232
303
  const plugin = plugins[pluginKey];
233
304
  if (plugin && typeof plugin === "object") {
@@ -236,6 +307,10 @@ export async function personalizeConfig(
236
307
  delete p.productionIntegrity;
237
308
  }
238
309
  }
310
+
311
+ if (Object.keys(plugins).length === 0) {
312
+ delete config.plugins;
313
+ }
239
314
  }
240
315
 
241
316
  writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
@@ -248,7 +323,13 @@ export async function personalizeConfig(
248
323
  if (pkg.workspaces && typeof pkg.workspaces === "object") {
249
324
  const ws = pkg.workspaces as { packages?: string[] };
250
325
  if (Array.isArray(ws.packages)) {
251
- ws.packages = ws.packages.filter((p: string) => p !== "host" && !p.startsWith("packages/"));
326
+ ws.packages = ws.packages
327
+ .filter((p: string) => p !== "host" && !p.startsWith("packages/"))
328
+ .filter((p: string) => {
329
+ const pluginMatch = p.match(/^plugins\/([^/]+)/);
330
+ if (!pluginMatch) return true;
331
+ return opts.plugins?.includes(pluginMatch[1]) ?? true;
332
+ });
252
333
  }
253
334
  }
254
335
 
@@ -322,7 +403,7 @@ async function resolveWorkspaceRefs(
322
403
  await normalizePackageManifestsInTree({
323
404
  sourceRootDir: options?.sourceDir ?? destination,
324
405
  targetDir: destination,
325
- resolveCatalogRefs: false,
406
+ resolveCatalogRefs: true,
326
407
  removeWorkspaceDeps: ["host"],
327
408
  });
328
409
 
@@ -363,12 +444,21 @@ export async function writeInitSnapshot(
363
444
  extendsGateway: string,
364
445
  sourceDir: string,
365
446
  patterns: string[],
366
- options: { withHost: boolean },
447
+ options: { withHost: boolean; plugins?: string[]; pluginRoutes?: Record<string, string[]> },
367
448
  ): Promise<void> {
368
449
  const effectivePatterns = options.withHost
369
450
  ? [...patterns, "host/**"]
370
451
  : patterns.filter((p) => !p.startsWith("host/") && p !== "host/**");
371
452
 
453
+ const excludedRoutePatterns: string[] = [];
454
+ if (options.pluginRoutes) {
455
+ for (const [pluginKey, routePatterns] of Object.entries(options.pluginRoutes)) {
456
+ if (!(options.plugins?.includes(pluginKey) ?? true)) {
457
+ excludedRoutePatterns.push(...routePatterns);
458
+ }
459
+ }
460
+ }
461
+
372
462
  const allFiles = new Set<string>();
373
463
  for (const pattern of effectivePatterns) {
374
464
  const matches = await glob(pattern, {
@@ -378,17 +468,42 @@ export async function writeInitSnapshot(
378
468
  absolute: false,
379
469
  });
380
470
  for (const match of matches) {
471
+ const pluginMatch = match.match(/^plugins\/([^/]+)/);
472
+ if (pluginMatch && !(options.plugins?.includes(pluginMatch[1]) ?? true)) continue;
473
+ if (isRouteExcluded(match, excludedRoutePatterns)) continue;
381
474
  allFiles.add(match);
382
475
  }
383
476
  }
384
477
 
478
+ if (options.pluginRoutes) {
479
+ for (const [pluginKey, routePatterns] of Object.entries(options.pluginRoutes)) {
480
+ if (!(options.plugins?.includes(pluginKey) ?? true)) continue;
481
+ for (const rp of routePatterns) {
482
+ const matches = await glob(rp, {
483
+ cwd: sourceDir,
484
+ nodir: true,
485
+ dot: true,
486
+ absolute: false,
487
+ });
488
+ for (const match of matches) {
489
+ if (!isRouteExcluded(match, excludedRoutePatterns)) {
490
+ allFiles.add(match);
491
+ }
492
+ }
493
+ }
494
+ }
495
+ }
496
+
385
497
  const fileHashes: Record<string, string> = {};
386
498
  for (const filePath of allFiles) {
387
499
  const src = join(sourceDir, filePath);
388
500
  const stat = lstatSync(src);
389
501
  if (!stat.isFile()) continue;
390
502
  const content = readFileSync(src);
391
- fileHashes[filePath] = computeHash(content);
503
+ const destPath = filePath.startsWith(".templates/")
504
+ ? filePath.slice(".templates/".length)
505
+ : filePath;
506
+ fileHashes[destPath] = computeHash(content);
392
507
  }
393
508
 
394
509
  await writeSnapshot(destination, {
@@ -25,9 +25,10 @@ export async function promptYesNo(question: string, defaultVal = false): Promise
25
25
  return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
26
26
  }
27
27
 
28
- function deriveDirectoryFromDomain(domain: string): string {
29
- const firstSegment = domain.split(".")[0];
30
- return firstSegment || domain;
28
+ function parseExtendsRef(ref: string): { account: string; gateway: string } | null {
29
+ const match = ref.match(/^(?:bos:\/\/)?([^/]+)\/(.+)$/);
30
+ if (!match) return null;
31
+ return { account: match[1], gateway: match[2] };
31
32
  }
32
33
 
33
34
  function deriveAccountFromDomain(domain: string, extendsAccount: string): string {
@@ -39,12 +40,73 @@ function deriveAccountFromDomain(domain: string, extendsAccount: string): string
39
40
  return `${firstSegment}.${suffix}`;
40
41
  }
41
42
 
43
+ const AVAILABLE_PLUGINS = [
44
+ {
45
+ key: "_template",
46
+ label: "template",
47
+ description: "Plugin scaffold and boilerplate",
48
+ default: true,
49
+ },
50
+ {
51
+ key: "registry",
52
+ label: "registry",
53
+ description: "FastKV app discovery and metadata",
54
+ default: false,
55
+ },
56
+ ];
57
+
58
+ async function promptPluginSelect(): Promise<string[]> {
59
+ const selected = new Set<string>(AVAILABLE_PLUGINS.filter((p) => p.default).map((p) => p.key));
60
+
61
+ console.log();
62
+ console.log(" Select plugins (enter number to toggle, enter to confirm):");
63
+ for (let i = 0; i < AVAILABLE_PLUGINS.length; i++) {
64
+ const p = AVAILABLE_PLUGINS[i];
65
+ const marker = selected.has(p.key) ? "●" : "○";
66
+ console.log(` ${marker} ${i + 1}. ${p.label} — ${p.description}`);
67
+ }
68
+ console.log();
69
+
70
+ while (true) {
71
+ const answer = await prompt(
72
+ " Plugins",
73
+ selected.size > 0 ? Array.from(selected).join(",") : "",
74
+ );
75
+ if (!answer) break;
76
+
77
+ const num = Number.parseInt(answer, 10);
78
+ if (num >= 1 && num <= AVAILABLE_PLUGINS.length) {
79
+ const plugin = AVAILABLE_PLUGINS[num - 1];
80
+ if (selected.has(plugin.key)) {
81
+ selected.delete(plugin.key);
82
+ } else {
83
+ selected.add(plugin.key);
84
+ }
85
+
86
+ console.log(" Current selection:");
87
+ for (let i = 0; i < AVAILABLE_PLUGINS.length; i++) {
88
+ const p = AVAILABLE_PLUGINS[i];
89
+ const marker = selected.has(p.key) ? "●" : "○";
90
+ console.log(` ${marker} ${i + 1}. ${p.label}`);
91
+ }
92
+ console.log();
93
+ continue;
94
+ }
95
+
96
+ break;
97
+ }
98
+
99
+ return Array.from(selected);
100
+ }
101
+
42
102
  export async function promptInitOptions(input: {
43
103
  extendsAccount?: string;
44
104
  extendsGateway?: string;
105
+ extends?: string;
45
106
  directory?: string;
46
107
  account?: string;
47
108
  domain?: string;
109
+ plugins?: string[];
48
110
  withHost?: boolean;
49
111
  }): Promise<{
50
112
  extendsAccount: string;
@@ -52,21 +114,32 @@ export async function promptInitOptions(input: {
52
114
  directory: string;
53
115
  account?: string;
54
116
  domain?: string;
117
+ plugins: string[];
55
118
  withHost: boolean;
56
119
  }> {
57
- const extendsAccount =
58
- input.extendsAccount || (await prompt("Extends account", "dev.everything.near"));
120
+ const domain = input.domain || (await prompt("Project domain"));
59
121
 
60
- const extendsGateway =
61
- input.extendsGateway || (await prompt("Extends gateway", "everything.dev"));
122
+ const extendsInput = input.extends || (await prompt("Extend from", ""));
123
+ let extendsAccount = input.extendsAccount || "";
124
+ let extendsGateway = input.extendsGateway || "";
62
125
 
63
- const domain = input.domain || (await prompt("Project domain"));
126
+ if (extendsInput) {
127
+ const parsed = parseExtendsRef(extendsInput);
128
+ if (parsed) {
129
+ extendsAccount = extendsAccount || parsed.account;
130
+ extendsGateway = extendsGateway || parsed.gateway;
131
+ }
132
+ }
133
+
134
+ extendsAccount = extendsAccount || "dev.everything.near";
135
+ extendsGateway = extendsGateway || "everything.dev";
64
136
 
65
137
  const accountDefault = domain ? deriveAccountFromDomain(domain, extendsAccount) : "";
66
138
  const account = input.account || (await prompt("Project NEAR account", accountDefault));
67
139
 
68
- const directoryDefault = domain ? deriveDirectoryFromDomain(domain) : extendsGateway;
69
- const directory = input.directory || (await prompt("Project directory", directoryDefault));
140
+ const directory = input.directory || domain || extendsGateway;
141
+
142
+ const plugins = input.plugins || (await promptPluginSelect());
70
143
 
71
144
  const withHost =
72
145
  input.withHost !== undefined ? input.withHost : await promptYesNo("Include host?", false);
@@ -77,6 +150,7 @@ export async function promptInitOptions(input: {
77
150
  directory,
78
151
  account: account || undefined,
79
152
  domain: domain || undefined,
153
+ plugins,
80
154
  withHost,
81
155
  };
82
156
  }
package/src/cli/sync.ts CHANGED
@@ -74,6 +74,76 @@ function backupFiles(projectDir: string, filePaths: string[]): string | null {
74
74
  return backupDir;
75
75
  }
76
76
 
77
+ function mergePackageJson(
78
+ local: Record<string, unknown>,
79
+ template: Record<string, unknown>,
80
+ ): Record<string, unknown> {
81
+ const merged = { ...template };
82
+
83
+ for (const depField of [
84
+ "dependencies",
85
+ "devDependencies",
86
+ "peerDependencies",
87
+ "overrides",
88
+ ] as const) {
89
+ const localDeps = local[depField] as Record<string, string> | undefined;
90
+ const templateDeps = template[depField] as Record<string, string> | undefined;
91
+
92
+ if (!localDeps && !templateDeps) continue;
93
+
94
+ const mergedDeps: Record<string, string> = { ...(templateDeps ?? {}) };
95
+
96
+ if (localDeps) {
97
+ for (const [name, version] of Object.entries(localDeps)) {
98
+ if (!(name in mergedDeps)) {
99
+ mergedDeps[name] = version;
100
+ }
101
+ }
102
+ }
103
+
104
+ if (Object.keys(mergedDeps).length > 0) {
105
+ merged[depField] = mergedDeps;
106
+ }
107
+ }
108
+
109
+ if (local.scripts && typeof local.scripts === "object") {
110
+ merged.scripts = {
111
+ ...((template.scripts as Record<string, string>) ?? {}),
112
+ ...(local.scripts as Record<string, string>),
113
+ };
114
+ }
115
+
116
+ return merged;
117
+ }
118
+
119
+ function toDestPath(filePath: string): string {
120
+ return filePath.startsWith(".templates/") ? filePath.slice(".templates/".length) : filePath;
121
+ }
122
+
123
+ function writeSyncedFile(sourceDir: string, projectDir: string, filePath: string): void {
124
+ const src = join(sourceDir, filePath);
125
+ const destPath = filePath.startsWith(".templates/")
126
+ ? filePath.slice(".templates/".length)
127
+ : filePath;
128
+ const dest = join(projectDir, destPath);
129
+ mkdirSync(dirname(dest), { recursive: true });
130
+
131
+ if (filePath.endsWith("package.json")) {
132
+ const localContent = existsSync(dest) ? readFileSync(dest, "utf-8") : null;
133
+ const templateContent = readFileSync(src, "utf-8");
134
+
135
+ if (localContent) {
136
+ const local = JSON.parse(localContent) as Record<string, unknown>;
137
+ const template = JSON.parse(templateContent) as Record<string, unknown>;
138
+ const merged = mergePackageJson(local, template);
139
+ writeFileSync(dest, `${JSON.stringify(merged, null, 2)}\n`);
140
+ return;
141
+ }
142
+ }
143
+
144
+ writeFileSync(dest, readFileSync(src));
145
+ }
146
+
77
147
  export async function syncTemplate(projectDir: string, options: SyncOptions): Promise<SyncResult> {
78
148
  const localConfig = JSON.parse(
79
149
  readFileSync(join(projectDir, "bos.config.json"), "utf-8"),
@@ -104,7 +174,10 @@ export async function syncTemplate(projectDir: string, options: SyncOptions): Pr
104
174
  const extendsAccount = extendsMatch[1];
105
175
  const extendsGateway = extendsMatch[2];
106
176
 
107
- const { sourceDir, cleanup } = await resolveSourceDir({ extendsAccount, extendsGateway });
177
+ const { sourceDir, parentConfig, cleanup } = await resolveSourceDir({
178
+ extendsAccount,
179
+ extendsGateway,
180
+ });
108
181
 
109
182
  try {
110
183
  const patterns = await readTemplatekeep(sourceDir);
@@ -135,40 +208,87 @@ export async function syncTemplate(projectDir: string, options: SyncOptions): Pr
135
208
  }
136
209
  }
137
210
 
211
+ const childPlugins =
212
+ localConfig.plugins && typeof localConfig.plugins === "object"
213
+ ? Object.keys(localConfig.plugins as Record<string, unknown>)
214
+ : [];
215
+
216
+ const pluginRoutes: Record<string, string[]> = {};
217
+ if (parentConfig.plugins) {
218
+ for (const [key, ref] of Object.entries(parentConfig.plugins)) {
219
+ if (ref.routes && ref.routes.length > 0) {
220
+ pluginRoutes[key] = ref.routes;
221
+ }
222
+ }
223
+ }
224
+
225
+ const excludedRoutePatterns: string[] = [];
226
+ for (const [pluginKey, routePatterns] of Object.entries(pluginRoutes)) {
227
+ if (!childPlugins.includes(pluginKey)) {
228
+ excludedRoutePatterns.push(...routePatterns);
229
+ }
230
+ }
231
+
232
+ const filteredFiles = new Set<string>();
233
+ for (const filePath of allTemplateFiles) {
234
+ const pluginMatch = filePath.match(/^plugins\/([^/]+)/);
235
+ if (pluginMatch && !childPlugins.includes(pluginMatch[1])) continue;
236
+ if (isExcluded(filePath, excludedRoutePatterns)) continue;
237
+ filteredFiles.add(filePath);
238
+ }
239
+
240
+ for (const [pluginKey, routePatterns] of Object.entries(pluginRoutes)) {
241
+ if (!childPlugins.includes(pluginKey)) continue;
242
+ for (const rp of routePatterns) {
243
+ const matches = await glob(rp, {
244
+ cwd: sourceDir,
245
+ nodir: true,
246
+ dot: true,
247
+ absolute: false,
248
+ });
249
+ for (const match of matches) {
250
+ if (!isExcluded(match, excludedRoutePatterns)) {
251
+ filteredFiles.add(match);
252
+ }
253
+ }
254
+ }
255
+ }
256
+
138
257
  const snapshot = await readSnapshot(projectDir);
139
258
 
140
259
  const updated: string[] = [];
141
260
  const skipped: string[] = [];
142
261
  const added: string[] = [];
143
262
 
144
- for (const filePath of allTemplateFiles) {
145
- if (isExcluded(filePath, excludePatterns)) continue;
263
+ for (const filePath of filteredFiles) {
264
+ const destPath = toDestPath(filePath);
265
+ if (isExcluded(destPath, excludePatterns)) continue;
146
266
 
147
- const localHash = computeLocalHash(projectDir, filePath);
267
+ const localHash = computeLocalHash(projectDir, destPath);
148
268
  const sourceContent = readFileSync(join(sourceDir, filePath));
149
269
  const sourceHash = createHash("sha256").update(sourceContent).digest("hex").substring(0, 16);
150
270
 
151
271
  if (localHash === null) {
152
- added.push(filePath);
272
+ added.push(destPath);
153
273
  continue;
154
274
  }
155
275
 
156
276
  if (localHash === sourceHash) continue;
157
277
 
158
- const snapshotHash = snapshot?.files[filePath];
278
+ const snapshotHash = snapshot?.files[destPath];
159
279
 
160
280
  if (snapshotHash === undefined) {
161
- updated.push(filePath);
281
+ updated.push(destPath);
162
282
  continue;
163
283
  }
164
284
 
165
285
  if (localHash === snapshotHash) {
166
- updated.push(filePath);
286
+ updated.push(destPath);
167
287
  } else {
168
288
  if (options.force) {
169
- updated.push(filePath);
289
+ updated.push(destPath);
170
290
  } else {
171
- skipped.push(filePath);
291
+ skipped.push(destPath);
172
292
  }
173
293
  }
174
294
  }
@@ -184,24 +304,27 @@ export async function syncTemplate(projectDir: string, options: SyncOptions): Pr
184
304
 
185
305
  const filesToWrite = [...updated, ...added].filter((f) => !isExcluded(f, excludePatterns));
186
306
 
307
+ const destToSource = new Map<string, string>();
308
+ for (const filePath of filteredFiles) {
309
+ destToSource.set(toDestPath(filePath), filePath);
310
+ }
311
+
187
312
  if (filesToWrite.length > 0) {
188
313
  backupFiles(projectDir, filesToWrite);
189
314
 
190
- for (const filePath of filesToWrite) {
191
- const src = join(sourceDir, filePath);
192
- const dest = join(projectDir, filePath);
193
- mkdirSync(dirname(dest), { recursive: true });
194
- writeFileSync(dest, readFileSync(src));
315
+ for (const destPath of filesToWrite) {
316
+ const sourcePath = destToSource.get(destPath) ?? destPath;
317
+ writeSyncedFile(sourceDir, projectDir, sourcePath);
195
318
  }
196
319
  }
197
320
 
198
321
  const newSnapshotFiles: Record<string, string> = {};
199
- for (const filePath of allTemplateFiles) {
322
+ for (const filePath of filteredFiles) {
200
323
  const src = join(sourceDir, filePath);
201
324
  const stat = lstatSync(src);
202
325
  if (!stat.isFile()) continue;
203
326
  const content = readFileSync(src);
204
- newSnapshotFiles[filePath] = createHash("sha256")
327
+ newSnapshotFiles[toDestPath(filePath)] = createHash("sha256")
205
328
  .update(content)
206
329
  .digest("hex")
207
330
  .substring(0, 16);
@@ -220,6 +343,8 @@ export async function syncTemplate(projectDir: string, options: SyncOptions): Pr
220
343
  extendsGateway,
221
344
  account,
222
345
  domain,
346
+ plugins: childPlugins,
347
+ pluginRoutes,
223
348
  workspaceOpts: { sourceDir },
224
349
  });
225
350
 
package/src/cli.ts CHANGED
@@ -113,6 +113,8 @@ async function main() {
113
113
  console.log(` ${colors.dim("Directory:")} ${result.directory}`);
114
114
  if (result.account) console.log(` ${colors.dim("Account:")} ${result.account}`);
115
115
  if (result.domain) console.log(` ${colors.dim("Domain:")} ${result.domain}`);
116
+ if (result.plugins && result.plugins.length > 0)
117
+ console.log(` ${colors.dim("Plugins:")} ${result.plugins.join(", ")}`);
116
118
  console.log(` ${colors.dim("Files copied:")} ${result.filesCopied}`);
117
119
  console.log();
118
120
  console.log(colors.dim(" Next steps:"));
@@ -156,6 +158,24 @@ async function main() {
156
158
  if (result.updated.length === 0 && result.added.length === 0 && result.skipped.length === 0) {
157
159
  console.log(` ${colors.dim("Already up to date")}`);
158
160
  }
161
+ if (result.status !== "dry-run" && result.updated.length > 0) {
162
+ console.log();
163
+ console.log(colors.dim(" Review changes — your customizations take priority:"));
164
+ console.log(
165
+ colors.dim(
166
+ " • api/src/contract.ts, api/src/index.ts, api/src/db/schema.ts — never overwritten",
167
+ ),
168
+ );
169
+ console.log(
170
+ colors.dim(" • ui/src/components/**, ui/src/styles.css — never overwritten"),
171
+ );
172
+ console.log(
173
+ colors.dim(
174
+ " • Other updated files — accept framework improvements, then restore your changes",
175
+ ),
176
+ );
177
+ console.log(colors.dim(" • Skipped files — yours already, only update with --force"));
178
+ }
159
179
  console.log();
160
180
  return;
161
181
  }
@@ -169,7 +189,7 @@ async function main() {
169
189
  if (result.status === "dry-run") {
170
190
  console.log(colors.cyan(`${icons.ok} Dry run — no changes applied`));
171
191
  } else {
172
- console.log(colors.green(`${icons.ok} Framework upgraded`));
192
+ console.log(colors.green(`${icons.ok} Upgrade successful`));
173
193
  }
174
194
  for (const pkg of result.packages) {
175
195
  if (pkg.from && pkg.from !== pkg.to) {
@@ -186,15 +206,46 @@ async function main() {
186
206
  if (result.sync) {
187
207
  const sync = result.sync;
188
208
  if (sync.updated.length > 0) {
189
- console.log(` ${colors.dim("Synced updated:")} ${sync.updated.length} file(s)`);
209
+ console.log(` ${colors.dim("Updated:")} ${sync.updated.length} file(s)`);
210
+ for (const f of sync.updated) console.log(` ${colors.dim(f)}`);
190
211
  }
191
212
  if (sync.added.length > 0) {
192
- console.log(` ${colors.dim("Synced added:")} ${sync.added.length} file(s)`);
213
+ console.log(` ${colors.dim("Added:")} ${sync.added.length} file(s)`);
214
+ for (const f of sync.added) console.log(` ${colors.dim(f)}`);
193
215
  }
194
216
  if (sync.skipped.length > 0) {
195
217
  console.log(
196
- ` ${colors.yellow("Synced skipped:")} ${sync.skipped.length} file(s) (locally modified, use --force to overwrite)`,
218
+ ` ${colors.yellow("Skipped:")} ${sync.skipped.length} file(s) (locally modified, use --force to overwrite)`,
219
+ );
220
+ for (const f of sync.skipped) console.log(` ${colors.dim(f)}`);
221
+ }
222
+ if (
223
+ result.status !== "dry-run" &&
224
+ (sync.updated.length > 0 || sync.added.length > 0 || sync.skipped.length > 0)
225
+ ) {
226
+ console.log();
227
+ console.log(colors.dim(" Resolve differences — your code takes priority:"));
228
+ console.log();
229
+ console.log(colors.dim(" Never overwritten (safe):"));
230
+ console.log(
231
+ colors.dim(" • api/src/contract.ts, api/src/index.ts, api/src/db/schema.ts"),
232
+ );
233
+ console.log(colors.dim(" • ui/src/components/**, ui/src/styles.css"));
234
+ console.log();
235
+ console.log(colors.dim(" Replaced — review and keep your changes:"));
236
+ console.log(
237
+ colors.dim(
238
+ " • api/drizzle.config.ts, api/tsconfig.json, api/tsconfig.contract.json",
239
+ ),
197
240
  );
241
+ console.log(colors.dim(" • api/plugin.dev.ts, api/rspack.config.js"));
242
+ console.log(colors.dim(" • ui/src/routes/* (core routes only)"));
243
+ console.log();
244
+ console.log(colors.dim(" Merged — your deps preserved:"));
245
+ console.log(colors.dim(" • package.json, api/package.json, ui/package.json"));
246
+ console.log();
247
+ console.log(colors.dim(" Skipped — already yours:"));
248
+ console.log(colors.dim(" • Use --force only if you want framework updates"));
198
249
  }
199
250
  }
200
251
  console.log();