create-flow-os 0.0.26 → 0.0.28
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 +8 -33
- package/src/init/scaffold.ts +61 -17
- package/src/init/ui.ts +76 -0
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-flow-os",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.28",
|
|
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.28"
|
|
8
8
|
},
|
|
9
9
|
"bin": {
|
|
10
10
|
"create-flow-os": "./src/index.ts"
|
package/src/init/index.ts
CHANGED
|
@@ -5,34 +5,9 @@ import { join, dirname } from "path";
|
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { libsWithConfig, toShortName, toPkgName } from "./lib";
|
|
7
7
|
import { initLib, fetchFlowPackageVersions } from "./scaffold";
|
|
8
|
+
import { box, withLoading, colors } from "./ui";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
// Usage: bun create flow-os i [lib...] | bun create flow-os i (prompt interattivo)
|
|
11
|
-
// ───────────────────────────────────────────────────────────────────────────────
|
|
12
|
-
|
|
13
|
-
// ───────────────────────────────────────────────────────────────────────────────
|
|
14
|
-
// ANSI: violet (ok), yellow (unrecognized), red (error/failure)
|
|
15
|
-
// ───────────────────────────────────────────────────────────────────────────────
|
|
16
|
-
const V = "\x1b[95m";
|
|
17
|
-
const Y = "\x1b[38;5;226m";
|
|
18
|
-
const E = "\x1b[91m";
|
|
19
|
-
const R = "\x1b[0m";
|
|
20
|
-
const B = "\x1b[1m";
|
|
21
|
-
|
|
22
|
-
// Icone: ◆ ok, ? unrecognized, ✕ failure
|
|
23
|
-
|
|
24
|
-
// ───────────────────────────────────────────────────────────────────────────────
|
|
25
|
-
// Box: contorno colorato, padding, strip ANSI per calcolo larghezza
|
|
26
|
-
// ───────────────────────────────────────────────────────────────────────────────
|
|
27
|
-
const strip = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, "").replace(/\n/g, "");
|
|
28
|
-
const pad = (s: string, w: number) => s + " ".repeat(Math.max(0, w - strip(s).length));
|
|
29
|
-
const box = (lines: string[], color: string, w = 52) => {
|
|
30
|
-
const c = color;
|
|
31
|
-
const top = c + "╭" + "─".repeat(w) + "╮" + R;
|
|
32
|
-
const bottom = c + "╰" + "─".repeat(w) + "╯" + R;
|
|
33
|
-
const body = lines.map((l) => c + "│" + R + " ".repeat(2) + pad(l, w - 2) + c + "│" + R).join("\n");
|
|
34
|
-
return "\n" + top + "\n" + body + "\n" + bottom + "\n";
|
|
35
|
-
};
|
|
10
|
+
const { V, Y, E, R, B } = colors;
|
|
36
11
|
|
|
37
12
|
// ───────────────────────────────────────────────────────────────────────────────
|
|
38
13
|
// Args: libs da argv o prompt interattivo
|
|
@@ -62,19 +37,19 @@ if (!toInit.length) {
|
|
|
62
37
|
}
|
|
63
38
|
|
|
64
39
|
// ───────────────────────────────────────────────────────────────────────────────
|
|
65
|
-
// Execute:
|
|
40
|
+
// Execute: init con loading UI elegante
|
|
66
41
|
// ───────────────────────────────────────────────────────────────────────────────
|
|
67
42
|
const pkgNames = toInit.map(toPkgName);
|
|
68
|
-
|
|
69
|
-
const versions = await fetchFlowPackageVersions(pkgNames);
|
|
70
|
-
|
|
71
|
-
|
|
43
|
+
await withLoading(async (onStep) => {
|
|
44
|
+
const versions = await fetchFlowPackageVersions(pkgNames);
|
|
45
|
+
await initLib(toInit, cwd, versions, onStep);
|
|
46
|
+
});
|
|
72
47
|
|
|
73
48
|
const iconOk = V + "◆" + R;
|
|
74
49
|
const iconUnrec = Y + "?" + R;
|
|
75
50
|
const iconFail = E + "✕" + R;
|
|
76
51
|
const out: string[] = [];
|
|
77
|
-
out.push(box([B + "Initialized:" + R, ...toInit.map((l) => iconOk + " " + V + l + R)], V));
|
|
52
|
+
out.push(box([B + "Initialized:" + R, ...toInit.map((l) => iconOk + " " + V + l + R), "", Y + "Per versione fresca: bun create flow-os@latest" + R], V));
|
|
78
53
|
if (skipped.length) out.push(box([B + "Unrecognized:" + R, ...skipped.map((l) => iconUnrec + " " + Y + l + R)], Y));
|
|
79
54
|
console.log(out.join(""));
|
|
80
55
|
|
package/src/init/scaffold.ts
CHANGED
|
@@ -7,6 +7,13 @@ import { mergeFile, resolveConflicts, mergeJsonTemplates, mergeCodeTemplates } f
|
|
|
7
7
|
const SKIP = new Set(["node_modules", ".git", ".vite", "package.json"]);
|
|
8
8
|
const NPM_REGISTRY = "https://registry.npmjs.org";
|
|
9
9
|
|
|
10
|
+
/** Cache versioni registry: 5 min TTL */
|
|
11
|
+
const versionCache = new Map<string, { v: string; ts: number }>();
|
|
12
|
+
const VERSION_TTL_MS = 5 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
/** Cache tarball estratti: riusa se esiste e < 1h */
|
|
15
|
+
const TARBALL_CACHE_MAX_AGE_MS = 60 * 60 * 1000;
|
|
16
|
+
|
|
10
17
|
/** Raccoglie file da configDir in una Map relPath -> content */
|
|
11
18
|
function collectConfigFiles(configDir: string, relPath = ""): Map<string, string> {
|
|
12
19
|
const out = new Map<string, string>();
|
|
@@ -72,17 +79,22 @@ function isCreateFlowOsDev(): boolean {
|
|
|
72
79
|
}
|
|
73
80
|
}
|
|
74
81
|
|
|
75
|
-
/** Recupera versione di un pacchetto @flow-os/* dal registry npm (
|
|
82
|
+
/** Recupera versione di un pacchetto @flow-os/* dal registry npm (con cache 5 min) */
|
|
76
83
|
async function fetchFlowPackageVersion(pkgName: string): Promise<string | undefined> {
|
|
77
84
|
const tag = isCreateFlowOsDev() ? "dev" : "latest";
|
|
85
|
+
const key = `${pkgName}@${tag}`;
|
|
86
|
+
const cached = versionCache.get(key);
|
|
87
|
+
if (cached && Date.now() - cached.ts < VERSION_TTL_MS) return cached.v;
|
|
88
|
+
|
|
78
89
|
try {
|
|
79
|
-
const res = await fetch(`${NPM_REGISTRY}/${pkgName}
|
|
80
|
-
|
|
81
|
-
headers: { "Cache-Control": "no-cache", Pragma: "no-cache" },
|
|
90
|
+
const res = await fetch(`${NPM_REGISTRY}/${pkgName}`, {
|
|
91
|
+
headers: { "Cache-Control": "max-age=300", Accept: "application/json" },
|
|
82
92
|
});
|
|
83
93
|
if (!res.ok) return undefined;
|
|
84
94
|
const data = (await res.json()) as { "dist-tags"?: Record<string, string> };
|
|
85
|
-
|
|
95
|
+
const v = data["dist-tags"]?.[tag] ?? data["dist-tags"]?.["latest"];
|
|
96
|
+
if (v) versionCache.set(key, { v, ts: Date.now() });
|
|
97
|
+
return v;
|
|
86
98
|
} catch {
|
|
87
99
|
return undefined;
|
|
88
100
|
}
|
|
@@ -105,24 +117,50 @@ export async function fetchFlowClientVersion(): Promise<string | undefined> {
|
|
|
105
117
|
return fetchFlowPackageVersion("@flow-os/client");
|
|
106
118
|
}
|
|
107
119
|
|
|
108
|
-
/** Scarica config da npm
|
|
120
|
+
/** Scarica config da npm; riusa cache se estratta di recente (< 1h) */
|
|
109
121
|
async function fetchConfigFromNpm(
|
|
110
122
|
pkgName: string,
|
|
111
|
-
version: string
|
|
123
|
+
version: string,
|
|
124
|
+
tmpDirs: string[]
|
|
112
125
|
): Promise<{ files: Map<string, string>; configDir: string; tmpDir: string } | null> {
|
|
113
126
|
const shortName = pkgName.replace(/^@flow-os\//, "");
|
|
127
|
+
const { tmpdir } = await import("os");
|
|
128
|
+
const cacheDir = join(tmpdir(), "flow-os-cache", `${shortName}-${version}`);
|
|
129
|
+
const configDir = join(cacheDir, "package", "config");
|
|
130
|
+
|
|
131
|
+
if (existsSync(configDir)) {
|
|
132
|
+
try {
|
|
133
|
+
const { statSync } = await import("fs");
|
|
134
|
+
const st = statSync(configDir);
|
|
135
|
+
if (Date.now() - st.mtimeMs < TARBALL_CACHE_MAX_AGE_MS) {
|
|
136
|
+
const files = collectConfigFiles(configDir);
|
|
137
|
+
return { files, configDir, tmpDir: cacheDir };
|
|
138
|
+
}
|
|
139
|
+
} catch {}
|
|
140
|
+
}
|
|
141
|
+
|
|
114
142
|
const url = `${NPM_REGISTRY}/${pkgName}/-/${shortName}-${version}.tgz`;
|
|
115
143
|
try {
|
|
116
|
-
const res = await fetch(url, {
|
|
144
|
+
const res = await fetch(url, { headers: { "Cache-Control": "max-age=3600" } });
|
|
117
145
|
if (!res.ok) return null;
|
|
118
146
|
const archive = new Bun.Archive(await res.blob());
|
|
119
|
-
const { tmpdir } = await import("os");
|
|
120
147
|
const tmpDir = join(tmpdir(), `flow-os-${shortName}-${version}-${Date.now()}`);
|
|
121
148
|
await archive.extract(tmpDir, { glob: ["package/config/**"] });
|
|
122
|
-
const
|
|
123
|
-
if (!existsSync(
|
|
124
|
-
const files = collectConfigFiles(
|
|
125
|
-
|
|
149
|
+
const extractedConfigDir = join(tmpDir, "package", "config");
|
|
150
|
+
if (!existsSync(extractedConfigDir)) return null;
|
|
151
|
+
const files = collectConfigFiles(extractedConfigDir);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const { mkdirSync } = await import("fs");
|
|
155
|
+
mkdirSync(join(cacheDir, "package"), { recursive: true });
|
|
156
|
+
const { cpSync } = await import("fs");
|
|
157
|
+
cpSync(join(tmpDir, "package", "config"), join(cacheDir, "package", "config"), {
|
|
158
|
+
recursive: true,
|
|
159
|
+
});
|
|
160
|
+
} catch {}
|
|
161
|
+
|
|
162
|
+
tmpDirs.push(tmpDir);
|
|
163
|
+
return { files, configDir: extractedConfigDir, tmpDir };
|
|
126
164
|
} catch {
|
|
127
165
|
return null;
|
|
128
166
|
}
|
|
@@ -234,11 +272,10 @@ async function collectAllTemplates(
|
|
|
234
272
|
let configDirForPkg: string;
|
|
235
273
|
const npmVer = versionsFromNpm.get(pkgName);
|
|
236
274
|
if (npmVer) {
|
|
237
|
-
const fetched = await fetchConfigFromNpm(pkgName, npmVer);
|
|
275
|
+
const fetched = await fetchConfigFromNpm(pkgName, npmVer, tmpDirs);
|
|
238
276
|
if (fetched) {
|
|
239
277
|
pkgFiles = fetched.files;
|
|
240
278
|
configDirForPkg = fetched.configDir;
|
|
241
|
-
tmpDirs.push(fetched.tmpDir);
|
|
242
279
|
const subDeps = flowDepsFromPkg(join(configDirForPkg, ".."));
|
|
243
280
|
await ensureVersions(versionsFromNpm, subDeps);
|
|
244
281
|
for (const sub of subDeps) {
|
|
@@ -272,17 +309,23 @@ async function collectAllTemplates(
|
|
|
272
309
|
}
|
|
273
310
|
}
|
|
274
311
|
|
|
275
|
-
|
|
312
|
+
export type InitProgressStep = "fetch" | "templates" | "write";
|
|
313
|
+
|
|
314
|
+
/** Init: 1) fetch versioni npm, 2) merge template, 3) mergePkg, 4) merge con file utente */
|
|
276
315
|
export async function initLib(
|
|
277
316
|
libs: string[],
|
|
278
317
|
cwd: string,
|
|
279
|
-
versionsFromNpm?: Map<string, string
|
|
318
|
+
versionsFromNpm?: Map<string, string>,
|
|
319
|
+
onProgress?: (step: InitProgressStep) => void
|
|
280
320
|
): Promise<void> {
|
|
281
321
|
const pkgNames = libs.map(toPkgName);
|
|
282
322
|
const versions = versionsFromNpm ?? new Map<string, string>();
|
|
323
|
+
|
|
324
|
+
onProgress?.("fetch");
|
|
283
325
|
const fetched = await fetchFlowPackageVersions(pkgNames);
|
|
284
326
|
for (const [k, v] of fetched) versions.set(k, v);
|
|
285
327
|
|
|
328
|
+
onProgress?.("templates");
|
|
286
329
|
const combined = new Map<string, string>();
|
|
287
330
|
const done = new Set<string>();
|
|
288
331
|
const order: string[] = [];
|
|
@@ -293,6 +336,7 @@ export async function initLib(
|
|
|
293
336
|
mergePkg(configDir, cwd, versions);
|
|
294
337
|
}
|
|
295
338
|
|
|
339
|
+
onProgress?.("write");
|
|
296
340
|
await writeMergedWithUser(combined, cwd, (path, conflicts) => resolveConflicts(conflicts, path));
|
|
297
341
|
|
|
298
342
|
const { rmSync } = await import("fs");
|
package/src/init/ui.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI per init: loading spinner e box coerenti con flow-os
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const V = "\x1b[95m";
|
|
6
|
+
const Y = "\x1b[38;5;226m";
|
|
7
|
+
const E = "\x1b[91m";
|
|
8
|
+
const R = "\x1b[0m";
|
|
9
|
+
const B = "\x1b[1m";
|
|
10
|
+
const DIM = "\x1b[2m";
|
|
11
|
+
|
|
12
|
+
const FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
|
|
13
|
+
|
|
14
|
+
export function stripAnsi(s: string): string {
|
|
15
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "").replace(/\n/g, "");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function pad(s: string, w: number): string {
|
|
19
|
+
return s + " ".repeat(Math.max(0, w - stripAnsi(s).length));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function box(lines: string[], color: string, w = 52): string {
|
|
23
|
+
const c = color;
|
|
24
|
+
const top = c + "╭" + "─".repeat(w) + "╮" + R;
|
|
25
|
+
const bottom = c + "╰" + "─".repeat(w) + "╯" + R;
|
|
26
|
+
const body = lines.map((l) => c + "│" + R + " " + pad(l, w - 2) + c + "│" + R).join("\n");
|
|
27
|
+
return "\n" + top + "\n" + body + "\n" + bottom + "\n";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
import type { InitProgressStep } from "./scaffold";
|
|
31
|
+
|
|
32
|
+
const STEP_LABELS: Record<InitProgressStep, string> = {
|
|
33
|
+
fetch: "Fetching packages",
|
|
34
|
+
templates: "Preparing templates",
|
|
35
|
+
write: "Writing files",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Esegue fn mostrando uno spinner elegante; onStep viene chiamato per aggiornare lo step */
|
|
39
|
+
export async function withLoading<T>(
|
|
40
|
+
fn: (onStep: (step: InitProgressStep) => void) => Promise<T>
|
|
41
|
+
): Promise<T> {
|
|
42
|
+
let step: InitProgressStep = "fetch";
|
|
43
|
+
let frame = 0;
|
|
44
|
+
const hideCursor = "\x1b[?25l";
|
|
45
|
+
const showCursor = "\x1b[?25h";
|
|
46
|
+
|
|
47
|
+
const render = () => {
|
|
48
|
+
const spin = V + FRAMES[frame % FRAMES.length] + R;
|
|
49
|
+
const msg = DIM + STEP_LABELS[step] + R;
|
|
50
|
+
const line = ` ${spin} ${msg}`;
|
|
51
|
+
process.stdout.write("\x1b[2K\r" + line);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
process.stdout.write(hideCursor);
|
|
55
|
+
const interval = setInterval(() => {
|
|
56
|
+
frame++;
|
|
57
|
+
render();
|
|
58
|
+
}, 80);
|
|
59
|
+
|
|
60
|
+
const onStep = (s: InitProgressStep) => {
|
|
61
|
+
step = s;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const result = await fn(onStep);
|
|
66
|
+
clearInterval(interval);
|
|
67
|
+
process.stdout.write("\x1b[2K\r" + showCursor);
|
|
68
|
+
return result;
|
|
69
|
+
} catch (e) {
|
|
70
|
+
clearInterval(interval);
|
|
71
|
+
process.stdout.write("\x1b[2K\r" + showCursor);
|
|
72
|
+
throw e;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const colors = { V, Y, E, R, B, DIM };
|