create-flow-os 0.0.47 → 0.0.51-dev.1772054587
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 -3
- package/src/init/index.ts +4 -5
- package/src/init/lib.ts +22 -2
- package/src/init/merge.ts +22 -106
- package/src/init/scaffold.ts +185 -20
- package/src/init/ui.ts +16 -12
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-flow-os",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.51-dev.1772054587",
|
|
4
4
|
"license": "PolyForm-Shield-1.0.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@flow-os/client": "
|
|
7
|
+
"@flow-os/client": ">=0.0.1-dev.0"
|
|
8
8
|
},
|
|
9
9
|
"bin": {
|
|
10
10
|
"create-flow-os": "./src/index.ts"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"create": "bun run src/create/index.ts",
|
|
14
14
|
"dev": "flow-os dev",
|
|
15
15
|
"build": "flow-os build",
|
|
16
|
-
"start": "bun
|
|
16
|
+
"start": "bun ./node_modules/@flow-os/server/src/bin.ts"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/node": "^25.3.0",
|
package/src/init/index.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
// Clear subito per nascondere resolving/installing di Bun
|
|
3
|
-
process.stdout.write("\x1b[2J\x1b[H");
|
|
4
2
|
|
|
5
3
|
import * as readline from "readline";
|
|
6
4
|
import { join, dirname } from "path";
|
|
7
5
|
import { fileURLToPath } from "url";
|
|
8
6
|
import { libsWithConfig, toShortName, toPkgName } from "./lib";
|
|
9
|
-
import { initLib, fetchFlowPackageVersions } from "./scaffold";
|
|
7
|
+
import { initLib, fetchFlowPackageVersions, shouldUseWorkspace, findFlowOsRepoRoot } from "./scaffold";
|
|
10
8
|
import { bannerBox, withLoading, colors } from "./ui";
|
|
11
9
|
|
|
12
10
|
const { V, V_LIGHT, Y, E, R, B } = colors;
|
|
@@ -16,7 +14,8 @@ const { V, V_LIGHT, Y, E, R, B } = colors;
|
|
|
16
14
|
// ───────────────────────────────────────────────────────────────────────────────
|
|
17
15
|
const cwd = process.cwd();
|
|
18
16
|
const cliRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
19
|
-
const
|
|
17
|
+
const flowOsRepoRoot = findFlowOsRepoRoot(cwd);
|
|
18
|
+
const available = libsWithConfig(cliRoot, flowOsRepoRoot).map(toShortName);
|
|
20
19
|
|
|
21
20
|
let libs = [...new Set(process.argv.slice(3))];
|
|
22
21
|
|
|
@@ -43,7 +42,7 @@ if (!toInit.length) {
|
|
|
43
42
|
// ───────────────────────────────────────────────────────────────────────────────
|
|
44
43
|
const pkgNames = toInit.map(toPkgName);
|
|
45
44
|
await withLoading(async (onStep) => {
|
|
46
|
-
const versions = await fetchFlowPackageVersions(pkgNames);
|
|
45
|
+
const versions = shouldUseWorkspace(cwd) ? new Map<string, string>() : await fetchFlowPackageVersions(pkgNames);
|
|
47
46
|
await initLib(toInit, cwd, versions, onStep);
|
|
48
47
|
});
|
|
49
48
|
|
package/src/init/lib.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "fs";
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
2
2
|
import { join, dirname } from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
|
|
@@ -21,7 +21,27 @@ export function flowDeps(root: string): string[] {
|
|
|
21
21
|
return Object.keys(deps).filter((k) => k.startsWith(FLOW_PREFIX));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
/** Scansiona packages/ per tutti gli @flow-os/* (con o senza config) */
|
|
25
|
+
function scanPackagesDir(packagesDir: string): string[] {
|
|
26
|
+
const found: string[] = [];
|
|
27
|
+
try {
|
|
28
|
+
for (const name of readdirSync(packagesDir, { withFileTypes: true })) {
|
|
29
|
+
if (!name.isDirectory()) continue;
|
|
30
|
+
const pkgPath = join(packagesDir, name.name, "package.json");
|
|
31
|
+
if (!existsSync(pkgPath)) continue;
|
|
32
|
+
try {
|
|
33
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { name?: string };
|
|
34
|
+
if (pkg?.name?.startsWith(FLOW_PREFIX)) found.push(pkg.name);
|
|
35
|
+
} catch {}
|
|
36
|
+
}
|
|
37
|
+
} catch {}
|
|
38
|
+
return found;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function libsWithConfig(cliRoot: string, flowOsRepoRoot?: string | null): string[] {
|
|
42
|
+
const packagesDir = flowOsRepoRoot ? join(flowOsRepoRoot, "packages") : join(cliRoot, "..");
|
|
43
|
+
const fromScan = scanPackagesDir(packagesDir);
|
|
44
|
+
if (fromScan.length > 0) return fromScan;
|
|
25
45
|
const pkg = JSON.parse(readFileSync(join(cliRoot, "package.json"), "utf-8"));
|
|
26
46
|
const deps = Object.keys(pkg.dependencies ?? {}).filter((k) => k.startsWith(FLOW_PREFIX));
|
|
27
47
|
return deps.filter((name) => existsSync(join(pkgRoot(name), "config")));
|
package/src/init/merge.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Merge
|
|
3
|
-
*
|
|
2
|
+
* Merge: strategia "never overwrite" (create-react-app, ecc.).
|
|
3
|
+
* File vuoto → template. File con contenuto → mantieni utente. Mai corrompere.
|
|
4
|
+
* JSON: merge additivo strutturato.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import * as readline from "readline";
|
|
7
8
|
|
|
8
9
|
export type Conflict = { key: string; userVal: unknown; templateVal: unknown; path: string };
|
|
9
10
|
|
|
11
|
+
/** Opzioni (compatibilità): non usate con never-overwrite */
|
|
12
|
+
export type Merge3WayOptions = {
|
|
13
|
+
baseContent?: string;
|
|
14
|
+
saveBase?: (content: string) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
10
17
|
/** Deep merge di oggetti: aggiunge chiavi nuove, per esistenti chiede se diverso */
|
|
11
18
|
function deepMergeAdditive(
|
|
12
19
|
user: Record<string, unknown>,
|
|
@@ -30,71 +37,6 @@ function deepMergeAdditive(
|
|
|
30
37
|
return out;
|
|
31
38
|
}
|
|
32
39
|
|
|
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
40
|
/** Merge template+template (senza conflitti: il secondo vince) - per pre-merge pacchetti */
|
|
99
41
|
function mergeTemplatesOnly(a: Record<string, unknown>, b: Record<string, unknown>): Record<string, unknown> {
|
|
100
42
|
const out = { ...a };
|
|
@@ -124,30 +66,9 @@ export function mergeJsonTemplates(a: string, b: string): string {
|
|
|
124
66
|
return JSON.stringify(mergeTemplatesOnly(objA, objB), null, 2);
|
|
125
67
|
}
|
|
126
68
|
|
|
127
|
-
/** Merge template+template per code (
|
|
69
|
+
/** Merge template+template per code: nessun merge automatico (evita corruzione). Il secondo vince. */
|
|
128
70
|
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);
|
|
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 };
|
|
71
|
+
return b || a;
|
|
151
72
|
}
|
|
152
73
|
|
|
153
74
|
/** Chiede all'utente se applicare i valori del template in conflitto */
|
|
@@ -166,15 +87,15 @@ export async function resolveConflicts(conflicts: Conflict[], path: string): Pro
|
|
|
166
87
|
return overrides;
|
|
167
88
|
}
|
|
168
89
|
|
|
169
|
-
/**
|
|
90
|
+
/** Never overwrite: file vuoto → template, file con contenuto → mantieni. Mai corrompere. */
|
|
170
91
|
export async function mergeFile(
|
|
171
92
|
userContent: string | null,
|
|
172
93
|
templateContent: string,
|
|
173
94
|
relPath: string,
|
|
174
|
-
promptConflict: (path: string, conflicts: Conflict[]) => Promise<Record<string, unknown
|
|
95
|
+
promptConflict: (path: string, conflicts: Conflict[]) => Promise<Record<string, unknown>>,
|
|
96
|
+
_options?: Merge3WayOptions
|
|
175
97
|
): Promise<string> {
|
|
176
98
|
const isJson = relPath.toLowerCase().endsWith(".json");
|
|
177
|
-
const isCode = /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(relPath);
|
|
178
99
|
|
|
179
100
|
if (isJson) {
|
|
180
101
|
const { merged, conflicts } = mergeJson(userContent ?? "{}", templateContent, relPath);
|
|
@@ -187,18 +108,13 @@ export async function mergeFile(
|
|
|
187
108
|
return merged;
|
|
188
109
|
}
|
|
189
110
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return replaceObjectArg(merged, extCur, obj);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return merged;
|
|
111
|
+
// Never overwrite: vuoto → template. Con contenuto → mantieni. Eccezione: file incompleto (manca export che template ha)
|
|
112
|
+
const content = (userContent ?? "").trim();
|
|
113
|
+
if (!content) return templateContent;
|
|
114
|
+
const tHasExport = /\bexport\s+default\b/.test(templateContent);
|
|
115
|
+
const uHasExport = /\bexport\s+default\b/.test(userContent ?? "");
|
|
116
|
+
if (tHasExport && !uHasExport) {
|
|
117
|
+
return templateContent; // File troncato
|
|
201
118
|
}
|
|
202
|
-
|
|
203
|
-
return userContent ?? templateContent;
|
|
119
|
+
return userContent!;
|
|
204
120
|
}
|
package/src/init/scaffold.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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";
|
|
@@ -48,7 +48,7 @@ function mergeTemplateInto(combined: Map<string, string>, pkgFiles: Map<string,
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/** Scrive combined template in cwd
|
|
51
|
+
/** Scrive combined template in cwd. Never overwrite: file con contenuto → mantieni. */
|
|
52
52
|
async function writeMergedWithUser(
|
|
53
53
|
combined: Map<string, string>,
|
|
54
54
|
cwd: string,
|
|
@@ -58,7 +58,6 @@ async function writeMergedWithUser(
|
|
|
58
58
|
const dest = join(cwd, relPath);
|
|
59
59
|
const destDir = dirname(dest);
|
|
60
60
|
if (!existsSync(destDir)) {
|
|
61
|
-
const { mkdirSync } = await import("fs");
|
|
62
61
|
mkdirSync(destDir, { recursive: true });
|
|
63
62
|
}
|
|
64
63
|
const userContent = existsSync(dest) ? readFileSync(dest, "utf-8") : null;
|
|
@@ -79,6 +78,29 @@ function isCreateFlowOsDev(): boolean {
|
|
|
79
78
|
}
|
|
80
79
|
}
|
|
81
80
|
|
|
81
|
+
/** Trova la root del repo flow-os salendo da cwd (contiene packages/client) */
|
|
82
|
+
export function findFlowOsRepoRoot(cwd: string): string | null {
|
|
83
|
+
let d = cwd;
|
|
84
|
+
while (true) {
|
|
85
|
+
const pkgClient = join(d, "packages", "client", "package.json");
|
|
86
|
+
if (existsSync(pkgClient)) {
|
|
87
|
+
try {
|
|
88
|
+
const pkg = JSON.parse(readFileSync(pkgClient, "utf-8")) as { name?: string };
|
|
89
|
+
if (pkg?.name === "@flow-os/client") return d;
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
const parent = dirname(d);
|
|
93
|
+
if (parent === d) break;
|
|
94
|
+
d = parent;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** True se usare workspace:* (cwd dentro repo flow-os) */
|
|
100
|
+
export function shouldUseWorkspace(cwd: string): boolean {
|
|
101
|
+
return !!findFlowOsRepoRoot(cwd);
|
|
102
|
+
}
|
|
103
|
+
|
|
82
104
|
/** Recupera versione di un pacchetto @flow-os/* dal registry npm (con cache 5 min) */
|
|
83
105
|
async function fetchFlowPackageVersion(pkgName: string): Promise<string | undefined> {
|
|
84
106
|
const tag = isCreateFlowOsDev() ? "dev" : "latest";
|
|
@@ -169,12 +191,19 @@ async function fetchConfigFromNpm(
|
|
|
169
191
|
/** Sostituisce workspace:* e 0.0.1 con versione concreta (workspace va bene solo dentro flow-os) */
|
|
170
192
|
function resolveFlowDeps(
|
|
171
193
|
deps: Record<string, string> | undefined,
|
|
172
|
-
|
|
173
|
-
versionsFromNpm: Map<string, string
|
|
194
|
+
pkgRootDir: string,
|
|
195
|
+
versionsFromNpm: Map<string, string>,
|
|
196
|
+
useWorkspace: boolean
|
|
174
197
|
): Record<string, string> {
|
|
175
198
|
if (!deps) return {};
|
|
176
199
|
const resolved = { ...deps };
|
|
177
|
-
|
|
200
|
+
if (useWorkspace) {
|
|
201
|
+
for (const k of Object.keys(resolved)) {
|
|
202
|
+
if (k.startsWith("@flow-os/")) resolved[k] = "workspace:*";
|
|
203
|
+
}
|
|
204
|
+
return resolved;
|
|
205
|
+
}
|
|
206
|
+
const ownerPkgPath = join(pkgRootDir, "package.json");
|
|
178
207
|
let ownerVersion: string | undefined;
|
|
179
208
|
if (existsSync(ownerPkgPath)) {
|
|
180
209
|
try {
|
|
@@ -205,7 +234,25 @@ function resolveFlowDeps(
|
|
|
205
234
|
return resolved;
|
|
206
235
|
}
|
|
207
236
|
|
|
208
|
-
|
|
237
|
+
const PKG_KEY_ORDER = ["name", "version", "private", "type", "scripts", "dependencies", "devDependencies"];
|
|
238
|
+
|
|
239
|
+
function sortPkgKeys(pkg: Record<string, unknown>): Record<string, unknown> {
|
|
240
|
+
const ordered: Record<string, unknown> = {};
|
|
241
|
+
for (const k of PKG_KEY_ORDER) {
|
|
242
|
+
if (k in pkg) {
|
|
243
|
+
const v = pkg[k];
|
|
244
|
+
ordered[k] = k === "scripts" && v && typeof v === "object" && !Array.isArray(v)
|
|
245
|
+
? Object.fromEntries(Object.entries(v as Record<string, string>).sort(([a], [b]) => a.localeCompare(b)))
|
|
246
|
+
: v;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const k of Object.keys(pkg).sort()) {
|
|
250
|
+
if (!(k in ordered)) ordered[k] = pkg[k];
|
|
251
|
+
}
|
|
252
|
+
return ordered;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function mergePkg(configDir: string, cwd: string, versionsFromNpm: Map<string, string>, useWorkspace: boolean): void {
|
|
209
256
|
const configPkg = join(configDir, "package.json");
|
|
210
257
|
if (!existsSync(configPkg)) return;
|
|
211
258
|
const targetPath = join(cwd, "package.json");
|
|
@@ -215,12 +262,75 @@ function mergePkg(configDir: string, cwd: string, versionsFromNpm: Map<string, s
|
|
|
215
262
|
: { ...config, name: basename(cwd) || "flow-app" };
|
|
216
263
|
target.dependencies = { ...target.dependencies, ...config.dependencies };
|
|
217
264
|
target.devDependencies = { ...target.devDependencies, ...config.devDependencies };
|
|
218
|
-
|
|
265
|
+
const pkgRootDir = join(configDir, "..");
|
|
266
|
+
for (const [k, v] of Object.entries(resolveFlowDeps(config.dependencies, pkgRootDir, versionsFromNpm, useWorkspace)))
|
|
219
267
|
target.dependencies[k] = v;
|
|
220
|
-
for (const [k, v] of Object.entries(resolveFlowDeps(config.devDependencies,
|
|
268
|
+
for (const [k, v] of Object.entries(resolveFlowDeps(config.devDependencies, pkgRootDir, versionsFromNpm, useWorkspace)))
|
|
221
269
|
target.devDependencies[k] = v;
|
|
222
270
|
target.scripts = { ...target.scripts, ...config.scripts };
|
|
223
|
-
writeFileSync(targetPath, JSON.stringify(target, null, 2));
|
|
271
|
+
writeFileSync(targetPath, JSON.stringify(sortPkgKeys(target), null, 2));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
type FlowOsInit = { dependencies?: Record<string, string>; devDependencies?: Record<string, string>; scripts?: Record<string, string>; name?: string; version?: string; type?: string };
|
|
275
|
+
|
|
276
|
+
/** Merge da flow-os-init: root package.json (se in repo) oppure package (npm) */
|
|
277
|
+
function mergeFlowOsInit(
|
|
278
|
+
packageRoot: string,
|
|
279
|
+
pkgName: string,
|
|
280
|
+
cwd: string,
|
|
281
|
+
versionsFromNpm: Map<string, string>,
|
|
282
|
+
useWorkspace: boolean,
|
|
283
|
+
flowOsRepoRoot: string | null
|
|
284
|
+
): void {
|
|
285
|
+
let init: FlowOsInit | undefined;
|
|
286
|
+
if (flowOsRepoRoot) {
|
|
287
|
+
const rootPkgPath = join(flowOsRepoRoot, "package.json");
|
|
288
|
+
if (existsSync(rootPkgPath)) {
|
|
289
|
+
try {
|
|
290
|
+
const rootPkg = JSON.parse(readFileSync(rootPkgPath, "utf-8")) as { "flow-os-init"?: Record<string, FlowOsInit> };
|
|
291
|
+
init = rootPkg["flow-os-init"]?.[pkgName];
|
|
292
|
+
} catch {}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (!init) {
|
|
296
|
+
const pkgPath = join(packageRoot, "package.json");
|
|
297
|
+
if (!existsSync(pkgPath)) return;
|
|
298
|
+
try {
|
|
299
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { "flow-os-init"?: FlowOsInit };
|
|
300
|
+
init = pkg["flow-os-init"];
|
|
301
|
+
} catch {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (!init) return;
|
|
306
|
+
const targetPath = join(cwd, "package.json");
|
|
307
|
+
const target = existsSync(targetPath)
|
|
308
|
+
? JSON.parse(readFileSync(targetPath, "utf-8"))
|
|
309
|
+
: { name: init.name ?? (basename(cwd) || "flow-app"), version: init.version ?? "0.0.1", type: init.type ?? "module" };
|
|
310
|
+
target.dependencies = { ...target.dependencies, ...init.dependencies };
|
|
311
|
+
target.devDependencies = { ...target.devDependencies, ...init.devDependencies };
|
|
312
|
+
for (const [k, v] of Object.entries(resolveFlowDeps(init.dependencies, packageRoot, versionsFromNpm, useWorkspace)))
|
|
313
|
+
target.dependencies[k] = v;
|
|
314
|
+
for (const [k, v] of Object.entries(resolveFlowDeps(init.devDependencies, packageRoot, versionsFromNpm, useWorkspace)))
|
|
315
|
+
target.devDependencies[k] = v;
|
|
316
|
+
target.scripts = { ...target.scripts, ...init.scripts };
|
|
317
|
+
writeFileSync(targetPath, JSON.stringify(sortPkgKeys(target), null, 2));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Aggiunge dipendenza per pacchetti senza config (solo install) */
|
|
321
|
+
function addDependencyForPackage(
|
|
322
|
+
pkgName: string,
|
|
323
|
+
cwd: string,
|
|
324
|
+
versionsFromNpm: Map<string, string>,
|
|
325
|
+
useWorkspace: boolean
|
|
326
|
+
): void {
|
|
327
|
+
const targetPath = join(cwd, "package.json");
|
|
328
|
+
if (!existsSync(targetPath)) return;
|
|
329
|
+
const target = JSON.parse(readFileSync(targetPath, "utf-8"));
|
|
330
|
+
target.dependencies = target.dependencies ?? {};
|
|
331
|
+
const spec = useWorkspace ? "workspace:*" : (versionsFromNpm.get(pkgName) ? `^${versionsFromNpm.get(pkgName)}` : undefined);
|
|
332
|
+
if (spec) target.dependencies[pkgName] = spec;
|
|
333
|
+
writeFileSync(targetPath, JSON.stringify(sortPkgKeys(target), null, 2));
|
|
224
334
|
}
|
|
225
335
|
|
|
226
336
|
/** Assicura che versions abbia le versioni per pkgNames (fetch lazy) */
|
|
@@ -251,12 +361,17 @@ async function collectAllTemplates(
|
|
|
251
361
|
done: Set<string>,
|
|
252
362
|
order: string[],
|
|
253
363
|
versionsFromNpm: Map<string, string>,
|
|
254
|
-
tmpDirs: string[]
|
|
364
|
+
tmpDirs: string[],
|
|
365
|
+
flowOsRepoRoot: string | null
|
|
255
366
|
): Promise<void> {
|
|
256
367
|
for (const lib of libs) {
|
|
257
368
|
const pkgName = toPkgName(lib);
|
|
258
369
|
if (done.has(pkgName)) continue;
|
|
259
370
|
|
|
371
|
+
const shortName = toShortName(pkgName);
|
|
372
|
+
const localFromRepo = flowOsRepoRoot ? join(flowOsRepoRoot, "packages", shortName) : "";
|
|
373
|
+
const localConfigFromRepo = localFromRepo ? join(localFromRepo, "config") : "";
|
|
374
|
+
|
|
260
375
|
let root: string;
|
|
261
376
|
let configDir: string;
|
|
262
377
|
try {
|
|
@@ -266,12 +381,22 @@ async function collectAllTemplates(
|
|
|
266
381
|
root = "";
|
|
267
382
|
configDir = "";
|
|
268
383
|
}
|
|
384
|
+
const hasLocalFromRepo = !!localConfigFromRepo && existsSync(localConfigFromRepo);
|
|
269
385
|
const hasLocal = existsSync(configDir);
|
|
270
386
|
|
|
271
387
|
let pkgFiles: Map<string, string>;
|
|
272
388
|
let configDirForPkg: string;
|
|
389
|
+
const useLocalRepo = hasLocalFromRepo && flowOsRepoRoot;
|
|
273
390
|
const npmVer = versionsFromNpm.get(pkgName);
|
|
274
|
-
|
|
391
|
+
|
|
392
|
+
if (useLocalRepo) {
|
|
393
|
+
configDirForPkg = localConfigFromRepo;
|
|
394
|
+
pkgFiles = collectConfigFiles(configDirForPkg);
|
|
395
|
+
const subDeps = flowDepsFromPkg(join(configDirForPkg, ".."));
|
|
396
|
+
for (const sub of subDeps) {
|
|
397
|
+
await collectAllTemplates([toShortName(sub)], combined, done, order, versionsFromNpm, tmpDirs, flowOsRepoRoot);
|
|
398
|
+
}
|
|
399
|
+
} else if (npmVer) {
|
|
275
400
|
const fetched = await fetchConfigFromNpm(pkgName, npmVer, tmpDirs);
|
|
276
401
|
if (fetched) {
|
|
277
402
|
pkgFiles = fetched.files;
|
|
@@ -279,13 +404,13 @@ async function collectAllTemplates(
|
|
|
279
404
|
const subDeps = flowDepsFromPkg(join(configDirForPkg, ".."));
|
|
280
405
|
await ensureVersions(versionsFromNpm, subDeps);
|
|
281
406
|
for (const sub of subDeps) {
|
|
282
|
-
await collectAllTemplates([toShortName(sub)], combined, done, order, versionsFromNpm, tmpDirs);
|
|
407
|
+
await collectAllTemplates([toShortName(sub)], combined, done, order, versionsFromNpm, tmpDirs, flowOsRepoRoot);
|
|
283
408
|
}
|
|
284
409
|
} else if (hasLocal) {
|
|
285
410
|
const subDeps = flowDeps(root);
|
|
286
411
|
await ensureVersions(versionsFromNpm, subDeps);
|
|
287
412
|
for (const sub of subDeps) {
|
|
288
|
-
await collectAllTemplates([toShortName(sub)], combined, done, order, versionsFromNpm, tmpDirs);
|
|
413
|
+
await collectAllTemplates([toShortName(sub)], combined, done, order, versionsFromNpm, tmpDirs, flowOsRepoRoot);
|
|
289
414
|
}
|
|
290
415
|
pkgFiles = collectConfigFiles(configDir);
|
|
291
416
|
configDirForPkg = configDir;
|
|
@@ -296,7 +421,7 @@ async function collectAllTemplates(
|
|
|
296
421
|
const subDeps = flowDeps(root);
|
|
297
422
|
await ensureVersions(versionsFromNpm, subDeps);
|
|
298
423
|
for (const sub of subDeps) {
|
|
299
|
-
await collectAllTemplates([toShortName(sub)], combined, done, order, versionsFromNpm, tmpDirs);
|
|
424
|
+
await collectAllTemplates([toShortName(sub)], combined, done, order, versionsFromNpm, tmpDirs, flowOsRepoRoot);
|
|
300
425
|
}
|
|
301
426
|
pkgFiles = collectConfigFiles(configDir);
|
|
302
427
|
configDirForPkg = configDir;
|
|
@@ -319,28 +444,68 @@ export async function initLib(
|
|
|
319
444
|
onProgress?: (step: InitProgressStep) => void
|
|
320
445
|
): Promise<void> {
|
|
321
446
|
const pkgNames = libs.map(toPkgName);
|
|
447
|
+
const flowOsRepoRoot = findFlowOsRepoRoot(cwd);
|
|
448
|
+
const useWorkspace = !!flowOsRepoRoot;
|
|
322
449
|
const versions = versionsFromNpm ?? new Map<string, string>();
|
|
323
450
|
|
|
324
451
|
onProgress?.("fetch");
|
|
325
|
-
|
|
326
|
-
|
|
452
|
+
if (!useWorkspace) {
|
|
453
|
+
const fetched = await fetchFlowPackageVersions(pkgNames);
|
|
454
|
+
for (const [k, v] of fetched) versions.set(k, v);
|
|
455
|
+
}
|
|
327
456
|
|
|
328
457
|
onProgress?.("templates");
|
|
329
458
|
const combined = new Map<string, string>();
|
|
330
459
|
const done = new Set<string>();
|
|
331
460
|
const order: string[] = [];
|
|
332
461
|
const tmpDirs: string[] = [];
|
|
333
|
-
await collectAllTemplates(libs, combined, done, order, versions, tmpDirs);
|
|
462
|
+
await collectAllTemplates(libs, combined, done, order, versions, tmpDirs, flowOsRepoRoot);
|
|
334
463
|
|
|
335
464
|
for (const configDir of order) {
|
|
336
|
-
|
|
465
|
+
const packageRoot = join(configDir, "..");
|
|
466
|
+
const pkgName = (() => {
|
|
467
|
+
try {
|
|
468
|
+
const p = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf-8")) as { name?: string };
|
|
469
|
+
return p?.name ?? "";
|
|
470
|
+
} catch {
|
|
471
|
+
return "";
|
|
472
|
+
}
|
|
473
|
+
})();
|
|
474
|
+
if (existsSync(join(configDir, "package.json"))) {
|
|
475
|
+
mergePkg(configDir, cwd, versions, useWorkspace);
|
|
476
|
+
} else {
|
|
477
|
+
mergeFlowOsInit(packageRoot, pkgName, cwd, versions, useWorkspace, flowOsRepoRoot);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const targetPath = join(cwd, "package.json");
|
|
482
|
+
if (!existsSync(targetPath)) {
|
|
483
|
+
writeFileSync(
|
|
484
|
+
targetPath,
|
|
485
|
+
JSON.stringify(sortPkgKeys({ name: basename(cwd) || "flow-app", version: "0.0.1", private: true, type: "module" }), null, 2)
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const toAdd = new Set<string>(pkgNames);
|
|
490
|
+
for (const configDir of order) {
|
|
491
|
+
const packageRoot = join(configDir, "..");
|
|
492
|
+
for (const peer of flowDepsFromPkg(packageRoot)) toAdd.add(peer);
|
|
493
|
+
}
|
|
494
|
+
for (const pkgName of pkgNames) {
|
|
495
|
+
const packageRoot = flowOsRepoRoot ? join(flowOsRepoRoot, "packages", toShortName(pkgName)) : (() => { try { return pkgRoot(pkgName); } catch { return ""; } })();
|
|
496
|
+
if (packageRoot) for (const peer of flowDepsFromPkg(packageRoot)) toAdd.add(peer);
|
|
497
|
+
}
|
|
498
|
+
await ensureVersions(versions, [...toAdd]);
|
|
499
|
+
for (const pkgName of toAdd) {
|
|
500
|
+
addDependencyForPackage(pkgName, cwd, versions, useWorkspace);
|
|
337
501
|
}
|
|
338
502
|
|
|
339
503
|
onProgress?.("write");
|
|
340
504
|
await writeMergedWithUser(combined, cwd, (path, conflicts) => resolveConflicts(conflicts, path));
|
|
341
505
|
|
|
342
506
|
onProgress?.("install");
|
|
343
|
-
const
|
|
507
|
+
const installCwd = useWorkspace && flowOsRepoRoot ? flowOsRepoRoot : cwd;
|
|
508
|
+
const proc = Bun.spawn(["bun", "install"], { cwd: installCwd, stdout: "pipe", stderr: "pipe" });
|
|
344
509
|
const exitCode = await proc.exited;
|
|
345
510
|
if (exitCode !== 0) {
|
|
346
511
|
throw new Error(`bun install exited with code ${exitCode}`);
|
package/src/init/ui.ts
CHANGED
|
@@ -9,6 +9,7 @@ const E = "\x1b[91m";
|
|
|
9
9
|
const R = "\x1b[0m";
|
|
10
10
|
const B = "\x1b[1m";
|
|
11
11
|
const DIM = "\x1b[2m";
|
|
12
|
+
const W = "\x1b[97m"; // bianco (solo per headerLogo)
|
|
12
13
|
|
|
13
14
|
const FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
|
|
14
15
|
|
|
@@ -25,19 +26,24 @@ export function bannerBox(title: string, lines: string[], color: string, w = 50)
|
|
|
25
26
|
const c = color;
|
|
26
27
|
const inner = " " + c + B + title + R + " ";
|
|
27
28
|
const innerLen = stripAnsi(inner).length;
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
29
|
+
const innerW = w - 4;
|
|
30
|
+
const half = Math.max(0, Math.floor((innerW - innerLen) / 2));
|
|
31
|
+
const rest = innerW - half - innerLen;
|
|
32
|
+
const top = c + "╭" + "─".repeat(half) + inner + c + "─".repeat(rest) + "╮" + R;
|
|
33
|
+
const bottom = c + "╰" + "─".repeat(innerW) + "╯" + R;
|
|
34
|
+
const emptyLine = c + "│" + R + " " + " ".repeat(innerW - 2) + c + "│" + R;
|
|
35
|
+
const bodyLines = lines.map((l) => c + "│" + R + " " + pad(l, innerW - 2) + c + "│" + R);
|
|
36
|
+
return "\n" + top + "\n" + emptyLine + "\n" + bodyLines.join("\n") + "\n" + bottom + "\n";
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
export function box(lines: string[], color: string, w =
|
|
39
|
+
export function box(lines: string[], color: string, w = 50): string {
|
|
36
40
|
const c = color;
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
+
const innerW = w - 4;
|
|
42
|
+
const top = c + "╭" + "─".repeat(innerW) + "╮" + R;
|
|
43
|
+
const bottom = c + "╰" + "─".repeat(innerW) + "╯" + R;
|
|
44
|
+
const emptyLine = c + "│" + R + " " + " ".repeat(innerW - 2) + c + "│" + R;
|
|
45
|
+
const body = lines.map((l) => c + "│" + R + " " + pad(l, innerW - 2) + c + "│" + R).join("\n");
|
|
46
|
+
return "\n" + top + "\n" + emptyLine + "\n" + body + "\n" + bottom + "\n";
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
import type { InitProgressStep } from "./scaffold";
|
|
@@ -87,8 +93,6 @@ export async function withLoading<T>(
|
|
|
87
93
|
}
|
|
88
94
|
}
|
|
89
95
|
|
|
90
|
-
const W = "\x1b[97m"; // bianco
|
|
91
|
-
|
|
92
96
|
export const colors = { V, V_LIGHT, Y, E, R, B, DIM, W };
|
|
93
97
|
|
|
94
98
|
/** Icona layers: 3 quadrati arrotondati sovrapposti, vista isometrica con shading ▓▒░ */
|