create-flow-os 0.0.21 → 0.0.23
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 +2 -2
- package/src/init/index.ts +1 -2
- package/src/init/merge.ts +204 -0
- package/src/init/scaffold.ts +88 -13
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-flow-os",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.23",
|
|
4
4
|
"license": "PolyForm-Shield-1.0.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@flow-os/client": "^0.0.
|
|
7
|
+
"@flow-os/client": "^0.0.23"
|
|
8
8
|
},
|
|
9
9
|
"bin": {
|
|
10
10
|
"create-flow-os": "./src/index.ts"
|
package/src/init/index.ts
CHANGED
|
@@ -67,8 +67,7 @@ if (!toInit.length) {
|
|
|
67
67
|
process.stdout.write(V + "Fetching latest @flow-os/client..." + R);
|
|
68
68
|
const clientVersion = await fetchFlowClientVersion();
|
|
69
69
|
process.stdout.write(clientVersion ? ` ${V}${clientVersion}${R}\n` : " (fallback)\n");
|
|
70
|
-
|
|
71
|
-
for (const lib of toInit) initLib(lib, cwd, done, clientVersion);
|
|
70
|
+
await initLib(toInit, cwd, clientVersion);
|
|
72
71
|
|
|
73
72
|
const iconOk = V + "◆" + R;
|
|
74
73
|
const iconUnrec = Y + "?" + R;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge generico per qualsiasi file: additivo (aggiunge, non toglie).
|
|
3
|
+
* In conflitto: chiede all'utente.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as readline from "readline";
|
|
7
|
+
|
|
8
|
+
export type Conflict = { key: string; userVal: unknown; templateVal: unknown; path: string };
|
|
9
|
+
|
|
10
|
+
/** Deep merge di oggetti: aggiunge chiavi nuove, per esistenti chiede se diverso */
|
|
11
|
+
function deepMergeAdditive(
|
|
12
|
+
user: Record<string, unknown>,
|
|
13
|
+
template: Record<string, unknown>,
|
|
14
|
+
path: string,
|
|
15
|
+
conflicts: Conflict[]
|
|
16
|
+
): Record<string, unknown> {
|
|
17
|
+
const out = { ...user };
|
|
18
|
+
for (const [k, v] of Object.entries(template)) {
|
|
19
|
+
if (!(k in out)) {
|
|
20
|
+
out[k] = v;
|
|
21
|
+
} else {
|
|
22
|
+
const uv = out[k];
|
|
23
|
+
if (uv !== null && typeof uv === "object" && !Array.isArray(uv) && v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
24
|
+
out[k] = deepMergeAdditive(uv as Record<string, unknown>, v as Record<string, unknown>, `${path}.${k}`, conflicts);
|
|
25
|
+
} else if (JSON.stringify(uv) !== JSON.stringify(v)) {
|
|
26
|
+
conflicts.push({ key: k, userVal: uv, templateVal: v, path: `${path}.${k}` });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
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
|
+
/** Merge template+template (senza conflitti: il secondo vince) - per pre-merge pacchetti */
|
|
99
|
+
function mergeTemplatesOnly(a: Record<string, unknown>, b: Record<string, unknown>): Record<string, unknown> {
|
|
100
|
+
const out = { ...a };
|
|
101
|
+
for (const [k, v] of Object.entries(b)) {
|
|
102
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v) && k in out && typeof out[k] === "object") {
|
|
103
|
+
out[k] = mergeTemplatesOnly(out[k] as Record<string, unknown>, v as Record<string, unknown>);
|
|
104
|
+
} else {
|
|
105
|
+
out[k] = v;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Merge per file JSON */
|
|
112
|
+
export function mergeJson(userContent: string, templateContent: string, _path: string): { merged: string; conflicts: Conflict[] } {
|
|
113
|
+
const conflicts: Conflict[] = [];
|
|
114
|
+
const user = (JSON.parse(userContent || "{}") || {}) as Record<string, unknown>;
|
|
115
|
+
const template = (JSON.parse(templateContent || "{}") || {}) as Record<string, unknown>;
|
|
116
|
+
const merged = deepMergeAdditive(user, template, "", conflicts);
|
|
117
|
+
return { merged: JSON.stringify(merged, null, 2), conflicts };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Merge template+template per JSON (sync, nessun conflitto) */
|
|
121
|
+
export function mergeJsonTemplates(a: string, b: string): string {
|
|
122
|
+
const objA = (JSON.parse(a || "{}") || {}) as Record<string, unknown>;
|
|
123
|
+
const objB = (JSON.parse(b || "{}") || {}) as Record<string, unknown>;
|
|
124
|
+
return JSON.stringify(mergeTemplatesOnly(objA, objB), null, 2);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Merge template+template per code (sync, nessun conflitto) */
|
|
128
|
+
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);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Merge per file con pattern fn({...}) - config(), defineConfig(), ecc. */
|
|
140
|
+
export function mergeCodeObject(userContent: string, templateContent: string, _path: string): { merged: string; conflicts: Conflict[]; ext: Extracted | null } {
|
|
141
|
+
const conflicts: Conflict[] = [];
|
|
142
|
+
const userExt = extractObjectArg(userContent);
|
|
143
|
+
const templateExt = extractObjectArg(templateContent);
|
|
144
|
+
if (!templateExt) return { merged: userContent || templateContent, conflicts: [], ext: null };
|
|
145
|
+
const userObj = userExt?.obj ?? {};
|
|
146
|
+
const templateObj = templateExt.obj;
|
|
147
|
+
const mergedObj = deepMergeAdditive(userObj, templateObj, "", conflicts);
|
|
148
|
+
const base = userContent || templateContent;
|
|
149
|
+
const merged = replaceObjectArg(base, templateExt, mergedObj);
|
|
150
|
+
return { merged, conflicts, ext: templateExt };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Chiede all'utente se applicare i valori del template in conflitto */
|
|
154
|
+
export async function resolveConflicts(conflicts: Conflict[], path: string): Promise<Record<string, unknown>> {
|
|
155
|
+
if (conflicts.length === 0) return {};
|
|
156
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
157
|
+
const overrides: Record<string, unknown> = {};
|
|
158
|
+
for (const c of conflicts) {
|
|
159
|
+
const msg = `\n${path}: conflitto su "${c.key}" - tuo: ${JSON.stringify(c.userVal)}, template: ${JSON.stringify(c.templateVal)}. Sovrascrivere? (s/n): `;
|
|
160
|
+
const answer = await new Promise<string>((resolve) => rl.question(msg, resolve));
|
|
161
|
+
if (answer.toLowerCase().startsWith("s") || answer.toLowerCase().startsWith("y")) {
|
|
162
|
+
overrides[c.key] = c.templateVal;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
rl.close();
|
|
166
|
+
return overrides;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Merge generico: sceglie strategia in base al file */
|
|
170
|
+
export async function mergeFile(
|
|
171
|
+
userContent: string | null,
|
|
172
|
+
templateContent: string,
|
|
173
|
+
relPath: string,
|
|
174
|
+
promptConflict: (path: string, conflicts: Conflict[]) => Promise<Record<string, unknown>>
|
|
175
|
+
): Promise<string> {
|
|
176
|
+
const isJson = relPath.toLowerCase().endsWith(".json");
|
|
177
|
+
const isCode = /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(relPath);
|
|
178
|
+
|
|
179
|
+
if (isJson) {
|
|
180
|
+
const { merged, conflicts } = mergeJson(userContent ?? "{}", templateContent, relPath);
|
|
181
|
+
if (conflicts.length > 0) {
|
|
182
|
+
const overrides = await promptConflict(relPath, conflicts);
|
|
183
|
+
const obj = JSON.parse(merged) as Record<string, unknown>;
|
|
184
|
+
for (const [k, v] of Object.entries(overrides)) obj[k] = v;
|
|
185
|
+
return JSON.stringify(obj, null, 2);
|
|
186
|
+
}
|
|
187
|
+
return merged;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (isCode) {
|
|
191
|
+
const { merged, conflicts, ext } = mergeCodeObject(userContent ?? "", templateContent, relPath);
|
|
192
|
+
if (conflicts.length > 0 && ext) {
|
|
193
|
+
const overrides = await promptConflict(relPath, conflicts);
|
|
194
|
+
const extCur = extractObjectArg(merged);
|
|
195
|
+
if (extCur) {
|
|
196
|
+
const obj = { ...extCur.obj, ...overrides };
|
|
197
|
+
return replaceObjectArg(merged, extCur, obj);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return merged;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return userContent ?? templateContent;
|
|
204
|
+
}
|
package/src/init/scaffold.ts
CHANGED
|
@@ -2,14 +2,61 @@ import { existsSync, cpSync, readFileSync, writeFileSync, readdirSync } 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
6
|
|
|
6
7
|
const SKIP = new Set(["node_modules", ".git", ".vite", "package.json"]);
|
|
7
8
|
const NPM_REGISTRY = "https://registry.npmjs.org";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
/** Raccoglie file da configDir in una Map relPath -> content */
|
|
11
|
+
function collectConfigFiles(configDir: string, relPath = ""): Map<string, string> {
|
|
12
|
+
const out = new Map<string, string>();
|
|
10
13
|
for (const e of readdirSync(configDir, { withFileTypes: true })) {
|
|
11
14
|
if (SKIP.has(e.name)) continue;
|
|
12
|
-
|
|
15
|
+
const src = join(configDir, e.name);
|
|
16
|
+
const childRel = relPath ? `${relPath}/${e.name}` : e.name;
|
|
17
|
+
if (e.isDirectory()) {
|
|
18
|
+
for (const [k, v] of collectConfigFiles(src, childRel)) out.set(k, v);
|
|
19
|
+
} else {
|
|
20
|
+
out.set(childRel, readFileSync(src, "utf-8"));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Merge template A con template B (stesso relPath) */
|
|
27
|
+
function mergeTemplateInto(combined: Map<string, string>, pkgFiles: Map<string, string>): void {
|
|
28
|
+
for (const [relPath, content] of pkgFiles) {
|
|
29
|
+
const existing = combined.get(relPath);
|
|
30
|
+
const isJson = relPath.toLowerCase().endsWith(".json");
|
|
31
|
+
const isCode = /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(relPath);
|
|
32
|
+
if (existing === undefined) {
|
|
33
|
+
combined.set(relPath, content);
|
|
34
|
+
} else if (isJson) {
|
|
35
|
+
combined.set(relPath, mergeJsonTemplates(existing, content));
|
|
36
|
+
} else if (isCode) {
|
|
37
|
+
combined.set(relPath, mergeCodeTemplates(existing, content));
|
|
38
|
+
} else {
|
|
39
|
+
combined.set(relPath, content);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Scrive combined template in cwd facendo merge con file utente (un solo pass) */
|
|
45
|
+
async function writeMergedWithUser(
|
|
46
|
+
combined: Map<string, string>,
|
|
47
|
+
cwd: string,
|
|
48
|
+
promptConflict: (path: string, conflicts: import("./merge").Conflict[]) => Promise<Record<string, unknown>>
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
for (const [relPath, templateContent] of combined) {
|
|
51
|
+
const dest = join(cwd, relPath);
|
|
52
|
+
const destDir = dirname(dest);
|
|
53
|
+
if (!existsSync(destDir)) {
|
|
54
|
+
const { mkdirSync } = await import("fs");
|
|
55
|
+
mkdirSync(destDir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
const userContent = existsSync(dest) ? readFileSync(dest, "utf-8") : null;
|
|
58
|
+
const merged = await mergeFile(userContent, templateContent, relPath, promptConflict);
|
|
59
|
+
writeFileSync(dest, merged);
|
|
13
60
|
}
|
|
14
61
|
}
|
|
15
62
|
|
|
@@ -107,19 +154,47 @@ function mergePkg(configDir: string, cwd: string, clientVersionFromNpm: string |
|
|
|
107
154
|
writeFileSync(targetPath, JSON.stringify(target, null, 2));
|
|
108
155
|
}
|
|
109
156
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
157
|
+
/** Fase 1: raccoglie e merge tutti i template in memoria, ritorna ordine pacchetti */
|
|
158
|
+
async function collectAllTemplates(
|
|
159
|
+
libs: string[],
|
|
160
|
+
combined: Map<string, string>,
|
|
161
|
+
done: Set<string>,
|
|
162
|
+
order: string[],
|
|
163
|
+
_clientVersionFromNpm?: string
|
|
164
|
+
): Promise<void> {
|
|
165
|
+
for (const lib of libs) {
|
|
166
|
+
const pkgName = toPkgName(lib);
|
|
167
|
+
if (done.has(pkgName)) continue;
|
|
168
|
+
|
|
169
|
+
const root = pkgRoot(pkgName);
|
|
170
|
+
const configDir = join(root, "config");
|
|
171
|
+
if (!existsSync(configDir)) continue;
|
|
172
|
+
|
|
173
|
+
for (const sub of flowDeps(root)) {
|
|
174
|
+
await collectAllTemplates([toShortName(sub)], combined, done, order, _clientVersionFromNpm);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const pkgFiles = collectConfigFiles(configDir);
|
|
178
|
+
mergeTemplateInto(combined, pkgFiles);
|
|
179
|
+
order.push(configDir);
|
|
180
|
+
done.add(pkgName);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
113
183
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
184
|
+
/** Init: 1) merge tutti i template in memoria, 2) mergePkg per ogni lib, 3) un solo merge con file utente */
|
|
185
|
+
export async function initLib(
|
|
186
|
+
libs: string[],
|
|
187
|
+
cwd: string,
|
|
188
|
+
clientVersionFromNpm?: string
|
|
189
|
+
): Promise<void> {
|
|
190
|
+
const combined = new Map<string, string>();
|
|
191
|
+
const done = new Set<string>();
|
|
192
|
+
const order: string[] = [];
|
|
193
|
+
await collectAllTemplates(libs, combined, done, order, clientVersionFromNpm);
|
|
117
194
|
|
|
118
|
-
for (const
|
|
119
|
-
|
|
195
|
+
for (const configDir of order) {
|
|
196
|
+
mergePkg(configDir, cwd, clientVersionFromNpm);
|
|
120
197
|
}
|
|
121
198
|
|
|
122
|
-
|
|
123
|
-
mergePkg(configDir, cwd, clientVersionFromNpm);
|
|
124
|
-
done.add(pkgName);
|
|
199
|
+
await writeMergedWithUser(combined, cwd, (path, conflicts) => resolveConflicts(conflicts, path));
|
|
125
200
|
}
|