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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +127 -8
  3. 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 in one command, in under a second. Built for CLI and agent workflows.
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 start sign-in again and paste the full 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.2.0";
249
- var HELP = `weplan deploy a self-contained HTML file, get a shareable link
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 expires ${page.expiresAt}. Run \`weplan login\` to keep pages.`);
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 ${baseUrl}
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}" run weplan --help`, asJson);
598
+ fail(`unknown command "${command}"; run weplan --help`, asJson);
480
599
  }
481
600
  }
482
601
  main().catch((cause) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weplan",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Deploy a self-contained HTML file and get back a shareable link. Built for CLI and agent workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",