From e76a18139c59e023ed8ef120d37ffc982d3aa21c Mon Sep 17 00:00:00 2001 From: sandwich Date: Thu, 5 Mar 2026 14:04:46 +0100 Subject: [PATCH 1/5] chore: bump version to 0.22.2 --- VERSION | 2 +- deno.json | 3 ++- src/version.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 88541566..faa5fb26 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.21.0 +0.22.2 diff --git a/deno.json b/deno.json index 353e6150..6dfde7f0 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@nsyte/cli", - "version": "0.21.0", + "version": "0.22.2", "description": "nsyte - publish your site to nostr and blossom servers", "license": "MIT", "exports": "./src/cli.ts", @@ -13,6 +13,7 @@ "coverage": "deno test --allow-read --allow-write --allow-net --allow-env --allow-import --coverage=test-output/coverage --no-check", "coverage:badge": "deno run --allow-read --allow-write --allow-run scripts/generate-coverage-badge.ts", "coverage:report": "deno task coverage && deno task coverage:badge", + "version": "deno run --allow-read --allow-write --allow-run scripts/sync-version.ts", "compile": "deno compile --allow-run --allow-read --allow-write --allow-net --allow-env --allow-sys --output dist/nsyte src/cli.ts", "compile:all": "deno task compile:linux && deno task compile:macos && deno task compile:windows", "compile:linux": "deno compile --no-check --allow-run --allow-read --allow-write --allow-net --allow-env --allow-sys --target x86_64-unknown-linux-gnu --output dist/nsyte-linux src/cli.ts", diff --git a/src/version.ts b/src/version.ts index e1f8534b..0e09107d 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = "0.21.0"; +export const version = "0.22.2"; From e746363bd7a9c98eb7736c851691a5f71889b6fb Mon Sep 17 00:00:00 2001 From: sandwich Date: Thu, 5 Mar 2026 14:05:35 +0100 Subject: [PATCH 2/5] update version and fix progress bar --- scripts/sync-version.ts | 26 ++++++++++++++++++++++++++ src/ui/progress.ts | 31 +++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/scripts/sync-version.ts b/scripts/sync-version.ts index 80c5454c..f8d4e6ad 100644 --- a/scripts/sync-version.ts +++ b/scripts/sync-version.ts @@ -22,3 +22,29 @@ const denoJsonPath = "deno.json"; const denoJson = JSON.parse(await Deno.readTextFile(denoJsonPath)); denoJson.version = version; await Deno.writeTextFile(denoJsonPath, JSON.stringify(denoJson, null, 2) + "\n"); + +// Git commit and tag +const tag = `v${version}`; +const commitMsg = `chore: bump version to ${version}`; + +const add = new Deno.Command("git", { args: ["add", "VERSION", "src/version.ts", "deno.json"] }); +const addResult = await add.output(); +if (!addResult.success) { + console.error("git add failed"); + Deno.exit(1); +} + +const commit = new Deno.Command("git", { args: ["commit", "-m", commitMsg] }); +const commitResult = await commit.output(); +if (!commitResult.success) { + console.error("git commit failed (maybe no changes?)"); +} + +const gitTag = new Deno.Command("git", { args: ["tag", tag] }); +const tagResult = await gitTag.output(); +if (!tagResult.success) { + console.error(`git tag ${tag} failed (tag may already exist)`); + Deno.exit(1); +} + +console.log(`Version synced and tagged as ${tag}`); diff --git a/src/ui/progress.ts b/src/ui/progress.ts index 39361dfc..57f3d0f2 100644 --- a/src/ui/progress.ts +++ b/src/ui/progress.ts @@ -5,6 +5,10 @@ const PROGRESS_BAR_WIDTH = 30; const PROGRESS_CHAR = "█"; const INCOMPLETE_CHAR = "░"; +function stripAnsi(text: string): string { + return text.replace(/\x1b\[[0-9;]*m/g, ""); +} + /** * Format a progress bar with colored output */ @@ -187,8 +191,31 @@ export class ProgressRenderer { } const skipped = data.skipped ?? 0; - const progressText = - `[${bar}] ${percent}% | ${done}/${data.total} files | ${data.completed} succeeded, ${skipped} skipped, ${data.failed} failed, ${data.inProgress} in progress | Elapsed: ${elapsed}s | ETA: ${eta}${serverInfo}`; + + // Build progress text with segments that can be dropped to fit terminal width + const segments = [ + `[${bar}] ${percent}%`, + `${done}/${data.total} files`, + `${data.completed} ok, ${skipped} skip, ${data.failed} fail, ${data.inProgress} active`, + `${elapsed}s`, + `ETA: ${eta}`, + ]; + if (serverInfo) segments.push(serverInfo); + + let progressText = segments.join(" | "); + + // Truncate to terminal width to prevent line wrapping, which breaks \r overwrite + try { + const { columns } = Deno.consoleSize(); + if (columns > 0) { + while (segments.length > 1 && stripAnsi(segments.join(" | ")).length > columns) { + segments.pop(); + } + progressText = segments.join(" | "); + } + } catch { + // consoleSize() throws if not a TTY — just write the full text + } Deno.stdout.writeSync(new TextEncoder().encode(progressText)); From 6cd614e11750e119e61c80f71079970a8449313c Mon Sep 17 00:00:00 2001 From: sandwich Date: Thu, 5 Mar 2026 14:30:27 +0100 Subject: [PATCH 3/5] add retries and visible cues --- src/commands/deploy.ts | 19 ++++- src/lib/upload.ts | 20 ++++- src/ui/progress.ts | 15 +++- tests/unit/ui_progress_test.ts | 138 +++++++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 8 deletions(-) diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 24d31d87..11591b66 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -1493,7 +1493,9 @@ async function uploadFiles( flushQueuedLogs(); console.log(""); - if (uploadedCount === preparedFiles.length) { + const allSucceeded = uploadedCount === preparedFiles.length && + uploadResponses.length === preparedFiles.length; + if (allSucceeded) { const msg = `${uploadedCount} files uploaded successfully (${formatFileSize(uploadedSize)})`; progressRenderer.complete(true, msg); } else if (uploadedCount > 0) { @@ -1523,11 +1525,14 @@ async function uploadFiles( if (uploadedCount > 0) { console.log(formatSectionHeader("Blobs Upload Results (Blossom)")); - if (uploadedCount === preparedFiles.length) { + if (allSucceeded) { console.log(colors.green(`✓ All ${uploadedCount} files successfully uploaded`)); } else { + const failedCount = preparedFiles.length - uploadedCount; console.log( - colors.yellow(`${uploadedCount}/${preparedFiles.length} files successfully uploaded`), + colors.yellow( + `${uploadedCount}/${preparedFiles.length} blobs uploaded, ${failedCount} failed`, + ), ); } messageCollector.printFileSuccessSummary(); @@ -1553,6 +1558,14 @@ async function uploadFiles( } } console.log(formatServerResults(serverResults)); + + const totalBlobs = uploadResponses.length; + const successBlobs = uploadResponses.filter((r) => r.success).length; + const pct = totalBlobs === 0 ? 100 : Math.round((successBlobs / totalBlobs) * 100); + const colorFn = pct === 100 ? colors.green : pct > 0 ? colors.yellow : colors.red; + console.log( + colorFn(`Overall: ${successBlobs}/${totalBlobs} blobs on at least one server (${pct}%)`), + ); console.log(""); // Create and publish site manifest event after all files are uploaded diff --git a/src/lib/upload.ts b/src/lib/upload.ts index 0d2e7a4b..9f83e7c5 100644 --- a/src/lib/upload.ts +++ b/src/lib/upload.ts @@ -153,6 +153,7 @@ export interface UploadProgress { failed: number; inProgress: number; skipped: number; + retrying: number; } export type UploadResponse = { @@ -395,6 +396,7 @@ export async function processUploads( failed: 0, inProgress: 0, skipped: 0, + retrying: 0, }; if (progressCallback) { @@ -446,7 +448,12 @@ export async function processUploads( const chunkResults = await Promise.all( chunk.map(async (file) => { try { - return await uploadFile(file, baseDir, servers, authTokenMap, relays, userPubkey); + return await uploadFile(file, baseDir, servers, authTokenMap, relays, userPubkey, 0, (delta) => { + progress.retrying += delta; + if (progressCallback) { + progressCallback({ ...progress }); + } + }); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -545,6 +552,7 @@ async function uploadFile( relays: string[], userPubkey: string, retryCount = 0, + onRetry?: (delta: number) => void, ): Promise { // Ensure serverResults is visible in catch blocks const serverResults: { @@ -647,7 +655,15 @@ async function uploadFile( `Retrying upload for ${file.path} (attempt ${retryCount + 1}/${MAX_RETRIES})`, ); await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS)); - return uploadFile(file, baseDir, servers, authTokenMap, relays, userPubkey, retryCount + 1); + onRetry?.(1); + try { + const result = await uploadFile(file, baseDir, servers, authTokenMap, relays, userPubkey, retryCount + 1, onRetry); + onRetry?.(-1); + return result; + } catch (e) { + onRetry?.(-1); + throw e; + } } return { diff --git a/src/ui/progress.ts b/src/ui/progress.ts index 57f3d0f2..a4eed266 100644 --- a/src/ui/progress.ts +++ b/src/ui/progress.ts @@ -58,6 +58,7 @@ interface ProgressData { failed: number; inProgress: number; skipped?: number; + retrying?: number; serverStats?: { [filename: string]: { successCount: number; @@ -175,8 +176,15 @@ export class ProgressRenderer { eta = etaSeconds <= 0 ? "0s" : `${etaSeconds}s`; } - const filledLength = Math.floor((percent * this.barLength) / 100); - const bar = "█".repeat(filledLength) + "░".repeat(this.barLength - filledLength); + const greenW = Math.floor((data.completed / data.total) * this.barLength); + const yellowW = Math.floor(((data.retrying ?? 0) / data.total) * this.barLength); + const redW = Math.floor((data.failed / data.total) * this.barLength); + const grayW = Math.max(0, this.barLength - greenW - yellowW - redW); + + const bar = colors.green("█".repeat(greenW)) + + colors.yellow("█".repeat(yellowW)) + + colors.red("█".repeat(redW)) + + "░".repeat(grayW); let serverInfo = ""; if (data.serverStats) { @@ -191,12 +199,13 @@ export class ProgressRenderer { } const skipped = data.skipped ?? 0; + const retrying = data.retrying ?? 0; // Build progress text with segments that can be dropped to fit terminal width const segments = [ `[${bar}] ${percent}%`, `${done}/${data.total} files`, - `${data.completed} ok, ${skipped} skip, ${data.failed} fail, ${data.inProgress} active`, + `${data.completed} ok, ${skipped} skip, ${retrying} retry, ${data.failed} fail, ${data.inProgress} active`, `${elapsed}s`, `ETA: ${eta}`, ]; diff --git a/tests/unit/ui_progress_test.ts b/tests/unit/ui_progress_test.ts index 28170d32..b72b3c7d 100644 --- a/tests/unit/ui_progress_test.ts +++ b/tests/unit/ui_progress_test.ts @@ -416,6 +416,144 @@ Deno.test("UI Progress - ProgressRenderer", async (t) => { }); }); +Deno.test("UI Progress - ProgressRenderer colored bar and retry count", async (t) => { + let stdoutStub: any; + + await t.step("should show retry count in progress text", () => { + restore(); + stdoutStub = stub(Deno.stdout, "writeSync", () => 0); + + const progress = new ProgressRenderer(10); + progress.update({ + total: 10, + completed: 5, + failed: 1, + inProgress: 2, + retrying: 2, + }); + + let found = false; + for (let i = 0; i < stdoutStub.calls.length; i++) { + const output = new TextDecoder().decode(stdoutStub.calls[i].args[0]); + if (output.includes("retry")) { + assertStringIncludes(output, "2 retry"); + found = true; + break; + } + } + assertEquals(found, true, "Should find retry count in output"); + + stdoutStub.restore(); + }); + + await t.step("should render green segments for completed files", () => { + restore(); + stdoutStub = stub(Deno.stdout, "writeSync", () => 0); + + const progress = new ProgressRenderer(10); + progress.update({ + total: 10, + completed: 10, + failed: 0, + inProgress: 0, + }); + + // All 30 bar chars should be green (ANSI green escape) + let found = false; + for (let i = 0; i < stdoutStub.calls.length; i++) { + const output = new TextDecoder().decode(stdoutStub.calls[i].args[0]); + if (output.includes("█")) { + // Should contain green ANSI code and not contain red blocks + assertStringIncludes(output, "\x1b[32m"); // green + found = true; + break; + } + } + assertEquals(found, true, "Should find green bar segments"); + + stdoutStub.restore(); + }); + + await t.step("should render red segments for failed files", () => { + restore(); + stdoutStub = stub(Deno.stdout, "writeSync", () => 0); + + const progress = new ProgressRenderer(10); + progress.update({ + total: 10, + completed: 5, + failed: 5, + inProgress: 0, + }); + + let found = false; + for (let i = 0; i < stdoutStub.calls.length; i++) { + const output = new TextDecoder().decode(stdoutStub.calls[i].args[0]); + if (output.includes("█")) { + assertStringIncludes(output, "\x1b[31m"); // red + found = true; + break; + } + } + assertEquals(found, true, "Should find red bar segments"); + + stdoutStub.restore(); + }); + + await t.step("should render yellow segments for retrying files", () => { + restore(); + stdoutStub = stub(Deno.stdout, "writeSync", () => 0); + + const progress = new ProgressRenderer(10); + progress.update({ + total: 10, + completed: 3, + failed: 0, + inProgress: 4, + retrying: 3, + }); + + let found = false; + for (let i = 0; i < stdoutStub.calls.length; i++) { + const output = new TextDecoder().decode(stdoutStub.calls[i].args[0]); + if (output.includes("█")) { + assertStringIncludes(output, "\x1b[33m"); // yellow + found = true; + break; + } + } + assertEquals(found, true, "Should find yellow bar segments"); + + stdoutStub.restore(); + }); + + await t.step("should default retrying to 0 when not provided", () => { + restore(); + stdoutStub = stub(Deno.stdout, "writeSync", () => 0); + + const progress = new ProgressRenderer(10); + progress.update({ + total: 10, + completed: 5, + failed: 0, + inProgress: 5, + }); + + let found = false; + for (let i = 0; i < stdoutStub.calls.length; i++) { + const output = new TextDecoder().decode(stdoutStub.calls[i].args[0]); + if (output.includes("retry")) { + assertStringIncludes(output, "0 retry"); + found = true; + break; + } + } + assertEquals(found, true, "Should show 0 retry when not provided"); + + stdoutStub.restore(); + }); +}); + // Clean up Deno.test("Cleanup", () => { restore(); From 35ecb33e1de81419ba04a59efa9c362ee7aaa9f9 Mon Sep 17 00:00:00 2001 From: sandwich Date: Thu, 5 Mar 2026 17:48:11 +0100 Subject: [PATCH 4/5] fix: minor bugs --- deno.json | 2 +- src/ui/progress.ts | 17 ++++++++++++----- tests/unit/ui_progress_test.ts | 2 ++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/deno.json b/deno.json index 6dfde7f0..2cc8a510 100644 --- a/deno.json +++ b/deno.json @@ -13,7 +13,7 @@ "coverage": "deno test --allow-read --allow-write --allow-net --allow-env --allow-import --coverage=test-output/coverage --no-check", "coverage:badge": "deno run --allow-read --allow-write --allow-run scripts/generate-coverage-badge.ts", "coverage:report": "deno task coverage && deno task coverage:badge", - "version": "deno run --allow-read --allow-write --allow-run scripts/sync-version.ts", + "version": "deno run --allow-read --allow-write --allow-run --allow-env scripts/sync-version.ts", "compile": "deno compile --allow-run --allow-read --allow-write --allow-net --allow-env --allow-sys --output dist/nsyte src/cli.ts", "compile:all": "deno task compile:linux && deno task compile:macos && deno task compile:windows", "compile:linux": "deno compile --no-check --allow-run --allow-read --allow-write --allow-net --allow-env --allow-sys --target x86_64-unknown-linux-gnu --output dist/nsyte-linux src/cli.ts", diff --git a/src/ui/progress.ts b/src/ui/progress.ts index a4eed266..6ecb5f76 100644 --- a/src/ui/progress.ts +++ b/src/ui/progress.ts @@ -176,10 +176,17 @@ export class ProgressRenderer { eta = etaSeconds <= 0 ? "0s" : `${etaSeconds}s`; } - const greenW = Math.floor((data.completed / data.total) * this.barLength); - const yellowW = Math.floor(((data.retrying ?? 0) / data.total) * this.barLength); - const redW = Math.floor((data.failed / data.total) * this.barLength); - const grayW = Math.max(0, this.barLength - greenW - yellowW - redW); + let greenW = 0; + let yellowW = 0; + let redW = 0; + let grayW = this.barLength; + + if (data.total > 0) { + greenW = Math.floor((data.completed / data.total) * this.barLength); + yellowW = Math.floor(((data.retrying ?? 0) / data.total) * this.barLength); + redW = Math.floor((data.failed / data.total) * this.barLength); + grayW = Math.max(0, this.barLength - greenW - yellowW - redW); + } const bar = colors.green("█".repeat(greenW)) + colors.yellow("█".repeat(yellowW)) @@ -192,7 +199,7 @@ export class ProgressRenderer { if (entries.length > 0) { const latestFile = entries[entries.length - 1]; const [filename, stats] = latestFile; - serverInfo = ` | ${ + serverInfo = `${ colors.cyan(`${stats.successCount}/${stats.totalServers}`) } servers for ${filename.split("/").pop()}`; } diff --git a/tests/unit/ui_progress_test.ts b/tests/unit/ui_progress_test.ts index b72b3c7d..15ce9af4 100644 --- a/tests/unit/ui_progress_test.ts +++ b/tests/unit/ui_progress_test.ts @@ -422,6 +422,8 @@ Deno.test("UI Progress - ProgressRenderer colored bar and retry count", async (t await t.step("should show retry count in progress text", () => { restore(); stdoutStub = stub(Deno.stdout, "writeSync", () => 0); + // Stub consoleSize to return wide terminal so truncation doesn't drop segments + stub(Deno, "consoleSize", () => ({ columns: 300, rows: 50 })); const progress = new ProgressRenderer(10); progress.update({ From 581d684449b0e27b1bf8ef3c1a916aeeceddcd4e Mon Sep 17 00:00:00 2001 From: sandwich Date: Sat, 7 Mar 2026 16:17:51 +0100 Subject: [PATCH 5/5] update version in a ridiculous number of places --- VERSION | 2 +- deno.json | 2 +- src/version.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index faa5fb26..d90746a7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.22.2 +0.22.3 diff --git a/deno.json b/deno.json index 2cc8a510..6ef51d7f 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@nsyte/cli", - "version": "0.22.2", + "version": "0.22.3", "description": "nsyte - publish your site to nostr and blossom servers", "license": "MIT", "exports": "./src/cli.ts", diff --git a/src/version.ts b/src/version.ts index 0e09107d..a1eb95e3 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = "0.22.2"; +export const version = "0.22.3";