create-flow-os 0.0.22 → 0.0.24

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,10 @@
1
1
  {
2
2
  "name": "create-flow-os",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "license": "PolyForm-Shield-1.0.0",
5
5
  "type": "module",
6
6
  "dependencies": {
7
- "@flow-os/client": "^0.0.22"
7
+ "@flow-os/client": "^0.0.24"
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
- const done = new Set<string>();
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
+ }
@@ -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
- function copyConfig(configDir: string, cwd: string): void {
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
- cpSync(join(configDir, e.name), join(cwd, e.name), { recursive: e.isDirectory() });
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
 
@@ -42,6 +89,29 @@ export async function fetchFlowClientVersion(): Promise<string | undefined> {
42
89
  }
43
90
  }
44
91
 
92
+ /** Scarica config da npm (bypass cache) - estrae in temp, ritorna { files, configDir, tmpDir } */
93
+ async function fetchConfigFromNpm(
94
+ pkgName: string,
95
+ version: string
96
+ ): Promise<{ files: Map<string, string>; configDir: string; tmpDir: string } | null> {
97
+ const shortName = pkgName.replace(/^@flow-os\//, "");
98
+ const url = `${NPM_REGISTRY}/${pkgName}/-/${shortName}-${version}.tgz`;
99
+ try {
100
+ const res = await fetch(url, { cache: "no-store" });
101
+ if (!res.ok) return null;
102
+ const archive = new Bun.Archive(await res.blob());
103
+ const { tmpdir } = await import("os");
104
+ const tmpDir = join(tmpdir(), `flow-os-client-${version}-${Date.now()}`);
105
+ await archive.extract(tmpDir, { glob: ["package/config/**"] });
106
+ const configDir = join(tmpDir, "package", "config");
107
+ if (!existsSync(configDir)) return null;
108
+ const files = collectConfigFiles(configDir);
109
+ return { files, configDir, tmpDir };
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+
45
115
  /** Sostituisce workspace:* e 0.0.1 con versione concreta (workspace va bene solo dentro flow-os) */
46
116
  function resolveFlowDeps(
47
117
  deps: Record<string, string> | undefined,
@@ -107,19 +177,73 @@ function mergePkg(configDir: string, cwd: string, clientVersionFromNpm: string |
107
177
  writeFileSync(targetPath, JSON.stringify(target, null, 2));
108
178
  }
109
179
 
110
- export function initLib(lib: string, cwd: string, done: Set<string>, clientVersionFromNpm?: string): void {
111
- const pkgName = toPkgName(lib);
112
- if (done.has(pkgName)) return;
180
+ /** Fase 1: raccoglie e merge tutti i template in memoria, ritorna ordine pacchetti */
181
+ async function collectAllTemplates(
182
+ libs: string[],
183
+ combined: Map<string, string>,
184
+ done: Set<string>,
185
+ order: string[],
186
+ clientVersionFromNpm: string | undefined,
187
+ tmpDirs: string[]
188
+ ): Promise<void> {
189
+ for (const lib of libs) {
190
+ const pkgName = toPkgName(lib);
191
+ if (done.has(pkgName)) continue;
192
+
193
+ const root = pkgRoot(pkgName);
194
+ const configDir = join(root, "config");
195
+ if (!existsSync(configDir) && pkgName !== "@flow-os/client") continue;
113
196
 
114
- const root = pkgRoot(pkgName);
115
- const configDir = join(root, "config");
116
- if (!existsSync(configDir)) return;
197
+ for (const sub of flowDeps(root)) {
198
+ await collectAllTemplates([toShortName(sub)], combined, done, order, clientVersionFromNpm, tmpDirs);
199
+ }
117
200
 
118
- for (const sub of flowDeps(root)) {
119
- initLib(toShortName(sub), cwd, done, clientVersionFromNpm);
201
+ let pkgFiles: Map<string, string>;
202
+ let configDirForPkg: string;
203
+ if (pkgName === "@flow-os/client" && clientVersionFromNpm) {
204
+ const fetched = await fetchConfigFromNpm(pkgName, clientVersionFromNpm);
205
+ if (fetched) {
206
+ pkgFiles = fetched.files;
207
+ configDirForPkg = fetched.configDir;
208
+ tmpDirs.push(fetched.tmpDir);
209
+ } else {
210
+ if (!existsSync(configDir)) continue;
211
+ pkgFiles = collectConfigFiles(configDir);
212
+ configDirForPkg = configDir;
213
+ }
214
+ } else {
215
+ if (!existsSync(configDir)) continue;
216
+ pkgFiles = collectConfigFiles(configDir);
217
+ configDirForPkg = configDir;
218
+ }
219
+ mergeTemplateInto(combined, pkgFiles);
220
+ order.push(configDirForPkg);
221
+ done.add(pkgName);
120
222
  }
223
+ }
121
224
 
122
- copyConfig(configDir, cwd);
123
- mergePkg(configDir, cwd, clientVersionFromNpm);
124
- done.add(pkgName);
225
+ /** Init: 1) merge tutti i template in memoria, 2) mergePkg per ogni lib, 3) un solo merge con file utente */
226
+ export async function initLib(
227
+ libs: string[],
228
+ cwd: string,
229
+ clientVersionFromNpm?: string
230
+ ): Promise<void> {
231
+ const combined = new Map<string, string>();
232
+ const done = new Set<string>();
233
+ const order: string[] = [];
234
+ const tmpDirs: string[] = [];
235
+ await collectAllTemplates(libs, combined, done, order, clientVersionFromNpm, tmpDirs);
236
+
237
+ for (const configDir of order) {
238
+ mergePkg(configDir, cwd, clientVersionFromNpm);
239
+ }
240
+
241
+ await writeMergedWithUser(combined, cwd, (path, conflicts) => resolveConflicts(conflicts, path));
242
+
243
+ const { rmSync } = await import("fs");
244
+ for (const d of tmpDirs) {
245
+ try {
246
+ rmSync(d, { recursive: true, force: true });
247
+ } catch {}
248
+ }
125
249
  }