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.
- package/package.json +2 -1
- package/scripts/noweb.mjs +51 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nowebcli",
|
|
3
|
-
"version": "0.2.
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|