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 +3 -2
- package/src/init/merge.ts +43 -140
- package/src/init/scaffold.ts +35 -4
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-flow-os",
|
|
3
|
-
"version": "0.0.47-dev.
|
|
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
|
|
3
|
-
*
|
|
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 (
|
|
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
|
-
|
|
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
|
-
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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:
|
|
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
|
|
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 (
|
|
228
|
-
const { merged,
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
|
241
|
-
const
|
|
242
|
-
|
|
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
|
}
|
package/src/init/scaffold.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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) */
|