create-flow-os 0.0.47-dev.1772047685 → 0.0.47-dev.1772051747

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.
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "create-flow-os",
3
- "version": "0.0.47-dev.1772047685",
3
+ "version": "0.0.47-dev.1772051747",
4
4
  "license": "PolyForm-Shield-1.0.0",
5
5
  "type": "module",
6
6
  "dependencies": {
7
- "@flow-os/client": ">=0.0.1-dev.0"
7
+ "@flow-os/client": ">=0.0.1-dev.0",
8
+ "node-diff3": "^3.2.0"
8
9
  },
9
10
  "bin": {
10
11
  "create-flow-os": "./src/index.ts"
package/src/init/merge.ts CHANGED
@@ -1,12 +1,21 @@
1
1
  /**
2
- * Merge generico per qualsiasi file: additivo (aggiunge, non toglie).
3
- * In conflitto: chiede all'utente.
2
+ * Merge generico per qualsiasi file.
3
+ * JSON: merge additivo. Code/altri: merge 3-way (tipo Git) quando possibile.
4
4
  */
5
5
 
6
6
  import * as readline from "readline";
7
+ import { merge as diff3Merge } from "node-diff3";
7
8
 
8
9
  export type Conflict = { key: string; userVal: unknown; templateVal: unknown; path: string };
9
10
 
11
+ /** Opzioni per merge 3-way: base (template precedente) e callback per salvare la nuova base */
12
+ export type Merge3WayOptions = {
13
+ baseContent?: string;
14
+ saveBase?: (content: string) => void;
15
+ };
16
+
17
+ const LINE_SEP = /\n/;
18
+
10
19
  /** Deep merge di oggetti: aggiunge chiavi nuove, per esistenti chiede se diverso */
11
20
  function deepMergeAdditive(
12
21
  user: Record<string, unknown>,
@@ -30,71 +39,6 @@ function deepMergeAdditive(
30
39
  return out;
31
40
  }
32
41
 
33
- /** Trova la posizione della parentesi graffa di chiusura corrispondente */
34
- function findMatchingBrace(str: string, start: number): number {
35
- let depth = 1;
36
- for (let i = start; i < str.length; i++) {
37
- if (str[i] === "{") depth++;
38
- else if (str[i] === "}") {
39
- depth--;
40
- if (depth === 0) return i;
41
- }
42
- }
43
- return -1;
44
- }
45
-
46
- type Extracted = { obj: Record<string, unknown>; fullMatch: string; fn: string };
47
-
48
- /** Estrae oggetto da pattern fn({...}) o fn() nel contenuto - qualsiasi libreria */
49
- function extractObjectArg(content: string): Extracted | null {
50
- const re = /(\w+)\s*\(\s*(\{)?/g;
51
- let m: RegExpExecArray | null;
52
- while ((m = re.exec(content)) !== null) {
53
- const fn = m[1];
54
- const openBrace = m[2];
55
- const startIdx = m.index + m[0].length;
56
- let arg = "";
57
- let closeIdx = startIdx - 1;
58
- if (openBrace === "{") {
59
- closeIdx = findMatchingBrace(content, startIdx);
60
- if (closeIdx >= 0) arg = content.slice(startIdx, closeIdx);
61
- }
62
- const parenIdx = content.indexOf(")", closeIdx + 1);
63
- const fullMatch = parenIdx >= 0 ? content.slice(m.index, parenIdx + 1) : "";
64
- if (arg && arg.trim()) {
65
- try {
66
- const obj = new Function("return " + "{" + arg + "}")() as Record<string, unknown>;
67
- return { obj, fullMatch, fn };
68
- } catch {
69
- continue;
70
- }
71
- }
72
- return { obj: {}, fullMatch, fn };
73
- }
74
- return null;
75
- }
76
-
77
- /** Serializza oggetto in formato JS compatibile */
78
- function serializeObject(obj: Record<string, unknown>): string {
79
- const entries = Object.entries(obj)
80
- .map(([k, v]) => {
81
- const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : JSON.stringify(k);
82
- if (typeof v === "object" && v !== null && !Array.isArray(v)) {
83
- return `${key}: ${serializeObject(v as Record<string, unknown>)}`;
84
- }
85
- return `${key}: ${JSON.stringify(v)}`;
86
- })
87
- .join(", ");
88
- return `{ ${entries} }`;
89
- }
90
-
91
- /** Sostituisce l'argomento nella chiamata fn(...) con il nuovo oggetto */
92
- function replaceObjectArg(content: string, ext: Extracted, newObj: Record<string, unknown>): string {
93
- const newArg = Object.keys(newObj).length > 0 ? serializeObject(newObj) : "";
94
- const replacement = `${ext.fn}(${newArg})`;
95
- return content.replace(ext.fullMatch, replacement);
96
- }
97
-
98
42
  /** Merge template+template (senza conflitti: il secondo vince) - per pre-merge pacchetti */
99
43
  function mergeTemplatesOnly(a: Record<string, unknown>, b: Record<string, unknown>): Record<string, unknown> {
100
44
  const out = { ...a };
@@ -124,49 +68,24 @@ export function mergeJsonTemplates(a: string, b: string): string {
124
68
  return JSON.stringify(mergeTemplatesOnly(objA, objB), null, 2);
125
69
  }
126
70
 
127
- /** Merge template+template per code (sync, nessun conflitto) */
71
+ /** Merge template+template per code: nessun merge automatico (evita corruzione). Il secondo vince. */
128
72
  export function mergeCodeTemplates(a: string, b: string): string {
129
- const extA = extractObjectArg(a);
130
- const extB = extractObjectArg(b);
131
- if (!extB) return a || b;
132
- const objA = extA?.obj ?? {};
133
- const objB = extB.obj;
134
- const merged = mergeTemplatesOnly(objA, objB);
135
- const base = a || b;
136
- return replaceObjectArg(base, extB, merged);
73
+ return b || a;
137
74
  }
138
75
 
139
- /** Estrae la riga "export default fn(...)" dal template */
140
- function extractExportDefault(content: string): string | null {
141
- const m = content.match(/export\s+default\s+\w+\s*\([^)]*\)\s*;?/);
142
- return m ? m[0].trim() : null;
143
- }
144
-
145
- /** True se user ha già export default (anche su righe diverse, formattazione diversa) */
146
- function userHasExportDefault(content: string): boolean {
147
- const normalized = content.replace(/\s+/g, " ");
148
- return /\bexport\s+default\b/.test(normalized);
149
- }
150
-
151
- /** Merge per file con pattern fn({...}) - config(), defineConfig(), ecc. Additivo: non sovrascrive. */
152
- export function mergeCodeObject(userContent: string, templateContent: string, _path: string): { merged: string; conflicts: Conflict[]; ext: Extracted | null } {
153
- const conflicts: Conflict[] = [];
154
- const userExt = extractObjectArg(userContent);
155
- const templateExt = extractObjectArg(templateContent);
156
- if (!templateExt) return { merged: userContent || templateContent, conflicts: [], ext: null };
157
- const userObj = userExt?.obj ?? {};
158
- const templateObj = templateExt.obj;
159
- const mergedObj = deepMergeAdditive(userObj, templateObj, "", conflicts);
160
- const base = userContent || templateContent;
161
- const extToReplace = userExt ?? templateExt;
162
- let merged = replaceObjectArg(base, extToReplace, mergedObj);
163
- const templateExport = extractExportDefault(templateContent);
164
- if (templateExport && !userHasExportDefault(userContent) && !userHasExportDefault(merged)) {
165
- const userTrimmed = merged.trimEnd();
166
- const sep = userTrimmed.endsWith("\n") || !userTrimmed ? "" : "\n\n";
167
- merged = userTrimmed + sep + templateExport;
168
- }
169
- return { merged, conflicts, ext: templateExt };
76
+ /** Merge 3-way per file di codice (tipo Git). Base = template precedente (da cache). */
77
+ function merge3Way(
78
+ userContent: string,
79
+ templateContent: string,
80
+ baseContent: string | undefined
81
+ ): { merged: string; hasConflicts: boolean } {
82
+ if (!userContent.trim()) return { merged: templateContent, hasConflicts: false };
83
+ const base = baseContent ?? templateContent;
84
+ const { conflict, result } = diff3Merge(userContent, base, templateContent, {
85
+ stringSeparator: LINE_SEP,
86
+ excludeFalseConflicts: true,
87
+ });
88
+ return { merged: result.join("\n"), hasConflicts: conflict };
170
89
  }
171
90
 
172
91
  /** Chiede all'utente se applicare i valori del template in conflitto */
@@ -185,15 +104,16 @@ export async function resolveConflicts(conflicts: Conflict[], path: string): Pro
185
104
  return overrides;
186
105
  }
187
106
 
188
- /** Merge generico: sceglie strategia in base al file */
107
+ /** Merge generico: JSON additivo, code/altri merge 3-way (tipo Git) */
189
108
  export async function mergeFile(
190
109
  userContent: string | null,
191
110
  templateContent: string,
192
111
  relPath: string,
193
- promptConflict: (path: string, conflicts: Conflict[]) => Promise<Record<string, unknown>>
112
+ promptConflict: (path: string, conflicts: Conflict[]) => Promise<Record<string, unknown>>,
113
+ options?: Merge3WayOptions
194
114
  ): Promise<string> {
195
115
  const isJson = relPath.toLowerCase().endsWith(".json");
196
- const isCode = /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(relPath);
116
+ const use3Way = /\.(ts|tsx|js|jsx|mjs|cjs|html|css|md|vue|svelte)$/.test(relPath);
197
117
 
198
118
  if (isJson) {
199
119
  const { merged, conflicts } = mergeJson(userContent ?? "{}", templateContent, relPath);
@@ -206,18 +126,21 @@ export async function mergeFile(
206
126
  return merged;
207
127
  }
208
128
 
209
- if (isCode) {
210
- const { merged, conflicts, ext } = mergeCodeObject(userContent ?? "", templateContent, relPath);
211
- if (conflicts.length > 0 && ext) {
212
- const overrides = await promptConflict(relPath, conflicts);
213
- const extCur = extractObjectArg(merged);
214
- if (extCur) {
215
- const obj = { ...extCur.obj, ...overrides };
216
- return replaceObjectArg(merged, extCur, obj);
217
- }
129
+ if (use3Way) {
130
+ const { merged, hasConflicts } = merge3Way(
131
+ userContent ?? "",
132
+ templateContent,
133
+ options?.baseContent
134
+ );
135
+ if (hasConflicts) {
136
+ console.warn(`\n[flow-os] Conflitti in ${relPath} - risolvi i marker <<<<<<< / >>>>>>>`);
218
137
  }
138
+ options?.saveBase?.(templateContent);
219
139
  return merged;
220
140
  }
221
141
 
222
- return userContent ?? templateContent;
142
+ // Altri file: merge 3-way se possibile
143
+ const { merged } = merge3Way(userContent ?? "", templateContent, options?.baseContent);
144
+ options?.saveBase?.(templateContent);
145
+ return merged;
223
146
  }
@@ -1,8 +1,8 @@
1
- import { existsSync, cpSync, readFileSync, writeFileSync, readdirSync } from "fs";
1
+ import { existsSync, cpSync, readFileSync, writeFileSync, readdirSync, mkdirSync } from "fs";
2
2
  import { join, basename, dirname } from "path";
3
3
  import { fileURLToPath } from "url";
4
4
  import { pkgRoot, flowDeps, toPkgName, toShortName } from "./lib";
5
- import { mergeFile, resolveConflicts, mergeJsonTemplates, mergeCodeTemplates } from "./merge";
5
+ import { mergeFile, resolveConflicts, mergeJsonTemplates, mergeCodeTemplates, type Merge3WayOptions } from "./merge";
6
6
 
7
7
  const SKIP = new Set(["node_modules", ".git", ".vite", "package.json"]);
8
8
  const NPM_REGISTRY = "https://registry.npmjs.org";
@@ -48,12 +48,33 @@ function mergeTemplateInto(combined: Map<string, string>, pkgFiles: Map<string,
48
48
  }
49
49
  }
50
50
 
51
- /** Scrive combined template in cwd facendo merge con file utente (un solo pass) */
51
+ const INIT_BASE_FILE = ".flow-os/init-base.json";
52
+
53
+ function loadInitBase(cwd: string): Record<string, string> {
54
+ const p = join(cwd, INIT_BASE_FILE);
55
+ if (!existsSync(p)) return {};
56
+ try {
57
+ return JSON.parse(readFileSync(p, "utf-8")) as Record<string, string>;
58
+ } catch {
59
+ return {};
60
+ }
61
+ }
62
+
63
+ function saveInitBase(cwd: string, base: Record<string, string>): void {
64
+ const dir = join(cwd, ".flow-os");
65
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
66
+ writeFileSync(join(cwd, INIT_BASE_FILE), JSON.stringify(base, null, 2));
67
+ }
68
+
69
+ /** Scrive combined template in cwd facendo merge con file utente (merge 3-way tipo Git) */
52
70
  async function writeMergedWithUser(
53
71
  combined: Map<string, string>,
54
72
  cwd: string,
55
73
  promptConflict: (path: string, conflicts: import("./merge").Conflict[]) => Promise<Record<string, unknown>>
56
74
  ): Promise<void> {
75
+ const baseCache = loadInitBase(cwd);
76
+ const newBase: Record<string, string> = {};
77
+
57
78
  for (const [relPath, templateContent] of combined) {
58
79
  const dest = join(cwd, relPath);
59
80
  const destDir = dirname(dest);
@@ -62,9 +83,19 @@ async function writeMergedWithUser(
62
83
  mkdirSync(destDir, { recursive: true });
63
84
  }
64
85
  const userContent = existsSync(dest) ? readFileSync(dest, "utf-8") : null;
65
- const merged = await mergeFile(userContent, templateContent, relPath, promptConflict);
86
+ const mergeOpts: Merge3WayOptions = {
87
+ baseContent: baseCache[relPath],
88
+ saveBase: (content) => {
89
+ newBase[relPath] = content;
90
+ },
91
+ };
92
+ const merged = await mergeFile(userContent, templateContent, relPath, promptConflict, mergeOpts);
66
93
  writeFileSync(dest, merged);
67
94
  }
95
+
96
+ if (Object.keys(newBase).length > 0) {
97
+ saveInitBase(cwd, { ...baseCache, ...newBase });
98
+ }
68
99
  }
69
100
 
70
101
  /** Indica se create-flow-os è in modalità dev (flow-os@dev) */