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 +3 -2
- package/src/init/merge.ts +43 -120
- 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,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 (
|
|
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
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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:
|
|
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
|
|
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 (
|
|
210
|
-
const { merged,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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
|
}
|
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) */
|