nowebcli 0.2.12 → 0.2.14

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.
Files changed (2) hide show
  1. package/package.json +2 -1
  2. package/scripts/noweb.mjs +51 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nowebcli",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "Create, preview and publish community templates for Noweb",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -68,6 +68,7 @@
68
68
  "motion": "^12.40.0",
69
69
  "next": "^16.2.10",
70
70
  "ogl": "^1.0.11",
71
+ "ora": "^9.4.1",
71
72
  "pg": "^8.22.0",
72
73
  "playwright": "^1.61.1",
73
74
  "postcss": "^8.5.6",
package/scripts/noweb.mjs CHANGED
@@ -10,6 +10,8 @@ import { basename, dirname, extname, join, resolve, sep } from "node:path";
10
10
  import { createInterface } from "node:readline/promises";
11
11
  import { fileURLToPath, pathToFileURL } from "node:url";
12
12
  import { register } from "tsx/esm/api";
13
+ import { styleText } from "node:util";
14
+ import ora from "ora";
13
15
 
14
16
  const root = process.cwd();
15
17
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -26,6 +28,20 @@ const apiBase = (process.env.NOWEB_URL || "https://noweb.app").replace(/\/$/, ""
26
28
  const clientId = "noweb-cli";
27
29
  register({ tsconfig: join(engineRoot, "tsconfig.json") });
28
30
 
31
+ const terminalColors = process.env.NO_COLOR === undefined && process.env.NODE_DISABLE_COLORS !== "1";
32
+ const progress = ora({
33
+ spinner: "dots",
34
+ color: terminalColors ? "cyan" : false,
35
+ prefixText: styleText("magenta", "noweb"),
36
+ isSilent: !process.stderr.isTTY || Boolean(process.env.CI),
37
+ discardStdin: false,
38
+ });
39
+
40
+ function success(message) {
41
+ progress.stop();
42
+ console.log(styleText("green", message));
43
+ }
44
+
29
45
  function usage() {
30
46
  console.log(`noweb
31
47
 
@@ -58,7 +74,7 @@ async function createTemplate(value) {
58
74
  writeFile(join(directory, "public/.gitkeep"), ""),
59
75
  ]);
60
76
  await setupWorkspace(shouldInstall);
61
- console.log(`Created ${validSlug} in ${directory}. Add a 16:9 public/cover.jpg (JPEG, up to 5 MB).`);
77
+ success(`Created ${validSlug} in ${directory}. Add a 16:9 public/cover.jpg (JPEG, up to 5 MB).`);
62
78
  }
63
79
 
64
80
  async function setupWorkspace(install = true) {
@@ -71,6 +87,7 @@ async function setupWorkspace(install = true) {
71
87
  ]);
72
88
  }
73
89
  if (!install) return;
90
+ progress.stop();
74
91
  const result = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", ["install"], { cwd: root, stdio: "inherit" });
75
92
  if (result.status !== 0) throw new Error("Could not install the template workspace dependencies.");
76
93
  }
@@ -82,16 +99,18 @@ async function checkRepositoryTemplates(value, quiet = false, assetRoot = "/asse
82
99
  const results = [];
83
100
  for (const currentSlug of slugs) {
84
101
  const assetBase = assetRoot === "/assets" ? assetRoot : `${assetRoot}/${currentSlug}`;
102
+ if (!quiet) progress.start(`Compiling ${currentSlug}…`);
85
103
  const result = await buildTemplate(join(repositoryTemplatesRoot, currentSlug), currentSlug, assetBase);
86
104
  results.push(result);
87
- if (!quiet) console.log(`✓ ${currentSlug} (${result.fields} fields, ${formatBytes(result.js.length + result.css.length)})`);
105
+ if (!quiet) success(`✓ ${currentSlug} (${result.fields} fields, ${formatBytes(result.js.length + result.css.length)})`);
88
106
  }
89
107
  return results;
90
108
  }
91
109
 
92
110
  async function checkProject(quiet = false) {
111
+ if (!quiet) progress.start("Validating and compiling template…");
93
112
  const result = await buildTemplate(root);
94
- if (!quiet) console.log(`✓ ${result.slug} (${result.fields} fields, ${formatBytes(result.js.length + result.css.length)})`);
113
+ if (!quiet) success(`✓ ${result.slug} (${result.fields} fields, ${formatBytes(result.js.length + result.css.length)})`);
95
114
  return result;
96
115
  }
97
116
 
@@ -141,6 +160,7 @@ async function createCover() {
141
160
  const coverPath = join(root, "public", "cover.jpg");
142
161
  const force = cliArgs.includes("--force");
143
162
  if (!force && existsSync(coverPath)) throw new Error("public/cover.jpg already exists. Use `noweb cover --force` to replace it.");
163
+ progress.start("Compiling cover preview…");
144
164
  const template = await buildTemplate(root, undefined, "/assets", false);
145
165
  const server = createServer(async (request, response) => {
146
166
  try {
@@ -162,12 +182,17 @@ async function createCover() {
162
182
  server.once("error", reject);
163
183
  server.listen(0, "127.0.0.1", resolve);
164
184
  });
165
- browser = await chromium.launch({ headless: true, args: ["--enable-unsafe-swiftshader", "--use-angle=swiftshader"] });
185
+ progress.color = terminalColors ? "magenta" : false;
186
+ progress.text = "Starting Chromium…";
187
+ browser = await chromium.launch({ headless: true, args: ["--enable-unsafe-swiftshader", "--enable-unsafe-webgpu", "--use-angle=swiftshader"] });
166
188
  const page = await browser.newPage({ viewport: { width: 1920, height: 1080 }, deviceScaleFactor: 1 });
167
189
  const errors = [];
168
190
  page.on("pageerror", (error) => errors.push(error.message));
169
191
  page.on("console", (message) => { if (message.type() === "error") errors.push(message.text()); });
170
192
  await page.goto(`http://127.0.0.1:${server.address().port}`, { waitUntil: "load" });
193
+ if (template.source.includes("shaders/react") && !await page.evaluate(async () => Boolean(await navigator.gpu?.requestAdapter()))) {
194
+ throw new Error("Cannot create shader cover: Chromium has no WebGPU adapter. Update Chromium with `npx playwright install chromium`.");
195
+ }
171
196
  await page.waitForFunction(() => document.getElementById("root")?.childElementCount, undefined, { timeout: 15_000 }).catch(() => {
172
197
  throw new Error(`Template did not render: ${errors.slice(0, 3).join("; ") || "empty preview"}`);
173
198
  });
@@ -177,13 +202,17 @@ async function createCover() {
177
202
  new Promise((resolve) => setTimeout(resolve, 10_000)),
178
203
  ]);
179
204
  });
