weplan 0.2.0 → 0.3.0
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/README.md +1 -1
- package/dist/index.js +127 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# weplan
|
|
2
2
|
|
|
3
|
-
Deploy a self-contained HTML file and get back a shareable link
|
|
3
|
+
Deploy a self-contained HTML file and get back a shareable link in one command, in under a second. Built for CLI and agent workflows.
|
|
4
4
|
|
|
5
5
|
```console
|
|
6
6
|
$ npx weplan create report.html
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as readFileSync2 } from "node:fs";
|
|
5
|
-
import { extname } from "node:path";
|
|
4
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
5
|
+
import { extname, join as join2 } from "node:path";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
7
|
|
|
8
8
|
// ../../packages/core/src/index.ts
|
|
@@ -66,6 +66,28 @@ class WeplanClient {
|
|
|
66
66
|
async delete(slug) {
|
|
67
67
|
await this.request("DELETE", `/api/pages/${encodeURIComponent(slug)}`);
|
|
68
68
|
}
|
|
69
|
+
async fetchPage(slug) {
|
|
70
|
+
const headers = { accept: "application/json" };
|
|
71
|
+
if (this.token)
|
|
72
|
+
headers["authorization"] = `Bearer ${this.token}`;
|
|
73
|
+
let response;
|
|
74
|
+
try {
|
|
75
|
+
response = await this.fetchFn(`${this.baseUrl}/p/${encodeURIComponent(slug)}`, { headers });
|
|
76
|
+
} catch (cause) {
|
|
77
|
+
throw new WeplanError(`could not reach ${this.baseUrl}: ${cause.message}`, 0);
|
|
78
|
+
}
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
let message = `${response.status} ${response.statusText}`;
|
|
81
|
+
try {
|
|
82
|
+
const parsed = await response.json();
|
|
83
|
+
if (parsed.error)
|
|
84
|
+
message = parsed.error;
|
|
85
|
+
} catch {}
|
|
86
|
+
throw new WeplanError(message, response.status);
|
|
87
|
+
}
|
|
88
|
+
const visibility = response.headers.get("x-weplan-visibility") === "private" ? "private" : "public";
|
|
89
|
+
return { html: await response.text(), visibility };
|
|
90
|
+
}
|
|
69
91
|
async health() {
|
|
70
92
|
return this.request("GET", "/health");
|
|
71
93
|
}
|
|
@@ -228,7 +250,7 @@ async function pasteLogin(client, config, log) {
|
|
|
228
250
|
const pasted = (await prompt(" Code: ")).trim();
|
|
229
251
|
const separator = pasted.indexOf(".");
|
|
230
252
|
if (separator === -1 || pasted.slice(0, separator) !== state) {
|
|
231
|
-
throw new Error("invalid or expired sign-in code
|
|
253
|
+
throw new Error("invalid or expired sign-in code; start sign-in again and paste the full code");
|
|
232
254
|
}
|
|
233
255
|
const exchange = await client.exchangeCode(pasted.slice(separator + 1));
|
|
234
256
|
return { apiKey: exchange.apiKey, email: exchange.user.email };
|
|
@@ -245,12 +267,14 @@ function prompt(question) {
|
|
|
245
267
|
|
|
246
268
|
// src/index.ts
|
|
247
269
|
var DEFAULT_URL = "https://plan.wecodeph.dev";
|
|
248
|
-
var VERSION = "0.
|
|
249
|
-
var HELP = `weplan
|
|
270
|
+
var VERSION = "0.3.0";
|
|
271
|
+
var HELP = `weplan: deploy a self-contained HTML file, get a shareable link
|
|
250
272
|
|
|
251
273
|
Usage
|
|
252
274
|
weplan create <file.html> [--name <name>] [--private] [--allow <emails>]
|
|
253
275
|
weplan update <slug> <file.html> Replace a page's content, same URL
|
|
276
|
+
weplan get <url|slug> [--assets <dir>] Print a page's HTML; --assets writes it to <dir>/index.html
|
|
277
|
+
and extracts embedded images to files (for vision review)
|
|
254
278
|
weplan access <slug> [--public|--private] [--allow <emails>] [--revoke <emails>]
|
|
255
279
|
weplan list List your pages
|
|
256
280
|
weplan delete <slug> Delete a page
|
|
@@ -314,12 +338,66 @@ function printPage(page, verb, asJson) {
|
|
|
314
338
|
if (page.allow?.length)
|
|
315
339
|
lines.push("", ` allowed: ${page.allow.join(", ")}`);
|
|
316
340
|
if (page.expiresAt) {
|
|
317
|
-
lines.push("", ` anonymous upload
|
|
341
|
+
lines.push("", ` anonymous upload, expires ${page.expiresAt}. Run \`weplan login\` to keep pages.`);
|
|
318
342
|
}
|
|
319
343
|
process.stdout.write(lines.join(`
|
|
320
344
|
`) + `
|
|
321
345
|
`);
|
|
322
346
|
}
|
|
347
|
+
function parsePageTarget(target, asJson) {
|
|
348
|
+
if (!/^https?:\/\//i.test(target)) {
|
|
349
|
+
if (!/^[a-z0-9][a-z0-9-]{1,62}$/.test(target))
|
|
350
|
+
fail(`not a weplan slug or page URL: ${target}`, asJson);
|
|
351
|
+
return { slug: target };
|
|
352
|
+
}
|
|
353
|
+
let url;
|
|
354
|
+
try {
|
|
355
|
+
url = new URL(target);
|
|
356
|
+
} catch {
|
|
357
|
+
fail(`not a valid URL: ${target}`, asJson);
|
|
358
|
+
}
|
|
359
|
+
const match = url.pathname.match(/^\/p\/([^/]+)\/?$/);
|
|
360
|
+
if (!match)
|
|
361
|
+
fail(`not a weplan page URL (expected https://host/p/<slug>): ${target}`, asJson);
|
|
362
|
+
return { slug: decodeURIComponent(match[1]), origin: url.origin };
|
|
363
|
+
}
|
|
364
|
+
var IMAGE_EXTENSIONS = {
|
|
365
|
+
"image/png": "png",
|
|
366
|
+
"image/jpeg": "jpg",
|
|
367
|
+
"image/jpg": "jpg",
|
|
368
|
+
"image/gif": "gif",
|
|
369
|
+
"image/webp": "webp",
|
|
370
|
+
"image/bmp": "bmp",
|
|
371
|
+
"image/avif": "avif",
|
|
372
|
+
"image/svg+xml": "svg",
|
|
373
|
+
"image/x-icon": "ico",
|
|
374
|
+
"image/vnd.microsoft.icon": "ico"
|
|
375
|
+
};
|
|
376
|
+
function extractAssets(html, dir) {
|
|
377
|
+
mkdirSync2(dir, { recursive: true });
|
|
378
|
+
const images = [];
|
|
379
|
+
const seen = new Map;
|
|
380
|
+
const rewritten = html.replace(/data:(image\/[a-z0-9.+-]+);base64,([A-Za-z0-9+/=\s]+)/gi, (whole, type, data) => {
|
|
381
|
+
const mime = type.toLowerCase();
|
|
382
|
+
const ext = IMAGE_EXTENSIONS[mime];
|
|
383
|
+
if (!ext)
|
|
384
|
+
return whole;
|
|
385
|
+
const existing = seen.get(whole);
|
|
386
|
+
if (existing)
|
|
387
|
+
return existing;
|
|
388
|
+
const buffer = Buffer.from(data.replace(/\s+/g, ""), "base64");
|
|
389
|
+
if (buffer.length === 0)
|
|
390
|
+
return whole;
|
|
391
|
+
const name = `img-${images.length + 1}.${ext}`;
|
|
392
|
+
writeFileSync2(join2(dir, name), buffer);
|
|
393
|
+
images.push({ path: join2(dir, name), type: mime, bytes: buffer.length });
|
|
394
|
+
seen.set(whole, name);
|
|
395
|
+
return name;
|
|
396
|
+
});
|
|
397
|
+
const htmlPath = join2(dir, "index.html");
|
|
398
|
+
writeFileSync2(htmlPath, rewritten);
|
|
399
|
+
return { htmlPath, images };
|
|
400
|
+
}
|
|
323
401
|
function splitEmails(value) {
|
|
324
402
|
if (!value)
|
|
325
403
|
return;
|
|
@@ -335,6 +413,7 @@ async function main() {
|
|
|
335
413
|
public: { type: "boolean", default: false },
|
|
336
414
|
allow: { type: "string" },
|
|
337
415
|
revoke: { type: "string" },
|
|
416
|
+
assets: { type: "string" },
|
|
338
417
|
paste: { type: "boolean", default: false },
|
|
339
418
|
json: { type: "boolean", default: false },
|
|
340
419
|
url: { type: "string" },
|
|
@@ -378,6 +457,46 @@ async function main() {
|
|
|
378
457
|
printPage(page, "Deployed", asJson);
|
|
379
458
|
return;
|
|
380
459
|
}
|
|
460
|
+
case "get": {
|
|
461
|
+
const target = positionals[1];
|
|
462
|
+
if (!target)
|
|
463
|
+
fail("usage: weplan get <url|slug>", asJson);
|
|
464
|
+
const { slug, origin } = parsePageTarget(target, asJson);
|
|
465
|
+
const reader = origin && origin !== client.baseUrl ? new WeplanClient({ baseUrl: origin, token }) : client;
|
|
466
|
+
const page = await reader.fetchPage(slug);
|
|
467
|
+
const pageUrl = `${reader.baseUrl}/p/${slug}`;
|
|
468
|
+
if (flags.assets) {
|
|
469
|
+
const saved2 = extractAssets(page.html, flags.assets);
|
|
470
|
+
if (asJson) {
|
|
471
|
+
process.stdout.write(JSON.stringify({ slug, url: pageUrl, visibility: page.visibility, size: Buffer.byteLength(page.html), htmlPath: saved2.htmlPath, images: saved2.images }) + `
|
|
472
|
+
`);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const lines = [`Saved ${saved2.htmlPath} (${formatSize(Buffer.byteLength(page.html))}, ${page.visibility})`];
|
|
476
|
+
if (saved2.images.length) {
|
|
477
|
+
lines.push(`${saved2.images.length} embedded image${saved2.images.length === 1 ? "" : "s"} extracted:`);
|
|
478
|
+
for (const image of saved2.images)
|
|
479
|
+
lines.push(` ${image.path} ${image.type} ${formatSize(image.bytes)}`);
|
|
480
|
+
} else {
|
|
481
|
+
lines.push("no embedded images");
|
|
482
|
+
}
|
|
483
|
+
process.stdout.write(lines.join(`
|
|
484
|
+
`) + `
|
|
485
|
+
`);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (asJson) {
|
|
489
|
+
process.stdout.write(JSON.stringify({ slug, url: pageUrl, visibility: page.visibility, size: Buffer.byteLength(page.html), html: page.html }) + `
|
|
490
|
+
`);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
process.stdout.write(page.html);
|
|
494
|
+
if (!page.html.endsWith(`
|
|
495
|
+
`))
|
|
496
|
+
process.stdout.write(`
|
|
497
|
+
`);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
381
500
|
case "update": {
|
|
382
501
|
const [, slug, file] = positionals;
|
|
383
502
|
if (!slug || !file)
|
|
@@ -469,14 +588,14 @@ Signed in as ${result.email}. Credentials saved to ${configPath()}
|
|
|
469
588
|
case "status": {
|
|
470
589
|
const health = await client.health();
|
|
471
590
|
process.stdout.write(asJson ? JSON.stringify(health) + `
|
|
472
|
-
` : `ok
|
|
591
|
+
` : `ok: ${baseUrl}
|
|
473
592
|
login: ${health.loginEnabled ? "enabled" : "not configured"}
|
|
474
593
|
anonymous uploads: ${health.anonymousUploads ? "enabled (12h expiry)" : "disabled"}
|
|
475
594
|
`);
|
|
476
595
|
return;
|
|
477
596
|
}
|
|
478
597
|
default:
|
|
479
|
-
fail(`unknown command "${command}"
|
|
598
|
+
fail(`unknown command "${command}"; run weplan --help`, asJson);
|
|
480
599
|
}
|
|
481
600
|
}
|
|
482
601
|
main().catch((cause) => {
|