create-flow-os 0.0.27 → 0.0.29

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.27",
3
+ "version": "0.0.29",
4
4
  "license": "PolyForm-Shield-1.0.0",
5
5
  "type": "module",
6
6
  "dependencies": {
7
- "@flow-os/client": "^0.0.27"
7
+ "@flow-os/client": "^0.0.29"
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: fetch versioni pacchetti da npm (latest/dev), init libs, poi output box
40
+ // Execute: init con loading UI elegante
66
41
  // ───────────────────────────────────────────────────────────────────────────────
67
42
  const pkgNames = toInit.map(toPkgName);
68
- process.stdout.write(V + "Fetching latest @flow-os/* packages..." + R);
69
- const versions = await fetchFlowPackageVersions(pkgNames);
70
- process.stdout.write(versions.size ? ` ${V}${versions.size} pkg${R}\n` : " (fallback)\n");
71
- await initLib(toInit, cwd, versions);
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), "", Y + "Suggerimento: bun create flow-os@latest" + R], V));
52
+ out.push(box([B + "Initialized:" + R, ...toInit.map((l) => iconOk + " " + V + l + 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
 
@@ -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 (latest o dev) */
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}?t=${Date.now()}`, {
80
- cache: "no-store",
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
- return data["dist-tags"]?.[tag] ?? data["dist-tags"]?.["latest"];
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 per qualsiasi @flow-os/* (bypass cache) */
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, { cache: "no-store" });
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 configDir = join(tmpDir, "package", "config");
123
- if (!existsSync(configDir)) return null;
124
- const files = collectConfigFiles(configDir);
125
- return { files, configDir, tmpDir };
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
- /** Init: 1) fetch versioni npm per tutti i pacchetti, 2) merge template in memoria, 3) mergePkg, 4) merge con file utente */
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 };