205
+ progress.color = terminalColors ? "yellow" : false;
206
+ progress.text = "Rendering shaders — waiting 5 seconds…";
180
207
  await page.waitForTimeout(5000);
208
+ progress.color = terminalColors ? "cyan" : false;
209
+ progress.text = "Saving 1920×1080 cover…";
181
210
  const bytes = await page.screenshot({ type: "jpeg", quality: 90, fullPage: false });
182
211
  const { validateTemplateCover } = await load("lib/template-cover.ts");
183
212
  validateTemplateCover(bytes);
184
213
  await mkdir(join(root, "public"), { recursive: true });
185
214
  await writeFile(coverPath, bytes, { flag: force ? "w" : "wx" });
186
- console.log("Created public/cover.jpg (1920×1080, 16:9). Check the image before uploading.");
215
+ success("Created public/cover.jpg (1920×1080, 16:9). Check the image before uploading.");
187
216
  } finally {
188
217
  await browser?.close();
189
218
  server.closeAllConnections();
@@ -202,7 +231,7 @@ async function dev() {
202
231
  const template = await checkProject(true);
203
232
  response.writeHead(200, { "Cache-Control": "no-store", "Content-Type": "text/html; charset=utf-8" });
204
233
  response.end(previewHtml(template));
205
- console.log(`✓ rebuilt ${template.slug}`);
234
+ success(`✓ rebuilt ${template.slug}`);
206
235
  return;
207
236
  }
208
237
  await servePublic(url.pathname, response);
@@ -216,14 +245,14 @@ async function dev() {
216
245
  process.exitCode = 1;
217
246
  });
218
247
  server.listen(port, "127.0.0.1", () => {
219
- console.log(`Preview: http://127.0.0.1:${port}\nRefresh the page to rebuild.`);
248
+ console.log(styleText("cyan", `Preview: http://127.0.0.1:${port}\nRefresh the page to rebuild.`));
220
249
  });
221
250
  }
222
251
 
