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
@@ -20,22 +20,79 @@ async function promptYesNo(question, defaultVal = false) {
20
20
  if (!answer) return defaultVal;
21
21
  return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
22
22
  }
23
- function deriveDirectoryFromDomain(domain) {
24
- return domain.split(".")[0] || domain;
23
+ function parseExtendsRef(ref) {
24
+ const match = ref.match(/^(?:bos:\/\/)?([^/]+)\/(.+)$/);
25
+ if (!match) return null;
26
+ return {
27
+ account: match[1],
28
+ gateway: match[2]
29
+ };
25
30
  }
26
31
  function deriveAccountFromDomain(domain, extendsAccount) {
27
32
  const firstSegment = domain.split(".")[0];
28
33
  if (!firstSegment) return "";
29
34
  return `${firstSegment}.${extendsAccount.includes(".") ? extendsAccount.substring(extendsAccount.indexOf(".") + 1) : extendsAccount}`;
30
35
  }
36
+ const AVAILABLE_PLUGINS = [{
37
+ key: "_template",
38
+ label: "template",
39
+ description: "Plugin scaffold and boilerplate",
40
+ default: true
41
+ }, {
42
+ key: "registry",
43
+ label: "registry",
44
+ description: "FastKV app discovery and metadata",
45
+ default: false
46
+ }];
47
+ async function promptPluginSelect() {
48
+ const selected = new Set(AVAILABLE_PLUGINS.filter((p) => p.default).map((p) => p.key));
49
+ console.log();
50
+ console.log(" Select plugins (enter number to toggle, enter to confirm):");
51
+ for (let i = 0; i < AVAILABLE_PLUGINS.length; i++) {
52
+ const p = AVAILABLE_PLUGINS[i];
53
+ const marker = selected.has(p.key) ? "●" : "○";
54
+ console.log(` ${marker} ${i + 1}. ${p.label} — ${p.description}`);
55
+ }
56
+ console.log();
57
+ while (true) {
58
+ const answer = await prompt(" Plugins", selected.size > 0 ? Array.from(selected).join(",") : "");
59
+ if (!answer) break;
60
+ const num = Number.parseInt(answer, 10);
61
+ if (num >= 1 && num <= AVAILABLE_PLUGINS.length) {
62
+ const plugin = AVAILABLE_PLUGINS[num - 1];
63
+ if (selected.has(plugin.key)) selected.delete(plugin.key);
64
+ else selected.add(plugin.key);
65
+ console.log(" Current selection:");
66
+ for (let i = 0; i < AVAILABLE_PLUGINS.length; i++) {
67
+ const p = AVAILABLE_PLUGINS[i];
68
+ const marker = selected.has(p.key) ? "●" : "○";
69
+ console.log(` ${marker} ${i + 1}. ${p.label}`);
70
+ }
71
+ console.log();
72
+ continue;
73
+ }
74
+ break;
75
+ }
76
+ return Array.from(selected);
77
+ }
31
78
  async function promptInitOptions(input) {
32
- const extendsAccount = input.extendsAccount || await prompt("Extends account", "dev.everything.near");
33
- const extendsGateway = input.extendsGateway || await prompt("Extends gateway", "everything.dev");
34
79
  const domain = input.domain || await prompt("Project domain");
80
+ const extendsInput = input.extends || await prompt("Extend from", "");
81
+ let extendsAccount = input.extendsAccount || "";
82
+ let extendsGateway = input.extendsGateway || "";
83
+ if (extendsInput) {
84
+ const parsed = parseExtendsRef(extendsInput);
85
+ if (parsed) {
86
+ extendsAccount = extendsAccount || parsed.account;
87
+ extendsGateway = extendsGateway || parsed.gateway;
88
+ }
89
+ }
90
+ extendsAccount = extendsAccount || "dev.everything.near";
91
+ extendsGateway = extendsGateway || "everything.dev";
35
92
  const accountDefault = domain ? deriveAccountFromDomain(domain, extendsAccount) : "";
36
93
  const account = input.account || await prompt("Project NEAR account", accountDefault);
37
- const directoryDefault = domain ? deriveDirectoryFromDomain(domain) : extendsGateway;
38
- const directory = input.directory || await prompt("Project directory", directoryDefault);
94
+ const directory = input.directory || domain || extendsGateway;
95
+ const plugins = input.plugins || await promptPluginSelect();
39
96
  const withHost = input.withHost !== void 0 ? input.withHost : await promptYesNo("Include host?", false);
40
97
  return {
41
98
  extendsAccount,
@@ -43,6 +100,7 @@ async function promptInitOptions(input) {
43
100
  directory,
44
101
  account: account || void 0,
45
102
  domain: domain || void 0,
103
+ plugins,
46
104
  withHost
47
105
  };
48
106
  }
@@ -1 +1 @@
1
- {"version":3,"file":"prompts.cjs","names":[],"sources":["../../src/cli/prompts.ts"],"sourcesContent":["import { createInterface } from \"node:readline\";\n\nexport async function prompt(question: string, defaultValue?: string): Promise<string> {\n const rl = createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n const suffix = defaultValue ? ` [${defaultValue}]` : \"\";\n const fullQuestion = `${question}${suffix}: `;\n\n return new Promise<string>((resolve) => {\n rl.question(fullQuestion, (answer) => {\n rl.close();\n const trimmed = answer.trim();\n resolve(trimmed || defaultValue || \"\");\n });\n });\n}\n\nexport async function promptYesNo(question: string, defaultVal = false): Promise<boolean> {\n const hint = defaultVal ? \"Y/n\" : \"y/N\";\n const answer = await prompt(`${question} (${hint})`);\n if (!answer) return defaultVal;\n return answer.toLowerCase() === \"y\" || answer.toLowerCase() === \"yes\";\n}\n\nfunction deriveDirectoryFromDomain(domain: string): string {\n const firstSegment = domain.split(\".\")[0];\n return firstSegment || domain;\n}\n\nfunction deriveAccountFromDomain(domain: string, extendsAccount: string): string {\n const firstSegment = domain.split(\".\")[0];\n if (!firstSegment) return \"\";\n const suffix = extendsAccount.includes(\".\")\n ? extendsAccount.substring(extendsAccount.indexOf(\".\") + 1)\n : extendsAccount;\n return `${firstSegment}.${suffix}`;\n}\n\nexport async function promptInitOptions(input: {\n extendsAccount?: string;\n extendsGateway?: string;\n directory?: string;\n account?: string;\n domain?: string;\n withHost?: boolean;\n}): Promise<{\n extendsAccount: string;\n extendsGateway: string;\n directory: string;\n account?: string;\n domain?: string;\n withHost: boolean;\n}> {\n const extendsAccount =\n input.extendsAccount || (await prompt(\"Extends account\", \"dev.everything.near\"));\n\n const extendsGateway =\n input.extendsGateway || (await prompt(\"Extends gateway\", \"everything.dev\"));\n\n const domain = input.domain || (await prompt(\"Project domain\"));\n\n const accountDefault = domain ? deriveAccountFromDomain(domain, extendsAccount) : \"\";\n const account = input.account || (await prompt(\"Project NEAR account\", accountDefault));\n\n const directoryDefault = domain ? deriveDirectoryFromDomain(domain) : extendsGateway;\n const directory = input.directory || (await prompt(\"Project directory\", directoryDefault));\n\n const withHost =\n input.withHost !== undefined ? input.withHost : await promptYesNo(\"Include host?\", false);\n\n return {\n extendsAccount,\n extendsGateway,\n directory,\n account: account || undefined,\n domain: domain || undefined,\n withHost,\n };\n}\n"],"mappings":";;;;AAEA,eAAsB,OAAO,UAAkB,cAAwC;CACrF,MAAM,wCAAqB;EACzB,OAAO,QAAQ;EACf,QAAQ,QAAQ;EACjB,CAAC;CAGF,MAAM,eAAe,GAAG,WADT,eAAe,KAAK,aAAa,KAAK,GACX;AAE1C,QAAO,IAAI,SAAiB,YAAY;AACtC,KAAG,SAAS,eAAe,WAAW;AACpC,MAAG,OAAO;AAEV,WADgB,OAAO,MAAM,IACV,gBAAgB,GAAG;IACtC;GACF;;AAGJ,eAAsB,YAAY,UAAkB,aAAa,OAAyB;CAExF,MAAM,SAAS,MAAM,OAAO,GAAG,SAAS,IAD3B,aAAa,QAAQ,MACe,GAAG;AACpD,KAAI,CAAC,OAAQ,QAAO;AACpB,QAAO,OAAO,aAAa,KAAK,OAAO,OAAO,aAAa,KAAK;;AAGlE,SAAS,0BAA0B,QAAwB;AAEzD,QADqB,OAAO,MAAM,IAAI,CAAC,MAChB;;AAGzB,SAAS,wBAAwB,QAAgB,gBAAgC;CAC/E,MAAM,eAAe,OAAO,MAAM,IAAI,CAAC;AACvC,KAAI,CAAC,aAAc,QAAO;AAI1B,QAAO,GAAG,aAAa,GAHR,eAAe,SAAS,IAAI,GACvC,eAAe,UAAU,eAAe,QAAQ,IAAI,GAAG,EAAE,GACzD;;AAIN,eAAsB,kBAAkB,OAcrC;CACD,MAAM,iBACJ,MAAM,kBAAmB,MAAM,OAAO,mBAAmB,sBAAsB;CAEjF,MAAM,iBACJ,MAAM,kBAAmB,MAAM,OAAO,mBAAmB,iBAAiB;CAE5E,MAAM,SAAS,MAAM,UAAW,MAAM,OAAO,iBAAiB;CAE9D,MAAM,iBAAiB,SAAS,wBAAwB,QAAQ,eAAe,GAAG;CAClF,MAAM,UAAU,MAAM,WAAY,MAAM,OAAO,wBAAwB,eAAe;CAEtF,MAAM,mBAAmB,SAAS,0BAA0B,OAAO,GAAG;CACtE,MAAM,YAAY,MAAM,aAAc,MAAM,OAAO,qBAAqB,iBAAiB;CAEzF,MAAM,WACJ,MAAM,aAAa,SAAY,MAAM,WAAW,MAAM,YAAY,iBAAiB,MAAM;AAE3F,QAAO;EACL;EACA;EACA;EACA,SAAS,WAAW;EACpB,QAAQ,UAAU;EAClB;EACD"}
1
+ {"version":3,"file":"prompts.cjs","names":[],"sources":["../../src/cli/prompts.ts"],"sourcesContent":["import { createInterface } from \"node:readline\";\n\nexport async function prompt(question: string, defaultValue?: string): Promise<string> {\n const rl = createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n const suffix = defaultValue ? ` [${defaultValue}]` : \"\";\n const fullQuestion = `${question}${suffix}: `;\n\n return new Promise<string>((resolve) => {\n rl.question(fullQuestion, (answer) => {\n rl.close();\n const trimmed = answer.trim();\n resolve(trimmed || defaultValue || \"\");\n });\n });\n}\n\nexport async function promptYesNo(question: string, defaultVal = false): Promise<boolean> {\n const hint = defaultVal ? \"Y/n\" : \"y/N\";\n const answer = await prompt(`${question} (${hint})`);\n if (!answer) return defaultVal;\n return answer.toLowerCase() === \"y\" || answer.toLowerCase() === \"yes\";\n}\n\nfunction parseExtendsRef(ref: string): { account: string; gateway: string } | null {\n const match = ref.match(/^(?:bos:\\/\\/)?([^/]+)\\/(.+)$/);\n if (!match) return null;\n return { account: match[1], gateway: match[2] };\n}\n\nfunction deriveAccountFromDomain(domain: string, extendsAccount: string): string {\n const firstSegment = domain.split(\".\")[0];\n if (!firstSegment) return \"\";\n const suffix = extendsAccount.includes(\".\")\n ? extendsAccount.substring(extendsAccount.indexOf(\".\") + 1)\n : extendsAccount;\n return `${firstSegment}.${suffix}`;\n}\n\nconst AVAILABLE_PLUGINS = [\n {\n key: \"_template\",\n label: \"template\",\n description: \"Plugin scaffold and boilerplate\",\n default: true,\n },\n {\n key: \"registry\",\n label: \"registry\",\n description: \"FastKV app discovery and metadata\",\n default: false,\n },\n];\n\nasync function promptPluginSelect(): Promise<string[]> {\n const selected = new Set<string>(AVAILABLE_PLUGINS.filter((p) => p.default).map((p) => p.key));\n\n console.log();\n console.log(\" Select plugins (enter number to toggle, enter to confirm):\");\n for (let i = 0; i < AVAILABLE_PLUGINS.length; i++) {\n const p = AVAILABLE_PLUGINS[i];\n const marker = selected.has(p.key) ? \"●\" : \"○\";\n console.log(` ${marker} ${i + 1}. ${p.label} — ${p.description}`);\n }\n console.log();\n\n while (true) {\n const answer = await prompt(\n \" Plugins\",\n selected.size > 0 ? Array.from(selected).join(\",\") : \"\",\n );\n if (!answer) break;\n\n const num = Number.parseInt(answer, 10);\n if (num >= 1 && num <= AVAILABLE_PLUGINS.length) {\n const plugin = AVAILABLE_PLUGINS[num - 1];\n if (selected.has(plugin.key)) {\n selected.delete(plugin.key);\n } else {\n selected.add(plugin.key);\n }\n\n console.log(\" Current selection:\");\n for (let i = 0; i < AVAILABLE_PLUGINS.length; i++) {\n const p = AVAILABLE_PLUGINS[i];\n const marker = selected.has(p.key) ? \"●\" : \"○\";\n console.log(` ${marker} ${i + 1}. ${p.label}`);\n }\n console.log();\n continue;\n }\n\n break;\n }\n\n return Array.from(selected);\n}\n\nexport async function promptInitOptions(input: {\n extendsAccount?: string;\n extendsGateway?: string;\n extends?: string;\n directory?: string;\n account?: string;\n domain?: string;\n plugins?: string[];\n withHost?: boolean;\n}): Promise<{\n extendsAccount: string;\n extendsGateway: string;\n directory: string;\n account?: string;\n domain?: string;\n plugins: string[];\n withHost: boolean;\n}> {\n const domain = input.domain || (await prompt(\"Project domain\"));\n\n const extendsInput = input.extends || (await prompt(\"Extend from\", \"\"));\n let extendsAccount = input.extendsAccount || \"\";\n let extendsGateway = input.extendsGateway || \"\";\n\n if (extendsInput) {\n const parsed = parseExtendsRef(extendsInput);\n if (parsed) {\n extendsAccount = extendsAccount || parsed.account;\n extendsGateway = extendsGateway || parsed.gateway;\n }\n }\n\n extendsAccount = extendsAccount || \"dev.everything.near\";\n extendsGateway = extendsGateway || \"everything.dev\";\n\n const accountDefault = domain ? deriveAccountFromDomain(domain, extendsAccount) : \"\";\n const account = input.account || (await prompt(\"Project NEAR account\", accountDefault));\n\n const directory = input.directory || domain || extendsGateway;\n\n const plugins = input.plugins || (await promptPluginSelect());\n\n const withHost =\n input.withHost !== undefined ? input.withHost : await promptYesNo(\"Include host?\", false);\n\n return {\n extendsAccount,\n extendsGateway,\n directory,\n account: account || undefined,\n domain: domain || undefined,\n plugins,\n withHost,\n };\n}\n"],"mappings":"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"}
@@ -19,22 +19,79 @@ async function promptYesNo(question, defaultVal = false) {
19
19
  if (!answer) return defaultVal;
20
20
  return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
21
21
  }
22
- function deriveDirectoryFromDomain(domain) {
23
- return domain.split(".")[0] || domain;
22
+ function parseExtendsRef(ref) {
23
+ const match = ref.match(/^(?:bos:\/\/)?([^/]+)\/(.+)$/);
24
+ if (!match) return null;
25
+ return {
26
+ account: match[1],
27
+ gateway: match[2]
28
+ };
24
29
  }
25
30
  function deriveAccountFromDomain(domain, extendsAccount) {
26
31
  const firstSegment = domain.split(".")[0];
27
32
  if (!firstSegment) return "";
28
33
  return `${firstSegment}.${extendsAccount.includes(".") ? extendsAccount.substring(extendsAccount.indexOf(".") + 1) : extendsAccount}`;
29
34
  }
35
+ const AVAILABLE_PLUGINS = [{
36
+ key: "_template",
37
+ label: "template",
38
+ description: "Plugin scaffold and boilerplate",
39
+ default: true
40
+ }, {
41
+ key: "registry",
42
+ label: "registry",
43
+ description: "FastKV app discovery and metadata",
44
+ default: false
45
+ }];
46
+ async function promptPluginSelect() {
47
+ const selected = new Set(AVAILABLE_PLUGINS.filter((p) => p.default).map((p) => p.key));
48
+ console.log();
49
+ console.log(" Select plugins (enter number to toggle, enter to confirm):");
50
+ for (let i = 0; i < AVAILABLE_PLUGINS.length; i++) {
51
+ const p = AVAILABLE_PLUGINS[i];
52
+ const marker = selected.has(p.key) ? "●" : "○";
53
+ console.log(` ${marker} ${i + 1}. ${p.label} — ${p.description}`);
54
+ }
55
+ console.log();
56
+ while (true) {
57
+ const answer = await prompt(" Plugins", selected.size > 0 ? Array.from(selected).join(",") : "");
58
+ if (!answer) break;
59
+ const num = Number.parseInt(answer, 10);
60
+ if (num >= 1 && num <= AVAILABLE_PLUGINS.length) {
61
+ const plugin = AVAILABLE_PLUGINS[num - 1];
62
+ if (selected.has(plugin.key)) selected.delete(plugin.key);
63
+ else selected.add(plugin.key);
64
+ console.log(" Current selection:");
65
+ for (let i = 0; i < AVAILABLE_PLUGINS.length; i++) {
66
+ const p = AVAILABLE_PLUGINS[i];
67
+ const marker = selected.has(p.key) ? "●" : "○";
68
+ console.log(` ${marker} ${i + 1}. ${p.label}`);
69
+ }
70
+ console.log();
71
+ continue;
72
+ }
73
+ break;
74
+ }
75
+ return Array.from(selected);
76
+ }
30
77
  async function promptInitOptions(input) {
31
- const extendsAccount = input.extendsAccount || await prompt("Extends account", "dev.everything.near");
32
- const extendsGateway = input.extendsGateway || await prompt("Extends gateway", "everything.dev");
33
78
  const domain = input.domain || await prompt("Project domain");
79
+ const extendsInput = input.extends || await prompt("Extend from", "");
80
+ let extendsAccount = input.extendsAccount || "";
81
+ let extendsGateway = input.extendsGateway || "";
82
+ if (extendsInput) {
83
+ const parsed = parseExtendsRef(extendsInput);
84
+ if (parsed) {
85
+ extendsAccount = extendsAccount || parsed.account;
86
+ extendsGateway = extendsGateway || parsed.gateway;
87
+ }
88
+ }
89
+ extendsAccount = extendsAccount || "dev.everything.near";
90
+ extendsGateway = extendsGateway || "everything.dev";
34
91
  const accountDefault = domain ? deriveAccountFromDomain(domain, extendsAccount) : "";
35
92
  const account = input.account || await prompt("Project NEAR account", accountDefault);
36
- const directoryDefault = domain ? deriveDirectoryFromDomain(domain) : extendsGateway;
37
- const directory = input.directory || await prompt("Project directory", directoryDefault);
93
+ const directory = input.directory || domain || extendsGateway;
94
+ const plugins = input.plugins || await promptPluginSelect();
38
95
  const withHost = input.withHost !== void 0 ? input.withHost : await promptYesNo("Include host?", false);
39
96
  return {
40
97
  extendsAccount,
@@ -42,6 +99,7 @@ async function promptInitOptions(input) {
42
99
  directory,
43
100
  account: account || void 0,
44
101
  domain: domain || void 0,
102
+ plugins,
45
103
  withHost
46
104
  };
47
105
  }
@@ -1 +1 @@
1
- {"version":3,"file":"prompts.mjs","names":[],"sources":["../../src/cli/prompts.ts"],"sourcesContent":["import { createInterface } from \"node:readline\";\n\nexport async function prompt(question: string, defaultValue?: string): Promise<string> {\n const rl = createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n const suffix = defaultValue ? ` [${defaultValue}]` : \"\";\n const fullQuestion = `${question}${suffix}: `;\n\n return new Promise<string>((resolve) => {\n rl.question(fullQuestion, (answer) => {\n rl.close();\n const trimmed = answer.trim();\n resolve(trimmed || defaultValue || \"\");\n });\n });\n}\n\nexport async function promptYesNo(question: string, defaultVal = false): Promise<boolean> {\n const hint = defaultVal ? \"Y/n\" : \"y/N\";\n const answer = await prompt(`${question} (${hint})`);\n if (!answer) return defaultVal;\n return answer.toLowerCase() === \"y\" || answer.toLowerCase() === \"yes\";\n}\n\nfunction deriveDirectoryFromDomain(domain: string): string {\n const firstSegment = domain.split(\".\")[0];\n return firstSegment || domain;\n}\n\nfunction deriveAccountFromDomain(domain: string, extendsAccount: string): string {\n const firstSegment = domain.split(\".\")[0];\n if (!firstSegment) return \"\";\n const suffix = extendsAccount.includes(\".\")\n ? extendsAccount.substring(extendsAccount.indexOf(\".\") + 1)\n : extendsAccount;\n return `${firstSegment}.${suffix}`;\n}\n\nexport async function promptInitOptions(input: {\n extendsAccount?: string;\n extendsGateway?: string;\n directory?: string;\n account?: string;\n domain?: string;\n withHost?: boolean;\n}): Promise<{\n extendsAccount: string;\n extendsGateway: string;\n directory: string;\n account?: string;\n domain?: string;\n withHost: boolean;\n}> {\n const extendsAccount =\n input.extendsAccount || (await prompt(\"Extends account\", \"dev.everything.near\"));\n\n const extendsGateway =\n input.extendsGateway || (await prompt(\"Extends gateway\", \"everything.dev\"));\n\n const domain = input.domain || (await prompt(\"Project domain\"));\n\n const accountDefault = domain ? deriveAccountFromDomain(domain, extendsAccount) : \"\";\n const account = input.account || (await prompt(\"Project NEAR account\", accountDefault));\n\n const directoryDefault = domain ? deriveDirectoryFromDomain(domain) : extendsGateway;\n const directory = input.directory || (await prompt(\"Project directory\", directoryDefault));\n\n const withHost =\n input.withHost !== undefined ? input.withHost : await promptYesNo(\"Include host?\", false);\n\n return {\n extendsAccount,\n extendsGateway,\n directory,\n account: account || undefined,\n domain: domain || undefined,\n withHost,\n };\n}\n"],"mappings":";;;AAEA,eAAsB,OAAO,UAAkB,cAAwC;CACrF,MAAM,KAAK,gBAAgB;EACzB,OAAO,QAAQ;EACf,QAAQ,QAAQ;EACjB,CAAC;CAGF,MAAM,eAAe,GAAG,WADT,eAAe,KAAK,aAAa,KAAK,GACX;AAE1C,QAAO,IAAI,SAAiB,YAAY;AACtC,KAAG,SAAS,eAAe,WAAW;AACpC,MAAG,OAAO;AAEV,WADgB,OAAO,MAAM,IACV,gBAAgB,GAAG;IACtC;GACF;;AAGJ,eAAsB,YAAY,UAAkB,aAAa,OAAyB;CAExF,MAAM,SAAS,MAAM,OAAO,GAAG,SAAS,IAD3B,aAAa,QAAQ,MACe,GAAG;AACpD,KAAI,CAAC,OAAQ,QAAO;AACpB,QAAO,OAAO,aAAa,KAAK,OAAO,OAAO,aAAa,KAAK;;AAGlE,SAAS,0BAA0B,QAAwB;AAEzD,QADqB,OAAO,MAAM,IAAI,CAAC,MAChB;;AAGzB,SAAS,wBAAwB,QAAgB,gBAAgC;CAC/E,MAAM,eAAe,OAAO,MAAM,IAAI,CAAC;AACvC,KAAI,CAAC,aAAc,QAAO;AAI1B,QAAO,GAAG,aAAa,GAHR,eAAe,SAAS,IAAI,GACvC,eAAe,UAAU,eAAe,QAAQ,IAAI,GAAG,EAAE,GACzD;;AAIN,eAAsB,kBAAkB,OAcrC;CACD,MAAM,iBACJ,MAAM,kBAAmB,MAAM,OAAO,mBAAmB,sBAAsB;CAEjF,MAAM,iBACJ,MAAM,kBAAmB,MAAM,OAAO,mBAAmB,iBAAiB;CAE5E,MAAM,SAAS,MAAM,UAAW,MAAM,OAAO,iBAAiB;CAE9D,MAAM,iBAAiB,SAAS,wBAAwB,QAAQ,eAAe,GAAG;CAClF,MAAM,UAAU,MAAM,WAAY,MAAM,OAAO,wBAAwB,eAAe;CAEtF,MAAM,mBAAmB,SAAS,0BAA0B,OAAO,GAAG;CACtE,MAAM,YAAY,MAAM,aAAc,MAAM,OAAO,qBAAqB,iBAAiB;CAEzF,MAAM,WACJ,MAAM,aAAa,SAAY,MAAM,WAAW,MAAM,YAAY,iBAAiB,MAAM;AAE3F,QAAO;EACL;EACA;EACA;EACA,SAAS,WAAW;EACpB,QAAQ,UAAU;EAClB;EACD"}
1
+ {"version":3,"file":"prompts.mjs","names":[],"sources":["../../src/cli/prompts.ts"],"sourcesContent":["import { createInterface } from \"node:readline\";\n\nexport async function prompt(question: string, defaultValue?: string): Promise<string> {\n const rl = createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n const suffix = defaultValue ? ` [${defaultValue}]` : \"\";\n const fullQuestion = `${question}${suffix}: `;\n\n return new Promise<string>((resolve) => {\n rl.question(fullQuestion, (answer) => {\n rl.close();\n const trimmed = answer.trim();\n resolve(trimmed || defaultValue || \"\");\n });\n });\n}\n\nexport async function promptYesNo(question: string, defaultVal = false): Promise<boolean> {\n const hint = defaultVal ? \"Y/n\" : \"y/N\";\n const answer = await prompt(`${question} (${hint})`);\n if (!answer) return defaultVal;\n return answer.toLowerCase() === \"y\" || answer.toLowerCase() === \"yes\";\n}\n\nfunction parseExtendsRef(ref: string): { account: string; gateway: string } | null {\n const match = ref.match(/^(?:bos:\\/\\/)?([^/]+)\\/(.+)$/);\n if (!match) return null;\n return { account: match[1], gateway: match[2] };\n}\n\nfunction deriveAccountFromDomain(domain: string, extendsAccount: string): string {\n const firstSegment = domain.split(\".\")[0];\n if (!firstSegment) return \"\";\n const suffix = extendsAccount.includes(\".\")\n ? extendsAccount.substring(extendsAccount.indexOf(\".\") + 1)\n : extendsAccount;\n return `${firstSegment}.${suffix}`;\n}\n\nconst AVAILABLE_PLUGINS = [\n {\n key: \"_template\",\n label: \"template\",\n description: \"Plugin scaffold and boilerplate\",\n default: true,\n },\n {\n key: \"registry\",\n label: \"registry\",\n description: \"FastKV app discovery and metadata\",\n default: false,\n },\n];\n\nasync function promptPluginSelect(): Promise<string[]> {\n const selected = new Set<string>(AVAILABLE_PLUGINS.filter((p) => p.default).map((p) => p.key));\n\n console.log();\n console.log(\" Select plugins (enter number to toggle, enter to confirm):\");\n for (let i = 0; i < AVAILABLE_PLUGINS.length; i++) {\n const p = AVAILABLE_PLUGINS[i];\n const marker = selected.has(p.key) ? \"●\" : \"○\";\n console.log(` ${marker} ${i + 1}. ${p.label} — ${p.description}`);\n }\n console.log();\n\n while (true) {\n const answer = await prompt(\n \" Plugins\",\n selected.size > 0 ? Array.from(selected).join(\",\") : \"\",\n );\n if (!answer) break;\n\n const num = Number.parseInt(answer, 10);\n if (num >= 1 && num <= AVAILABLE_PLUGINS.length) {\n const plugin = AVAILABLE_PLUGINS[num - 1];\n if (selected.has(plugin.key)) {\n selected.delete(plugin.key);\n } else {\n selected.add(plugin.key);\n }\n\n console.log(\" Current selection:\");\n for (let i = 0; i < AVAILABLE_PLUGINS.length; i++) {\n const p = AVAILABLE_PLUGINS[i];\n const marker = selected.has(p.key) ? \"●\" : \"○\";\n console.log(` ${marker} ${i + 1}. ${p.label}`);\n }\n console.log();\n continue;\n }\n\n break;\n }\n\n return Array.from(selected);\n}\n\nexport async function promptInitOptions(input: {\n extendsAccount?: string;\n extendsGateway?: string;\n extends?: string;\n directory?: string;\n account?: string;\n domain?: string;\n plugins?: string[];\n withHost?: boolean;\n}): Promise<{\n extendsAccount: string;\n extendsGateway: string;\n directory: string;\n account?: string;\n domain?: string;\n plugins: string[];\n withHost: boolean;\n}> {\n const domain = input.domain || (await prompt(\"Project domain\"));\n\n const extendsInput = input.extends || (await prompt(\"Extend from\", \"\"));\n let extendsAccount = input.extendsAccount || \"\";\n let extendsGateway = input.extendsGateway || \"\";\n\n if (extendsInput) {\n const parsed = parseExtendsRef(extendsInput);\n if (parsed) {\n extendsAccount = extendsAccount || parsed.account;\n extendsGateway = extendsGateway || parsed.gateway;\n }\n }\n\n extendsAccount = extendsAccount || \"dev.everything.near\";\n extendsGateway = extendsGateway || \"everything.dev\";\n\n const accountDefault = domain ? deriveAccountFromDomain(domain, extendsAccount) : \"\";\n const account = input.account || (await prompt(\"Project NEAR account\", accountDefault));\n\n const directory = input.directory || domain || extendsGateway;\n\n const plugins = input.plugins || (await promptPluginSelect());\n\n const withHost =\n input.withHost !== undefined ? input.withHost : await promptYesNo(\"Include host?\", false);\n\n return {\n extendsAccount,\n extendsGateway,\n directory,\n account: account || undefined,\n domain: domain || undefined,\n plugins,\n withHost,\n };\n}\n"],"mappings":";;;AAEA,eAAsB,OAAO,UAAkB,cAAwC;CACrF,MAAM,KAAK,gBAAgB;EACzB,OAAO,QAAQ;EACf,QAAQ,QAAQ;EACjB,CAAC;CAGF,MAAM,eAAe,GAAG,WADT,eAAe,KAAK,aAAa,KAAK,GACX;AAE1C,QAAO,IAAI,SAAiB,YAAY;AACtC,KAAG,SAAS,eAAe,WAAW;AACpC,MAAG,OAAO;AAEV,WADgB,OAAO,MAAM,IACV,gBAAgB,GAAG;IACtC;GACF;;AAGJ,eAAsB,YAAY,UAAkB,aAAa,OAAyB;CAExF,MAAM,SAAS,MAAM,OAAO,GAAG,SAAS,IAD3B,aAAa,QAAQ,MACe,GAAG;AACpD,KAAI,CAAC,OAAQ,QAAO;AACpB,QAAO,OAAO,aAAa,KAAK,OAAO,OAAO,aAAa,KAAK;;AAGlE,SAAS,gBAAgB,KAA0D;CACjF,MAAM,QAAQ,IAAI,MAAM,+BAA+B;AACvD,KAAI,CAAC,MAAO,QAAO;AACnB,QAAO;EAAE,SAAS,MAAM;EAAI,SAAS,MAAM;EAAI;;AAGjD,SAAS,wBAAwB,QAAgB,gBAAgC;CAC/E,MAAM,eAAe,OAAO,MAAM,IAAI,CAAC;AACvC,KAAI,CAAC,aAAc,QAAO;AAI1B,QAAO,GAAG,aAAa,GAHR,eAAe,SAAS,IAAI,GACvC,eAAe,UAAU,eAAe,QAAQ,IAAI,GAAG,EAAE,GACzD;;AAIN,MAAM,oBAAoB,CACxB;CACE,KAAK;CACL,OAAO;CACP,aAAa;CACb,SAAS;CACV,EACD;CACE,KAAK;CACL,OAAO;CACP,aAAa;CACb,SAAS;CACV,CACF;AAED,eAAe,qBAAwC;CACrD,MAAM,WAAW,IAAI,IAAY,kBAAkB,QAAQ,MAAM,EAAE,QAAQ,CAAC,KAAK,MAAM,EAAE,IAAI,CAAC;AAE9F,SAAQ,KAAK;AACb,SAAQ,IAAI,+DAA+D;AAC3E,MAAK,IAAI,IAAI,GAAG,IAAI,kBAAkB,QAAQ,KAAK;EACjD,MAAM,IAAI,kBAAkB;EAC5B,MAAM,SAAS,SAAS,IAAI,EAAE,IAAI,GAAG,MAAM;AAC3C,UAAQ,IAAI,OAAO,OAAO,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,KAAK,EAAE,cAAc;;AAEtE,SAAQ,KAAK;AAEb,QAAO,MAAM;EACX,MAAM,SAAS,MAAM,OACnB,aACA,SAAS,OAAO,IAAI,MAAM,KAAK,SAAS,CAAC,KAAK,IAAI,GAAG,GACtD;AACD,MAAI,CAAC,OAAQ;EAEb,MAAM,MAAM,OAAO,SAAS,QAAQ,GAAG;AACvC,MAAI,OAAO,KAAK,OAAO,kBAAkB,QAAQ;GAC/C,MAAM,SAAS,kBAAkB,MAAM;AACvC,OAAI,SAAS,IAAI,OAAO,IAAI,CAC1B,UAAS,OAAO,OAAO,IAAI;OAE3B,UAAS,IAAI,OAAO,IAAI;AAG1B,WAAQ,IAAI,uBAAuB;AACnC,QAAK,IAAI,IAAI,GAAG,IAAI,kBAAkB,QAAQ,KAAK;IACjD,MAAM,IAAI,kBAAkB;IAC5B,MAAM,SAAS,SAAS,IAAI,EAAE,IAAI,GAAG,MAAM;AAC3C,YAAQ,IAAI,OAAO,OAAO,GAAG,IAAI,EAAE,IAAI,EAAE,QAAQ;;AAEnD,WAAQ,KAAK;AACb;;AAGF;;AAGF,QAAO,MAAM,KAAK,SAAS;;AAG7B,eAAsB,kBAAkB,OAiBrC;CACD,MAAM,SAAS,MAAM,UAAW,MAAM,OAAO,iBAAiB;CAE9D,MAAM,eAAe,MAAM,WAAY,MAAM,OAAO,eAAe,GAAG;CACtE,IAAI,iBAAiB,MAAM,kBAAkB;CAC7C,IAAI,iBAAiB,MAAM,kBAAkB;AAE7C,KAAI,cAAc;EAChB,MAAM,SAAS,gBAAgB,aAAa;AAC5C,MAAI,QAAQ;AACV,oBAAiB,kBAAkB,OAAO;AAC1C,oBAAiB,kBAAkB,OAAO;;;AAI9C,kBAAiB,kBAAkB;AACnC,kBAAiB,kBAAkB;CAEnC,MAAM,iBAAiB,SAAS,wBAAwB,QAAQ,eAAe,GAAG;CAClF,MAAM,UAAU,MAAM,WAAY,MAAM,OAAO,wBAAwB,eAAe;CAEtF,MAAM,YAAY,MAAM,aAAa,UAAU;CAE/C,MAAM,UAAU,MAAM,WAAY,MAAM,oBAAoB;CAE5D,MAAM,WACJ,MAAM,aAAa,SAAY,MAAM,WAAW,MAAM,YAAY,iBAAiB,MAAM;AAE3F,QAAO;EACL;EACA;EACA;EACA,SAAS,WAAW;EACpB,QAAQ,UAAU;EAClB;EACA;EACD"}
package/dist/cli/sync.cjs CHANGED
@@ -50,6 +50,47 @@ function backupFiles(projectDir, filePaths) {
50
50
  }
51
51
  return backupDir;
52
52
  }
53
+ function mergePackageJson(local, template) {
54
+ const merged = { ...template };
55
+ for (const depField of [
56
+ "dependencies",
57
+ "devDependencies",
58
+ "peerDependencies",
59
+ "overrides"
60
+ ]) {
61
+ const localDeps = local[depField];
62
+ const templateDeps = template[depField];
63
+ if (!localDeps && !templateDeps) continue;
64
+ const mergedDeps = { ...templateDeps ?? {} };
65
+ if (localDeps) {
66
+ for (const [name, version] of Object.entries(localDeps)) if (!(name in mergedDeps)) mergedDeps[name] = version;
67
+ }
68
+ if (Object.keys(mergedDeps).length > 0) merged[depField] = mergedDeps;
69
+ }
70
+ if (local.scripts && typeof local.scripts === "object") merged.scripts = {
71
+ ...template.scripts ?? {},
72
+ ...local.scripts
73
+ };
74
+ return merged;
75
+ }
76
+ function toDestPath(filePath) {
77
+ return filePath.startsWith(".templates/") ? filePath.slice(11) : filePath;
78
+ }
79
+ function writeSyncedFile(sourceDir, projectDir, filePath) {
80
+ const src = (0, node_path.join)(sourceDir, filePath);
81
+ const dest = (0, node_path.join)(projectDir, filePath.startsWith(".templates/") ? filePath.slice(11) : filePath);
82
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(dest), { recursive: true });
83
+ if (filePath.endsWith("package.json")) {
84
+ const localContent = (0, node_fs.existsSync)(dest) ? (0, node_fs.readFileSync)(dest, "utf-8") : null;
85
+ const templateContent = (0, node_fs.readFileSync)(src, "utf-8");
86
+ if (localContent) {
87
+ const merged = mergePackageJson(JSON.parse(localContent), JSON.parse(templateContent));
88
+ (0, node_fs.writeFileSync)(dest, `${JSON.stringify(merged, null, 2)}\n`);
89
+ return;
90
+ }
91
+ }
92
+ (0, node_fs.writeFileSync)(dest, (0, node_fs.readFileSync)(src));
93
+ }
53
94
  async function syncTemplate(projectDir, options) {
54
95
  const localConfig = JSON.parse((0, node_fs.readFileSync)((0, node_path.join)(projectDir, "bos.config.json"), "utf-8"));
55
96
  const extendsRef = localConfig.extends;
@@ -70,7 +111,7 @@ async function syncTemplate(projectDir, options) {
70
111
  };
71
112
  const extendsAccount = extendsMatch[1];
72
113
  const extendsGateway = extendsMatch[2];
73
- const { sourceDir, cleanup } = await require_cli_init.resolveSourceDir({
114
+ const { sourceDir, parentConfig, cleanup } = await require_cli_init.resolveSourceDir({
74
115
  extendsAccount,
75
116
  extendsGateway
76
117
  });
@@ -96,28 +137,55 @@ async function syncTemplate(projectDir, options) {
96
137
  });
97
138
  for (const match of matches) allTemplateFiles.add(match);
98
139
  }
140
+ const childPlugins = localConfig.plugins && typeof localConfig.plugins === "object" ? Object.keys(localConfig.plugins) : [];
141
+ const pluginRoutes = {};
142
+ if (parentConfig.plugins) {
143
+ for (const [key, ref] of Object.entries(parentConfig.plugins)) if (ref.routes && ref.routes.length > 0) pluginRoutes[key] = ref.routes;
144
+ }
145
+ const excludedRoutePatterns = [];
146
+ for (const [pluginKey, routePatterns] of Object.entries(pluginRoutes)) if (!childPlugins.includes(pluginKey)) excludedRoutePatterns.push(...routePatterns);
147
+ const filteredFiles = /* @__PURE__ */ new Set();
148
+ for (const filePath of allTemplateFiles) {
149
+ const pluginMatch = filePath.match(/^plugins\/([^/]+)/);
150
+ if (pluginMatch && !childPlugins.includes(pluginMatch[1])) continue;
151
+ if (isExcluded(filePath, excludedRoutePatterns)) continue;
152
+ filteredFiles.add(filePath);
153
+ }
154
+ for (const [pluginKey, routePatterns] of Object.entries(pluginRoutes)) {
155
+ if (!childPlugins.includes(pluginKey)) continue;
156
+ for (const rp of routePatterns) {
157
+ const matches = await (0, glob.glob)(rp, {
158
+ cwd: sourceDir,
159
+ nodir: true,
160
+ dot: true,
161
+ absolute: false
162
+ });
163
+ for (const match of matches) if (!isExcluded(match, excludedRoutePatterns)) filteredFiles.add(match);
164
+ }
165
+ }
99
166
  const snapshot = await require_snapshot.readSnapshot(projectDir);
100
167
  const updated = [];
101
168
  const skipped = [];
102
169
  const added = [];
103
- for (const filePath of allTemplateFiles) {
104
- if (isExcluded(filePath, excludePatterns)) continue;
105
- const localHash = computeLocalHash(projectDir, filePath);
170
+ for (const filePath of filteredFiles) {
171
+ const destPath = toDestPath(filePath);
172
+ if (isExcluded(destPath, excludePatterns)) continue;
173
+ const localHash = computeLocalHash(projectDir, destPath);
106
174
  const sourceContent = (0, node_fs.readFileSync)((0, node_path.join)(sourceDir, filePath));
107
175
  const sourceHash = (0, node_crypto.createHash)("sha256").update(sourceContent).digest("hex").substring(0, 16);
108
176
  if (localHash === null) {
109
- added.push(filePath);
177
+ added.push(destPath);
110
178
  continue;
111
179
  }
112
180
  if (localHash === sourceHash) continue;
113
- const snapshotHash = snapshot?.files[filePath];
181
+ const snapshotHash = snapshot?.files[destPath];
114
182
  if (snapshotHash === void 0) {
115
- updated.push(filePath);
183
+ updated.push(destPath);
116
184
  continue;
117
185
  }
118
- if (localHash === snapshotHash) updated.push(filePath);
119
- else if (options.force) updated.push(filePath);
120
- else skipped.push(filePath);
186
+ if (localHash === snapshotHash) updated.push(destPath);
187
+ else if (options.force) updated.push(destPath);
188
+ else skipped.push(destPath);
121
189
  }
122
190
  if (options.dryRun) return {
123
191
  status: "dry-run",
@@ -126,21 +194,18 @@ async function syncTemplate(projectDir, options) {
126
194
  added
127
195
  };
128
196
  const filesToWrite = [...updated, ...added].filter((f) => !isExcluded(f, excludePatterns));
197
+ const destToSource = /* @__PURE__ */ new Map();
198
+ for (const filePath of filteredFiles) destToSource.set(toDestPath(filePath), filePath);
129
199
  if (filesToWrite.length > 0) {
130
200
  backupFiles(projectDir, filesToWrite);
131
- for (const filePath of filesToWrite) {
132
- const src = (0, node_path.join)(sourceDir, filePath);
133
- const dest = (0, node_path.join)(projectDir, filePath);
134
- (0, node_fs.mkdirSync)((0, node_path.dirname)(dest), { recursive: true });
135
- (0, node_fs.writeFileSync)(dest, (0, node_fs.readFileSync)(src));
136
- }
201
+ for (const destPath of filesToWrite) writeSyncedFile(sourceDir, projectDir, destToSource.get(destPath) ?? destPath);
137
202
  }
138
203
  const newSnapshotFiles = {};
139
- for (const filePath of allTemplateFiles) {
204
+ for (const filePath of filteredFiles) {
140
205
  const src = (0, node_path.join)(sourceDir, filePath);
141
206
  if (!(0, node_fs.lstatSync)(src).isFile()) continue;
142
207
  const content = (0, node_fs.readFileSync)(src);
143
- newSnapshotFiles[filePath] = (0, node_crypto.createHash)("sha256").update(content).digest("hex").substring(0, 16);
208
+ newSnapshotFiles[toDestPath(filePath)] = (0, node_crypto.createHash)("sha256").update(content).digest("hex").substring(0, 16);
144
209
  }
145
210
  await require_snapshot.writeSnapshot(projectDir, {
146
211
  parentRef: `bos://${extendsAccount}/${extendsGateway}`,
@@ -151,6 +216,8 @@ async function syncTemplate(projectDir, options) {
151
216
  extendsGateway,
152
217
  account: localConfig.account || extendsAccount,
153
218
  domain: localConfig.domain || extendsGateway,
219
+ plugins: childPlugins,
220
+ pluginRoutes,
154
221
  workspaceOpts: { sourceDir }
155
222
  });
156
223
  if (!options.noInstall) await require_cli_init.runBunInstall(projectDir);
@@ -1 +1 @@
1
- {"version":3,"file":"sync.cjs","names":["resolveSourceDir","readTemplatekeep","readSnapshot","writeSnapshot","personalizeConfig","runBunInstall"],"sources":["../../src/cli/sync.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport {\n copyFileSync,\n existsSync,\n lstatSync,\n mkdirSync,\n readFileSync,\n writeFileSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { glob } from \"glob\";\nimport type { SyncOptions, SyncResult } from \"../contract\";\nimport { personalizeConfig, readTemplatekeep, resolveSourceDir, runBunInstall } from \"./init\";\nimport { readSnapshot, writeSnapshot } from \"./snapshot\";\n\nfunction readExcludeFile(filePath: string): string[] {\n if (!existsSync(filePath)) return [];\n const content = readFileSync(filePath, \"utf-8\");\n return content\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter((line) => line.length > 0 && !line.startsWith(\"#\"));\n}\n\nexport async function readTemplatesyncExclude(sourceDir: string): Promise<string[]> {\n return readExcludeFile(join(sourceDir, \".templatesync-exclude\"));\n}\n\nexport function readLocalSyncExcludes(projectDir: string): string[] {\n return readExcludeFile(join(projectDir, \".bos\", \"sync-local-exclude\"));\n}\n\nfunction isExcluded(filePath: string, excludePatterns: string[]): boolean {\n for (const pattern of excludePatterns) {\n if (pattern.endsWith(\"/**\")) {\n const prefix = pattern.slice(0, -3);\n if (filePath.startsWith(`${prefix}/`) || filePath === prefix) return true;\n } else if (pattern.endsWith(\"/*\")) {\n const prefix = pattern.slice(0, -2);\n const slashIdx = filePath.indexOf(\"/\", prefix.length + 1);\n if (filePath.startsWith(`${prefix}/`) && slashIdx === -1) return true;\n } else if (filePath === pattern || filePath.startsWith(`${pattern}/`)) {\n return true;\n }\n }\n return false;\n}\n\nfunction computeLocalHash(projectDir: string, filePath: string): string | null {\n const fullPath = join(projectDir, filePath);\n if (!existsSync(fullPath)) return null;\n try {\n const content = readFileSync(fullPath);\n return createHash(\"sha256\").update(content).digest(\"hex\").substring(0, 16);\n } catch {\n return null;\n }\n}\n\nfunction backupFiles(projectDir: string, filePaths: string[]): string | null {\n const filesToBackup = filePaths.filter((f) => existsSync(join(projectDir, f)));\n if (filesToBackup.length === 0) return null;\n\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const backupDir = join(projectDir, \".bos\", \"sync-backup\", timestamp);\n\n for (const filePath of filesToBackup) {\n const src = join(projectDir, filePath);\n const dest = join(backupDir, filePath);\n mkdirSync(dirname(dest), { recursive: true });\n copyFileSync(src, dest);\n }\n\n return backupDir;\n}\n\nexport async function syncTemplate(projectDir: string, options: SyncOptions): Promise<SyncResult> {\n const localConfig = JSON.parse(\n readFileSync(join(projectDir, \"bos.config.json\"), \"utf-8\"),\n ) as Record<string, unknown>;\n\n const extendsRef = localConfig.extends as string | undefined;\n if (!extendsRef?.startsWith(\"bos://\")) {\n return {\n status: \"error\",\n updated: [],\n skipped: [],\n added: [],\n error: \"No extends field found in bos.config.json — cannot determine parent\",\n };\n }\n\n const extendsMatch = extendsRef.match(/^bos:\\/\\/([^/]+)\\/(.+)$/);\n if (!extendsMatch) {\n return {\n status: \"error\",\n updated: [],\n skipped: [],\n added: [],\n error: `Invalid extends reference: ${extendsRef}`,\n };\n }\n\n const extendsAccount = extendsMatch[1];\n const extendsGateway = extendsMatch[2];\n\n const { sourceDir, cleanup } = await resolveSourceDir({ extendsAccount, extendsGateway });\n\n try {\n const patterns = await readTemplatekeep(sourceDir);\n if (patterns.length === 0) {\n return {\n status: \"error\",\n updated: [],\n skipped: [],\n added: [],\n error: \"No .templatekeep found in template source\",\n };\n }\n\n const parentExcludes = await readTemplatesyncExclude(sourceDir);\n const localExcludes = readLocalSyncExcludes(projectDir);\n const excludePatterns = [...parentExcludes, ...localExcludes];\n\n const allTemplateFiles = new Set<string>();\n for (const pattern of patterns) {\n const matches = await glob(pattern, {\n cwd: sourceDir,\n nodir: true,\n dot: true,\n absolute: false,\n });\n for (const match of matches) {\n allTemplateFiles.add(match);\n }\n }\n\n const snapshot = await readSnapshot(projectDir);\n\n const updated: string[] = [];\n const skipped: string[] = [];\n const added: string[] = [];\n\n for (const filePath of allTemplateFiles) {\n if (isExcluded(filePath, excludePatterns)) continue;\n\n const localHash = computeLocalHash(projectDir, filePath);\n const sourceContent = readFileSync(join(sourceDir, filePath));\n const sourceHash = createHash(\"sha256\").update(sourceContent).digest(\"hex\").substring(0, 16);\n\n if (localHash === null) {\n added.push(filePath);\n continue;\n }\n\n if (localHash === sourceHash) continue;\n\n const snapshotHash = snapshot?.files[filePath];\n\n if (snapshotHash === undefined) {\n updated.push(filePath);\n continue;\n }\n\n if (localHash === snapshotHash) {\n updated.push(filePath);\n } else {\n if (options.force) {\n updated.push(filePath);\n } else {\n skipped.push(filePath);\n }\n }\n }\n\n if (options.dryRun) {\n return {\n status: \"dry-run\",\n updated,\n skipped,\n added,\n };\n }\n\n const filesToWrite = [...updated, ...added].filter((f) => !isExcluded(f, excludePatterns));\n\n if (filesToWrite.length > 0) {\n backupFiles(projectDir, filesToWrite);\n\n for (const filePath of filesToWrite) {\n const src = join(sourceDir, filePath);\n const dest = join(projectDir, filePath);\n mkdirSync(dirname(dest), { recursive: true });\n writeFileSync(dest, readFileSync(src));\n }\n }\n\n const newSnapshotFiles: Record<string, string> = {};\n for (const filePath of allTemplateFiles) {\n const src = join(sourceDir, filePath);\n const stat = lstatSync(src);\n if (!stat.isFile()) continue;\n const content = readFileSync(src);\n newSnapshotFiles[filePath] = createHash(\"sha256\")\n .update(content)\n .digest(\"hex\")\n .substring(0, 16);\n }\n\n await writeSnapshot(projectDir, {\n parentRef: `bos://${extendsAccount}/${extendsGateway}`,\n files: newSnapshotFiles,\n });\n\n const account = (localConfig.account as string) || extendsAccount;\n const domain = (localConfig.domain as string) || extendsGateway;\n\n await personalizeConfig(projectDir, {\n extendsAccount,\n extendsGateway,\n account,\n domain,\n workspaceOpts: { sourceDir },\n });\n\n if (!options.noInstall) {\n await runBunInstall(projectDir);\n }\n\n return {\n status: \"synced\",\n updated,\n skipped,\n added,\n };\n } finally {\n await cleanup();\n }\n}\n"],"mappings":";;;;;;;;;AAeA,SAAS,gBAAgB,UAA4B;AACnD,KAAI,yBAAY,SAAS,CAAE,QAAO,EAAE;AAEpC,kCAD6B,UAAU,QAAQ,CAE5C,MAAM,KAAK,CACX,KAAK,SAAS,KAAK,MAAM,CAAC,CAC1B,QAAQ,SAAS,KAAK,SAAS,KAAK,CAAC,KAAK,WAAW,IAAI,CAAC;;AAG/D,eAAsB,wBAAwB,WAAsC;AAClF,QAAO,oCAAqB,WAAW,wBAAwB,CAAC;;AAGlE,SAAgB,sBAAsB,YAA8B;AAClE,QAAO,oCAAqB,YAAY,QAAQ,qBAAqB,CAAC;;AAGxE,SAAS,WAAW,UAAkB,iBAAoC;AACxE,MAAK,MAAM,WAAW,gBACpB,KAAI,QAAQ,SAAS,MAAM,EAAE;EAC3B,MAAM,SAAS,QAAQ,MAAM,GAAG,GAAG;AACnC,MAAI,SAAS,WAAW,GAAG,OAAO,GAAG,IAAI,aAAa,OAAQ,QAAO;YAC5D,QAAQ,SAAS,KAAK,EAAE;EACjC,MAAM,SAAS,QAAQ,MAAM,GAAG,GAAG;EACnC,MAAM,WAAW,SAAS,QAAQ,KAAK,OAAO,SAAS,EAAE;AACzD,MAAI,SAAS,WAAW,GAAG,OAAO,GAAG,IAAI,aAAa,GAAI,QAAO;YACxD,aAAa,WAAW,SAAS,WAAW,GAAG,QAAQ,GAAG,CACnE,QAAO;AAGX,QAAO;;AAGT,SAAS,iBAAiB,YAAoB,UAAiC;CAC7E,MAAM,+BAAgB,YAAY,SAAS;AAC3C,KAAI,yBAAY,SAAS,CAAE,QAAO;AAClC,KAAI;EACF,MAAM,oCAAuB,SAAS;AACtC,qCAAkB,SAAS,CAAC,OAAO,QAAQ,CAAC,OAAO,MAAM,CAAC,UAAU,GAAG,GAAG;SACpE;AACN,SAAO;;;AAIX,SAAS,YAAY,YAAoB,WAAoC;CAC3E,MAAM,gBAAgB,UAAU,QAAQ,kDAAsB,YAAY,EAAE,CAAC,CAAC;AAC9E,KAAI,cAAc,WAAW,EAAG,QAAO;CAGvC,MAAM,gCAAiB,YAAY,QAAQ,gCADzB,IAAI,MAAM,EAAC,aAAa,CAAC,QAAQ,SAAS,IAAI,CACI;AAEpE,MAAK,MAAM,YAAY,eAAe;EACpC,MAAM,0BAAW,YAAY,SAAS;EACtC,MAAM,2BAAY,WAAW,SAAS;AACtC,gDAAkB,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AAC7C,4BAAa,KAAK,KAAK;;AAGzB,QAAO;;AAGT,eAAsB,aAAa,YAAoB,SAA2C;CAChG,MAAM,cAAc,KAAK,oDACL,YAAY,kBAAkB,EAAE,QAAQ,CAC3D;CAED,MAAM,aAAa,YAAY;AAC/B,KAAI,CAAC,YAAY,WAAW,SAAS,CACnC,QAAO;EACL,QAAQ;EACR,SAAS,EAAE;EACX,SAAS,EAAE;EACX,OAAO,EAAE;EACT,OAAO;EACR;CAGH,MAAM,eAAe,WAAW,MAAM,0BAA0B;AAChE,KAAI,CAAC,aACH,QAAO;EACL,QAAQ;EACR,SAAS,EAAE;EACX,SAAS,EAAE;EACX,OAAO,EAAE;EACT,OAAO,8BAA8B;EACtC;CAGH,MAAM,iBAAiB,aAAa;CACpC,MAAM,iBAAiB,aAAa;CAEpC,MAAM,EAAE,WAAW,YAAY,MAAMA,kCAAiB;EAAE;EAAgB;EAAgB,CAAC;AAEzF,KAAI;EACF,MAAM,WAAW,MAAMC,kCAAiB,UAAU;AAClD,MAAI,SAAS,WAAW,EACtB,QAAO;GACL,QAAQ;GACR,SAAS,EAAE;GACX,SAAS,EAAE;GACX,OAAO,EAAE;GACT,OAAO;GACR;EAGH,MAAM,iBAAiB,MAAM,wBAAwB,UAAU;EAC/D,MAAM,gBAAgB,sBAAsB,WAAW;EACvD,MAAM,kBAAkB,CAAC,GAAG,gBAAgB,GAAG,cAAc;EAE7D,MAAM,mCAAmB,IAAI,KAAa;AAC1C,OAAK,MAAM,WAAW,UAAU;GAC9B,MAAM,UAAU,qBAAW,SAAS;IAClC,KAAK;IACL,OAAO;IACP,KAAK;IACL,UAAU;IACX,CAAC;AACF,QAAK,MAAM,SAAS,QAClB,kBAAiB,IAAI,MAAM;;EAI/B,MAAM,WAAW,MAAMC,8BAAa,WAAW;EAE/C,MAAM,UAAoB,EAAE;EAC5B,MAAM,UAAoB,EAAE;EAC5B,MAAM,QAAkB,EAAE;AAE1B,OAAK,MAAM,YAAY,kBAAkB;AACvC,OAAI,WAAW,UAAU,gBAAgB,CAAE;GAE3C,MAAM,YAAY,iBAAiB,YAAY,SAAS;GACxD,MAAM,8DAAkC,WAAW,SAAS,CAAC;GAC7D,MAAM,yCAAwB,SAAS,CAAC,OAAO,cAAc,CAAC,OAAO,MAAM,CAAC,UAAU,GAAG,GAAG;AAE5F,OAAI,cAAc,MAAM;AACtB,UAAM,KAAK,SAAS;AACpB;;AAGF,OAAI,cAAc,WAAY;GAE9B,MAAM,eAAe,UAAU,MAAM;AAErC,OAAI,iBAAiB,QAAW;AAC9B,YAAQ,KAAK,SAAS;AACtB;;AAGF,OAAI,cAAc,aAChB,SAAQ,KAAK,SAAS;YAElB,QAAQ,MACV,SAAQ,KAAK,SAAS;OAEtB,SAAQ,KAAK,SAAS;;AAK5B,MAAI,QAAQ,OACV,QAAO;GACL,QAAQ;GACR;GACA;GACA;GACD;EAGH,MAAM,eAAe,CAAC,GAAG,SAAS,GAAG,MAAM,CAAC,QAAQ,MAAM,CAAC,WAAW,GAAG,gBAAgB,CAAC;AAE1F,MAAI,aAAa,SAAS,GAAG;AAC3B,eAAY,YAAY,aAAa;AAErC,QAAK,MAAM,YAAY,cAAc;IACnC,MAAM,0BAAW,WAAW,SAAS;IACrC,MAAM,2BAAY,YAAY,SAAS;AACvC,kDAAkB,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AAC7C,+BAAc,gCAAmB,IAAI,CAAC;;;EAI1C,MAAM,mBAA2C,EAAE;AACnD,OAAK,MAAM,YAAY,kBAAkB;GACvC,MAAM,0BAAW,WAAW,SAAS;AAErC,OAAI,wBADmB,IAAI,CACjB,QAAQ,CAAE;GACpB,MAAM,oCAAuB,IAAI;AACjC,oBAAiB,wCAAuB,SAAS,CAC9C,OAAO,QAAQ,CACf,OAAO,MAAM,CACb,UAAU,GAAG,GAAG;;AAGrB,QAAMC,+BAAc,YAAY;GAC9B,WAAW,SAAS,eAAe,GAAG;GACtC,OAAO;GACR,CAAC;AAKF,QAAMC,mCAAkB,YAAY;GAClC;GACA;GACA,SANe,YAAY,WAAsB;GAOjD,QANc,YAAY,UAAqB;GAO/C,eAAe,EAAE,WAAW;GAC7B,CAAC;AAEF,MAAI,CAAC,QAAQ,UACX,OAAMC,+BAAc,WAAW;AAGjC,SAAO;GACL,QAAQ;GACR;GACA;GACA;GACD;WACO;AACR,QAAM,SAAS"}
1
+ {"version":3,"file":"sync.cjs","names":["resolveSourceDir","readTemplatekeep","readSnapshot","writeSnapshot","personalizeConfig","runBunInstall"],"sources":["../../src/cli/sync.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport {\n copyFileSync,\n existsSync,\n lstatSync,\n mkdirSync,\n readFileSync,\n writeFileSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { glob } from \"glob\";\nimport type { SyncOptions, SyncResult } from \"../contract\";\nimport { personalizeConfig, readTemplatekeep, resolveSourceDir, runBunInstall } from \"./init\";\nimport { readSnapshot, writeSnapshot } from \"./snapshot\";\n\nfunction readExcludeFile(filePath: string): string[] {\n if (!existsSync(filePath)) return [];\n const content = readFileSync(filePath, \"utf-8\");\n return content\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter((line) => line.length > 0 && !line.startsWith(\"#\"));\n}\n\nexport async function readTemplatesyncExclude(sourceDir: string): Promise<string[]> {\n return readExcludeFile(join(sourceDir, \".templatesync-exclude\"));\n}\n\nexport function readLocalSyncExcludes(projectDir: string): string[] {\n return readExcludeFile(join(projectDir, \".bos\", \"sync-local-exclude\"));\n}\n\nfunction isExcluded(filePath: string, excludePatterns: string[]): boolean {\n for (const pattern of excludePatterns) {\n if (pattern.endsWith(\"/**\")) {\n const prefix = pattern.slice(0, -3);\n if (filePath.startsWith(`${prefix}/`) || filePath === prefix) return true;\n } else if (pattern.endsWith(\"/*\")) {\n const prefix = pattern.slice(0, -2);\n const slashIdx = filePath.indexOf(\"/\", prefix.length + 1);\n if (filePath.startsWith(`${prefix}/`) && slashIdx === -1) return true;\n } else if (filePath === pattern || filePath.startsWith(`${pattern}/`)) {\n return true;\n }\n }\n return false;\n}\n\nfunction computeLocalHash(projectDir: string, filePath: string): string | null {\n const fullPath = join(projectDir, filePath);\n if (!existsSync(fullPath)) return null;\n try {\n const content = readFileSync(fullPath);\n return createHash(\"sha256\").update(content).digest(\"hex\").substring(0, 16);\n } catch {\n return null;\n }\n}\n\nfunction backupFiles(projectDir: string, filePaths: string[]): string | null {\n const filesToBackup = filePaths.filter((f) => existsSync(join(projectDir, f)));\n if (filesToBackup.length === 0) return null;\n\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const backupDir = join(projectDir, \".bos\", \"sync-backup\", timestamp);\n\n for (const filePath of filesToBackup) {\n const src = join(projectDir, filePath);\n const dest = join(backupDir, filePath);\n mkdirSync(dirname(dest), { recursive: true });\n copyFileSync(src, dest);\n }\n\n return backupDir;\n}\n\nfunction mergePackageJson(\n local: Record<string, unknown>,\n template: Record<string, unknown>,\n): Record<string, unknown> {\n const merged = { ...template };\n\n for (const depField of [\n \"dependencies\",\n \"devDependencies\",\n \"peerDependencies\",\n \"overrides\",\n ] as const) {\n const localDeps = local[depField] as Record<string, string> | undefined;\n const templateDeps = template[depField] as Record<string, string> | undefined;\n\n if (!localDeps && !templateDeps) continue;\n\n const mergedDeps: Record<string, string> = { ...(templateDeps ?? {}) };\n\n if (localDeps) {\n for (const [name, version] of Object.entries(localDeps)) {\n if (!(name in mergedDeps)) {\n mergedDeps[name] = version;\n }\n }\n }\n\n if (Object.keys(mergedDeps).length > 0) {\n merged[depField] = mergedDeps;\n }\n }\n\n if (local.scripts && typeof local.scripts === \"object\") {\n merged.scripts = {\n ...((template.scripts as Record<string, string>) ?? {}),\n ...(local.scripts as Record<string, string>),\n };\n }\n\n return merged;\n}\n\nfunction toDestPath(filePath: string): string {\n return filePath.startsWith(\".templates/\") ? filePath.slice(\".templates/\".length) : filePath;\n}\n\nfunction writeSyncedFile(sourceDir: string, projectDir: string, filePath: string): void {\n const src = join(sourceDir, filePath);\n const destPath = filePath.startsWith(\".templates/\")\n ? filePath.slice(\".templates/\".length)\n : filePath;\n const dest = join(projectDir, destPath);\n mkdirSync(dirname(dest), { recursive: true });\n\n if (filePath.endsWith(\"package.json\")) {\n const localContent = existsSync(dest) ? readFileSync(dest, \"utf-8\") : null;\n const templateContent = readFileSync(src, \"utf-8\");\n\n if (localContent) {\n const local = JSON.parse(localContent) as Record<string, unknown>;\n const template = JSON.parse(templateContent) as Record<string, unknown>;\n const merged = mergePackageJson(local, template);\n writeFileSync(dest, `${JSON.stringify(merged, null, 2)}\\n`);\n return;\n }\n }\n\n writeFileSync(dest, readFileSync(src));\n}\n\nexport async function syncTemplate(projectDir: string, options: SyncOptions): Promise<SyncResult> {\n const localConfig = JSON.parse(\n readFileSync(join(projectDir, \"bos.config.json\"), \"utf-8\"),\n ) as Record<string, unknown>;\n\n const extendsRef = localConfig.extends as string | undefined;\n if (!extendsRef?.startsWith(\"bos://\")) {\n return {\n status: \"error\",\n updated: [],\n skipped: [],\n added: [],\n error: \"No extends field found in bos.config.json — cannot determine parent\",\n };\n }\n\n const extendsMatch = extendsRef.match(/^bos:\\/\\/([^/]+)\\/(.+)$/);\n if (!extendsMatch) {\n return {\n status: \"error\",\n updated: [],\n skipped: [],\n added: [],\n error: `Invalid extends reference: ${extendsRef}`,\n };\n }\n\n const extendsAccount = extendsMatch[1];\n const extendsGateway = extendsMatch[2];\n\n const { sourceDir, parentConfig, cleanup } = await resolveSourceDir({\n extendsAccount,\n extendsGateway,\n });\n\n try {\n const patterns = await readTemplatekeep(sourceDir);\n if (patterns.length === 0) {\n return {\n status: \"error\",\n updated: [],\n skipped: [],\n added: [],\n error: \"No .templatekeep found in template source\",\n };\n }\n\n const parentExcludes = await readTemplatesyncExclude(sourceDir);\n const localExcludes = readLocalSyncExcludes(projectDir);\n const excludePatterns = [...parentExcludes, ...localExcludes];\n\n const allTemplateFiles = new Set<string>();\n for (const pattern of patterns) {\n const matches = await glob(pattern, {\n cwd: sourceDir,\n nodir: true,\n dot: true,\n absolute: false,\n });\n for (const match of matches) {\n allTemplateFiles.add(match);\n }\n }\n\n const childPlugins =\n localConfig.plugins && typeof localConfig.plugins === \"object\"\n ? Object.keys(localConfig.plugins as Record<string, unknown>)\n : [];\n\n const pluginRoutes: Record<string, string[]> = {};\n if (parentConfig.plugins) {\n for (const [key, ref] of Object.entries(parentConfig.plugins)) {\n if (ref.routes && ref.routes.length > 0) {\n pluginRoutes[key] = ref.routes;\n }\n }\n }\n\n const excludedRoutePatterns: string[] = [];\n for (const [pluginKey, routePatterns] of Object.entries(pluginRoutes)) {\n if (!childPlugins.includes(pluginKey)) {\n excludedRoutePatterns.push(...routePatterns);\n }\n }\n\n const filteredFiles = new Set<string>();\n for (const filePath of allTemplateFiles) {\n const pluginMatch = filePath.match(/^plugins\\/([^/]+)/);\n if (pluginMatch && !childPlugins.includes(pluginMatch[1])) continue;\n if (isExcluded(filePath, excludedRoutePatterns)) continue;\n filteredFiles.add(filePath);\n }\n\n for (const [pluginKey, routePatterns] of Object.entries(pluginRoutes)) {\n if (!childPlugins.includes(pluginKey)) continue;\n for (const rp of routePatterns) {\n const matches = await glob(rp, {\n cwd: sourceDir,\n nodir: true,\n dot: true,\n absolute: false,\n });\n for (const match of matches) {\n if (!isExcluded(match, excludedRoutePatterns)) {\n filteredFiles.add(match);\n }\n }\n }\n }\n\n const snapshot = await readSnapshot(projectDir);\n\n const updated: string[] = [];\n const skipped: string[] = [];\n const added: string[] = [];\n\n for (const filePath of filteredFiles) {\n const destPath = toDestPath(filePath);\n if (isExcluded(destPath, excludePatterns)) continue;\n\n const localHash = computeLocalHash(projectDir, destPath);\n const sourceContent = readFileSync(join(sourceDir, filePath));\n const sourceHash = createHash(\"sha256\").update(sourceContent).digest(\"hex\").substring(0, 16);\n\n if (localHash === null) {\n added.push(destPath);\n continue;\n }\n\n if (localHash === sourceHash) continue;\n\n const snapshotHash = snapshot?.files[destPath];\n\n if (snapshotHash === undefined) {\n updated.push(destPath);\n continue;\n }\n\n if (localHash === snapshotHash) {\n updated.push(destPath);\n } else {\n if (options.force) {\n updated.push(destPath);\n } else {\n skipped.push(destPath);\n }\n }\n }\n\n if (options.dryRun) {\n return {\n status: \"dry-run\",\n updated,\n skipped,\n added,\n };\n }\n\n const filesToWrite = [...updated, ...added].filter((f) => !isExcluded(f, excludePatterns));\n\n const destToSource = new Map<string, string>();\n for (const filePath of filteredFiles) {\n destToSource.set(toDestPath(filePath), filePath);\n }\n\n if (filesToWrite.length > 0) {\n backupFiles(projectDir, filesToWrite);\n\n for (const destPath of filesToWrite) {\n const sourcePath = destToSource.get(destPath) ?? destPath;\n writeSyncedFile(sourceDir, projectDir, sourcePath);\n }\n }\n\n const newSnapshotFiles: Record<string, string> = {};\n for (const filePath of filteredFiles) {\n const src = join(sourceDir, filePath);\n const stat = lstatSync(src);\n if (!stat.isFile()) continue;\n const content = readFileSync(src);\n newSnapshotFiles[toDestPath(filePath)] = createHash(\"sha256\")\n .update(content)\n .digest(\"hex\")\n .substring(0, 16);\n }\n\n await writeSnapshot(projectDir, {\n parentRef: `bos://${extendsAccount}/${extendsGateway}`,\n files: newSnapshotFiles,\n });\n\n const account = (localConfig.account as string) || extendsAccount;\n const domain = (localConfig.domain as string) || extendsGateway;\n\n await personalizeConfig(projectDir, {\n extendsAccount,\n extendsGateway,\n account,\n domain,\n plugins: childPlugins,\n pluginRoutes,\n workspaceOpts: { sourceDir },\n });\n\n if (!options.noInstall) {\n await runBunInstall(projectDir);\n }\n\n return {\n status: \"synced\",\n updated,\n skipped,\n added,\n };\n } finally {\n await cleanup();\n }\n}\n"],"mappings":"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"}
package/dist/cli/sync.mjs CHANGED
@@ -49,6 +49,47 @@ function backupFiles(projectDir, filePaths) {
49
49
  }
50
50
  return backupDir;
51
51
  }
52
+ function mergePackageJson(local, template) {
53
+ const merged = { ...template };
54
+ for (const depField of [
55
+ "dependencies",
56
+ "devDependencies",
57
+ "peerDependencies",
58
+ "overrides"
59
+ ]) {
60
+ const localDeps = local[depField];
61
+ const templateDeps = template[depField];
62
+ if (!localDeps && !templateDeps) continue;
63
+ const mergedDeps = { ...templateDeps ?? {} };
64
+ if (localDeps) {
65
+ for (const [name, version] of Object.entries(localDeps)) if (!(name in mergedDeps)) mergedDeps[name] = version;
66
+ }
67
+ if (Object.keys(mergedDeps).length > 0) merged[depField] = mergedDeps;
68
+ }
69
+ if (local.scripts && typeof local.scripts === "object") merged.scripts = {
70
+ ...template.scripts ?? {},
71
+ ...local.scripts
72
+ };
73
+ return merged;
74
+ }
75
+ function toDestPath(filePath) {
76
+ return filePath.startsWith(".templates/") ? filePath.slice(11) : filePath;
77
+ }
78
+ function writeSyncedFile(sourceDir, projectDir, filePath) {
79
+ const src = join(sourceDir, filePath);
80
+ const dest = join(projectDir, filePath.startsWith(".templates/") ? filePath.slice(11) : filePath);
81
+ mkdirSync(dirname(dest), { recursive: true });
82
+ if (filePath.endsWith("package.json")) {
83
+ const localContent = existsSync(dest) ? readFileSync(dest, "utf-8") : null;
84
+ const templateContent = readFileSync(src, "utf-8");
85
+ if (localContent) {
86
+ const merged = mergePackageJson(JSON.parse(localContent), JSON.parse(templateContent));
87
+ writeFileSync(dest, `${JSON.stringify(merged, null, 2)}\n`);
88
+ return;
89
+ }
90
+ }
91
+ writeFileSync(dest, readFileSync(src));
92
+ }
52
93
  async function syncTemplate(projectDir, options) {
53
94
  const localConfig = JSON.parse(readFileSync(join(projectDir, "bos.config.json"), "utf-8"));
54
95
  const extendsRef = localConfig.extends;
@@ -69,7 +110,7 @@ async function syncTemplate(projectDir, options) {
69
110
  };
70
111
  const extendsAccount = extendsMatch[1];
71
112
  const extendsGateway = extendsMatch[2];
72
- const { sourceDir, cleanup } = await resolveSourceDir({
113
+ const { sourceDir, parentConfig, cleanup } = await resolveSourceDir({
73
114
  extendsAccount,
74
115
  extendsGateway
75
116
  });
@@ -95,28 +136,55 @@ async function syncTemplate(projectDir, options) {
95
136
  });
96
137
  for (const match of matches) allTemplateFiles.add(match);
97
138
  }
139
+ const childPlugins = localConfig.plugins && typeof localConfig.plugins === "object" ? Object.keys(localConfig.plugins) : [];
140
+ const pluginRoutes = {};
141
+ if (parentConfig.plugins) {
142
+ for (const [key, ref] of Object.entries(parentConfig.plugins)) if (ref.routes && ref.routes.length > 0) pluginRoutes[key] = ref.routes;
143
+ }
144
+ const excludedRoutePatterns = [];
145
+ for (const [pluginKey, routePatterns] of Object.entries(pluginRoutes)) if (!childPlugins.includes(pluginKey)) excludedRoutePatterns.push(...routePatterns);
146
+ const filteredFiles = /* @__PURE__ */ new Set();
147
+ for (const filePath of allTemplateFiles) {
148
+ const pluginMatch = filePath.match(/^plugins\/([^/]+)/);
149
+ if (pluginMatch && !childPlugins.includes(pluginMatch[1])) continue;
150
+ if (isExcluded(filePath, excludedRoutePatterns)) continue;
151
+ filteredFiles.add(filePath);
152
+ }
153
+ for (const [pluginKey, routePatterns] of Object.entries(pluginRoutes)) {
154
+ if (!childPlugins.includes(pluginKey)) continue;
155
+ for (const rp of routePatterns) {
156
+ const matches = await glob(rp, {
157
+ cwd: sourceDir,
158
+ nodir: true,
159
+ dot: true,
160
+ absolute: false
161
+ });
162
+ for (const match of matches) if (!isExcluded(match, excludedRoutePatterns)) filteredFiles.add(match);
163
+ }
164
+ }
98
165
  const snapshot = await readSnapshot(projectDir);
99
166
  const updated = [];
100
167
  const skipped = [];
101
168
  const added = [];
102
- for (const filePath of allTemplateFiles) {
103
- if (isExcluded(filePath, excludePatterns)) continue;
104
- const localHash = computeLocalHash(projectDir, filePath);
169
+ for (const filePath of filteredFiles) {
170
+ const destPath = toDestPath(filePath);
171
+ if (isExcluded(destPath, excludePatterns)) continue;
172
+ const localHash = computeLocalHash(projectDir, destPath);
105
173
  const sourceContent = readFileSync(join(sourceDir, filePath));
106
174
  const sourceHash = createHash("sha256").update(sourceContent).digest("hex").substring(0, 16);
107
175
  if (localHash === null) {
108
- added.push(filePath);
176
+ added.push(destPath);
109
177
  continue;
110
178
  }
111
179
  if (localHash === sourceHash) continue;
112
- const snapshotHash = snapshot?.files[filePath];
180
+ const snapshotHash = snapshot?.files[destPath];
113
181
  if (snapshotHash === void 0) {
114
- updated.push(filePath);
182
+ updated.push(destPath);
115
183
  continue;
116
184
  }
117
- if (localHash === snapshotHash) updated.push(filePath);
118
- else if (options.force) updated.push(filePath);
119
- else skipped.push(filePath);
185
+ if (localHash === snapshotHash) updated.push(destPath);
186
+ else if (options.force) updated.push(destPath);
187
+ else skipped.push(destPath);
120
188
  }
121
189
  if (options.dryRun) return {
122
190
  status: "dry-run",
@@ -125,21 +193,18 @@ async function syncTemplate(projectDir, options) {
125
193
  added
126
194
  };
127
195
  const filesToWrite = [...updated, ...added].filter((f) => !isExcluded(f, excludePatterns));
196
+ const destToSource = /* @__PURE__ */ new Map();
197
+ for (const filePath of filteredFiles) destToSource.set(toDestPath(filePath), filePath);
128
198
  if (filesToWrite.length > 0) {
129
199
  backupFiles(projectDir, filesToWrite);
130
- for (const filePath of filesToWrite) {
131
- const src = join(sourceDir, filePath);
132
- const dest = join(projectDir, filePath);
133
- mkdirSync(dirname(dest), { recursive: true });
134
- writeFileSync(dest, readFileSync(src));
135
- }
200
+ for (const destPath of filesToWrite) writeSyncedFile(sourceDir, projectDir, destToSource.get(destPath) ?? destPath);
136
201
  }
137
202
  const newSnapshotFiles = {};
138
- for (const filePath of allTemplateFiles) {
203
+ for (const filePath of filteredFiles) {
139
204
  const src = join(sourceDir, filePath);
140
205
  if (!lstatSync(src).isFile()) continue;
141
206
  const content = readFileSync(src);
142
- newSnapshotFiles[filePath] = createHash("sha256").update(content).digest("hex").substring(0, 16);
207
+ newSnapshotFiles[toDestPath(filePath)] = createHash("sha256").update(content).digest("hex").substring(0, 16);
143
208
  }
144
209
  await writeSnapshot(projectDir, {
145
210
  parentRef: `bos://${extendsAccount}/${extendsGateway}`,
@@ -150,6 +215,8 @@ async function syncTemplate(projectDir, options) {
150
215
  extendsGateway,
151
216
  account: localConfig.account || extendsAccount,
152
217
  domain: localConfig.domain || extendsGateway,
218
+ plugins: childPlugins,
219
+ pluginRoutes,
153
220
  workspaceOpts: { sourceDir }
154
221
  });
155
222
  if (!options.noInstall) await runBunInstall(projectDir);