create-flow-os 0.0.47-dev.1772051359 → 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.1772051359",
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,67 +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
- /** Pattern universali: aggiungi blocco solo se manca. Vale per tutti i file di tutti i package. */
140
- type AddIfMissingPattern = {
141
- /** Controlla se il contenuto ha già questo (anche su righe diverse, formattazione diversa) */
142
- hasSignature: (content: string) => boolean;
143
- /** Estrae il blocco completo dal template da aggiungere */
144
- extractBlock: (content: string) => string | null;
145
- };
146
-
147
- const ADD_IF_MISSING_PATTERNS: AddIfMissingPattern[] = [
148
- {
149
- hasSignature: (c) => /\bexport\s+default\b/.test(c.replace(/\s+/g, " ")),
150
- extractBlock: (c) => {
151
- const m = c.match(/export\s+default\s+[\s\S]+?;\s*$/m) ?? c.match(/export\s+default\s+[\s\S]+?(?=\s*\n\s*\n|$)/s);
152
- return m?.[0]?.trim() ?? null;
153
- },
154
- },
155
- ];
156
-
157
- /** Applica i pattern add-if-missing: aggiunge blocchi dal template solo se mancano. Univoco per tutti i file. */
158
- function applyAddIfMissing(userOrMerged: string, templateContent: string): string {
159
- let result = userOrMerged;
160
- for (const p of ADD_IF_MISSING_PATTERNS) {
161
- const block = p.extractBlock(templateContent);
162
- if (block && !p.hasSignature(result)) {
163
- const trimmed = result.trimEnd();
164
- const sep = trimmed.endsWith("\n") || !trimmed ? "" : "\n\n";
165
- result = trimmed + sep + block;
166
- }
167
- }
168
- return result;
169
- }
170
-
171
- /** Merge per file con pattern fn({...}) - config(), defineConfig(), ecc. Additivo: non sovrascrive. */
172
- export function mergeCodeObject(userContent: string, templateContent: string, _path: string): { merged: string; conflicts: Conflict[]; ext: Extracted | null } {
173
- const conflicts: Conflict[] = [];
174
- const userExt = extractObjectArg(userContent);
175
- const templateExt = extractObjectArg(templateContent);
176
- if (!templateExt) {
177
- const base = userContent || templateContent;
178
- return { merged: applyAddIfMissing(base, templateContent), conflicts: [], ext: null };
179
- }
180
- const userObj = userExt?.obj ?? {};
181
- const templateObj = templateExt.obj;
182
- const mergedObj = deepMergeAdditive(userObj, templateObj, "", conflicts);
183
- const base = userContent || templateContent;
184
- const extToReplace = userExt ?? templateExt;
185
- let merged = replaceObjectArg(base, extToReplace, mergedObj);
186
- merged = applyAddIfMissing(merged, templateContent);
187
- 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 };
188
89
  }
189
90
 
190
91
  /** Chiede all'utente se applicare i valori del template in conflitto */
@@ -203,15 +104,16 @@ export async function resolveConflicts(conflicts: Conflict[], path: string): Pro
203
104
  return overrides;
204
105
  }
205
106
 
206
- /** Merge generico: sceglie strategia in base al file */
107
+ /** Merge generico: JSON additivo, code/altri merge 3-way (tipo Git) */
207
108
  export async function mergeFile(
208
109
  userContent: string | null,
209
110
  templateContent: string,
210
111
  relPath: string,
211
- promptConflict: (path: string, conflicts: Conflict[]) => Promise<Record<string, unknown>>
112
+ promptConflict: (path: string, conflicts: Conflict[]) => Promise<Record<string, unknown>>,
113
+ options?: Merge3WayOptions
212
114
  ): Promise<string> {
213
115
  const isJson = relPath.toLowerCase().endsWith(".json");
214
- 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);
215
117
 
216
118
  if (isJson) {
217
119
  const { merged, conflicts } = mergeJson(userContent ?? "{}", templateContent, relPath);
@@ -224,20 +126,21 @@ export async function mergeFile(
224
126
  return merged;
225
127
  }
226
128
 
227
- if (isCode) {
228
- const { merged, conflicts, ext } = mergeCodeObject(userContent ?? "", templateContent, relPath);
229
- if (conflicts.length > 0 && ext) {
230
- const overrides = await promptConflict(relPath, conflicts);
231
- const extCur = extractObjectArg(merged);
232
- if (extCur) {
233
- const obj = { ...extCur.obj, ...overrides };
234
- return replaceObjectArg(merged, extCur, obj);
235
- }
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 <<<<<<< / >>>>>>>`);
236
137
  }
138
+ options?.saveBase?.(templateContent);
237
139
  return merged;
238
140
  }
239
141
 
240
- // Altri file (html, css, md, ecc.): stesso sistema add-if-missing univoco
241
- const base = userContent ?? templateContent;
242
- return applyAddIfMissing(base, templateContent);
142
+ // Altri file: merge 3-way se possibile
143
+ const { merged } = merge3Way(userContent ?? "", templateContent, options?.baseContent);
144
+ options?.saveBase?.(templateContent);
145
+ return merged;
243
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) */