223
252
  async function upload() {
224
253
  const template = await checkProject();
225
254
  if (dryRun) {
226
- console.log(`Ready to upload ${template.slug}`);
255
+ success(`Ready to upload ${template.slug}`);
227
256
  return;
228
257
  }
229
258
 
@@ -239,6 +268,7 @@ async function upload() {
239
268
  for (const file of template.assetFiles) {
240
269
  form.append("assets", new Blob([file.body]), basename(file.path));
241
270
  }
271
+ progress.start("Uploading template, assets and cover…");
242
272
  const response = await fetch(`${apiBase}/api/templates/upload`, {
243
273
  method: "POST",
244
274
  headers: { Authorization: `Bearer ${credentials.token}` },
@@ -248,13 +278,14 @@ async function upload() {
248
278
  if (!response.ok) throw new Error(result.error || `Upload failed (${response.status}).`);
249
279
  if (!Number.isInteger(result.id) || result.id < 1) throw new Error("Upload returned an invalid template ID.");
250
280
  await writeFile(join(root, "template.json"), `${JSON.stringify({ id: result.id, slug: template.slug, type: template.type }, null, 2)}\n`);
251
- console.log(`Uploaded ${template.slug} (unpublished): ${new URL(result.url, apiBase)}`);
281
+ success(`Uploaded ${template.slug} (unpublished): ${new URL(result.url, apiBase)}`);
252
282
  }
253
283
 
254
284
  async function publishUploadedTemplate() {
255
285
  const template = JSON.parse(await readFile(join(root, "template.json"), "utf8"));
256
286
  if (!Number.isInteger(template.id) || template.id < 1) throw new Error("Upload this template first: npx nowebcli upload");
257
287
  const credentials = await requireCredentials();
288
+ progress.start("Publishing template…");
258
289
  const response = await fetch(`${apiBase}/api/templates/visibility`, {
259
290
  method: "PATCH",
260
291
  headers: { Authorization: `Bearer ${credentials.token}`, "Content-Type": "application/json" },
@@ -262,7 +293,7 @@ async function publishUploadedTemplate() {
262
293
  });
263
294
  const result = await response.json().catch(() => ({}));
264
295
  if (!response.ok) throw new Error(result.error || `Publish failed (${response.status}).`);
265
- console.log(`Published ${template.slug}: ${new URL(result.url, apiBase)}`);
296
+ success(`Published ${template.slug}: ${new URL(result.url, apiBase)}`);
266
297
  }
267
298
 
268
299
  async function deleteTemplate(value) {
@@ -286,7 +317,7 @@ async function deleteTemplate(value) {
286
317
  });
287
318
  const result = await response.json().catch(() => ({}));
288
319
  if (!response.ok) throw new Error(result.error || `Delete failed (${response.status}).`);
289
- console.log(`Deleted ${selectedSlug}`);
320
+ success(`Deleted ${selectedSlug}`);
290
321
  }
291
322
 
292
323
  async function listTemplates() {
@@ -320,6 +351,7 @@ async function downloadTemplate(value) {
320
351
  }
321
352
 
322
353
  const credentials = await requireCredentials();
354
+ progress.start(`Downloading ${selectedSlug}…`);
323
355
  const response = await fetch(`${apiBase}/api/templates/download?slug=${encodeURIComponent(selectedSlug)}`, {
324
356
  headers: { Authorization: `Bearer ${credentials.token}` },
325
357
  });
@@ -361,7 +393,7 @@ async function downloadTemplate(value) {
361
393
  ...(!assets.length ? [writeFile(join(root, "public/.gitkeep"), "")] : []),
362
394
  ]);
363
395
  await setupWorkspace(shouldInstall);
364
- console.log(`Downloaded ${downloadedSlug} into ${root}`);
396
+ success(`Downloaded ${downloadedSlug} into ${root}`);
365
397
  }
366
398
 
367
399
  async function login() {
@@ -375,6 +407,7 @@ async function login() {
375
407
 
376
408
  console.log(`Open ${device.verification_uri}\nCode: ${device.user_code}`);
377
409
  openBrowser(device.verification_uri_complete);
410
+ progress.start("Waiting for browser login…");
378
411
  let interval = Math.max(1, Number(device.interval) || 5);
379
412
  const expiresAt = Date.now() + Number(device.expires_in) * 1000;
380
413
  while (Date.now() < expiresAt) {
@@ -395,7 +428,7 @@ async function login() {
395
428
  token: token.access_token,
396
429
  expiresAt: Date.now() + Number(token.expires_in) * 1000,
397
430
  });
398
- console.log("✓ Logged in to Noweb");
431
+ success("✓ Logged in to Noweb");
399
432
  return;
400
433
  }
401
434
  if (token.error === "authorization_pending") continue;
@@ -508,7 +541,7 @@ async function publish(value) {
508
541
  continue;
509
542
  }
510
543
  await (await load("lib/template-screenshot.ts")).generateTemplatePreview(Number(published.id));
511
- console.log(`Published ${original.slug} as template ${published.id}`);
544
+ success(`Published ${original.slug} as template ${published.id}`);
512
545
  }
513
546
  } finally {
514
547
  await sql.end({ timeout: 5 });
@@ -738,6 +771,9 @@ try {
738
771
  else if (command === "help" || command === "--help" || command === "-h") usage();
739
772
  else throw new Error(`Unknown command: ${command}`);
740
773
  } catch (error) {
741
- console.error(`\n${error instanceof Error ? error.message : error}`);
774
+ progress.stop();
775
+ console.error(styleText("red", `\n${error instanceof Error ? error.message : error}`));
742
776
  process.exitCode = 1;
777
+ } finally {
778
+ progress.stop();
743
779
  }