@hirokisakabe/pom-cli 0.3.2 → 0.4.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 CHANGED
@@ -8,6 +8,16 @@ CLI tool for [pom](https://github.com/hirokisakabe/pom) — preview and build pr
8
8
  npm install -g @hirokisakabe/pom-cli
9
9
  ```
10
10
 
11
+ ## Quick Start
12
+
13
+ One command is all it takes — no global install required:
14
+
15
+ ```bash
16
+ npx @hirokisakabe/pom-cli preview slides.pom.xml
17
+ ```
18
+
19
+ This starts a local preview server, opens your browser automatically, and live-reloads the preview every time the file is saved. This also works well when invoked from agent skills or scripts.
20
+
11
21
  ## Usage
12
22
 
13
23
  ### Preview
@@ -19,7 +29,13 @@ pom preview slides.pom.xml
19
29
  pom preview slides.pom.md
20
30
  ```
21
31
 
22
- Open http://localhost:3000 in your browser. The page updates automatically when the file is saved.
32
+ The browser opens http://localhost:3000 automatically. The page updates whenever the file is saved — including atomic saves performed by editors like Vim.
33
+
34
+ To suppress the automatic browser open (e.g. in CI or headless environments):
35
+
36
+ ```bash
37
+ pom preview slides.pom.xml --no-open
38
+ ```
23
39
 
24
40
  To use a different port (e.g. when 3000 is already in use):
25
41
 
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAUA,wBAAsB,QAAQ,CAC5B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACpD,OAAO,CAAC,IAAI,CAAC,CAwEf;AAED,wBAAsB,aAAa,CACjC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAClC,OAAO,CAAC,IAAI,CAAC,CA2Df"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAWA,wBAAsB,QAAQ,CAC5B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACpD,OAAO,CAAC,IAAI,CAAC,CAwEf;AAED,wBAAsB,aAAa,CACjC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAClC,OAAO,CAAC,IAAI,CAAC,CAuDf"}
package/dist/build.js CHANGED
@@ -2,6 +2,7 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import { buildPptx, DiagnosticsError } from "@hirokisakabe/pom";
4
4
  import { parseMd } from "@hirokisakabe/pom-md";
5
+ import { watchInputFile } from "./watch.js";
5
6
  function makeLog(verbose) {
6
7
  if (!verbose)
7
8
  return (_msg) => { };
@@ -110,13 +111,8 @@ export async function runBuildWatch(inputFile, outputFile, options = {}) {
110
111
  }
111
112
  }
112
113
  await doBuild();
113
- let debounceTimer = null;
114
- fs.watch(absInput, () => {
115
- if (debounceTimer)
116
- clearTimeout(debounceTimer);
117
- debounceTimer = setTimeout(() => {
118
- watchLog("File changed, rebuilding...");
119
- void doBuild();
120
- }, 100);
114
+ watchInputFile(absInput, () => {
115
+ watchLog("File changed, rebuilding...");
116
+ void doBuild();
121
117
  });
122
118
  }
package/dist/cli.js CHANGED
@@ -17,6 +17,7 @@ program
17
17
  .argument("<input>", "Input file (.pom.xml or .pom.md)")
18
18
  .option("--port <number>", "Port to listen on")
19
19
  .option("--verbose", "Show build step timing on stderr")
20
+ .option("--no-open", "Do not open the browser automatically")
20
21
  .action((input, options) => {
21
22
  let port;
22
23
  if (options.port !== undefined) {
@@ -27,7 +28,10 @@ program
27
28
  }
28
29
  }
29
30
  try {
30
- runPreview(input, port, { verbose: options.verbose });
31
+ runPreview(input, port, {
32
+ verbose: options.verbose,
33
+ open: options.open,
34
+ });
31
35
  }
32
36
  catch (err) {
33
37
  console.error(err instanceof Error ? err.message : String(err));
package/dist/preview.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export declare function runPreview(inputFile: string, port?: number, options?: {
2
2
  verbose?: boolean;
3
+ open?: boolean;
3
4
  }): void;
4
5
  //# sourceMappingURL=preview.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AA8XA,wBAAgB,UAAU,CACxB,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,MAAqB,EAC3B,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAClC,IAAI,CAkHN"}
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AAqZA,wBAAgB,UAAU,CACxB,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,MAAqB,EAC3B,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAClD,IAAI,CAmIN"}
package/dist/preview.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { spawn } from "child_process";
1
2
  import fs from "fs";
2
3
  import http from "http";
3
4
  import { fileURLToPath } from "url";
@@ -5,6 +6,7 @@ import path from "path";
5
6
  import { buildPptx } from "@hirokisakabe/pom";
6
7
  import { parseMd } from "@hirokisakabe/pom-md";
7
8
  import { convertPptxToSvg } from "pptx-glimpse";
9
+ import { watchInputFile } from "./watch.js";
8
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
11
  const DEFAULT_PORT = 3000;
10
12
  function makeLog(verbose) {
@@ -12,6 +14,28 @@ function makeLog(verbose) {
12
14
  return (_msg) => { };
13
15
  return (msg) => process.stderr.write(`[pom] ${msg}\n`);
14
16
  }
17
+ function openBrowser(url) {
18
+ let cmd;
19
+ let args;
20
+ if (process.platform === "darwin") {
21
+ cmd = "open";
22
+ args = [url];
23
+ }
24
+ else if (process.platform === "win32") {
25
+ // 空文字はウィンドウタイトル。省略すると URL がタイトル扱いされる
26
+ cmd = "cmd";
27
+ args = ["/c", "start", "", url];
28
+ }
29
+ else {
30
+ cmd = "xdg-open";
31
+ args = [url];
32
+ }
33
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
34
+ child.on("error", () => {
35
+ console.log(`Could not open browser automatically. Open ${url} manually.`);
36
+ });
37
+ child.unref();
38
+ }
15
39
  const EXTRA_FONT_MAPPING = {
16
40
  "游ゴシック Light": "Noto Sans CJK JP",
17
41
  "Yu Gothic Light": "Noto Sans CJK JP",
@@ -371,7 +395,15 @@ export function runPreview(inputFile, port = DEFAULT_PORT, options = {}) {
371
395
  client.write(`event: update\ndata: ${data}\n\n`);
372
396
  }
373
397
  }
398
+ let isBuilding = false;
399
+ let pendingRefresh = false;
374
400
  function refresh() {
401
+ if (isBuilding) {
402
+ pendingRefresh = true;
403
+ return;
404
+ }
405
+ isBuilding = true;
406
+ pendingRefresh = false;
375
407
  const totalStart = Date.now();
376
408
  log("File changed, rebuilding...");
377
409
  broadcastBuilding();
@@ -385,29 +417,34 @@ export function runPreview(inputFile, port = DEFAULT_PORT, options = {}) {
385
417
  type: "error",
386
418
  message: err instanceof Error ? err.message : String(err),
387
419
  });
420
+ })
421
+ .finally(() => {
422
+ isBuilding = false;
423
+ if (pendingRefresh)
424
+ refresh();
388
425
  });
389
426
  }
427
+ isBuilding = true;
390
428
  const initialStart = Date.now();
391
429
  generateSvgs(absInput, verbose)
392
430
  .then((result) => {
393
431
  currentResult = result;
394
- initialBuildDone = true;
395
432
  log(`Initial build done (total: ${Date.now() - initialStart}ms)`);
396
433
  broadcast(result);
397
434
  })
398
435
  .catch((err) => {
399
- initialBuildDone = true;
400
436
  broadcast({
401
437
  type: "error",
402
438
  message: err instanceof Error ? err.message : String(err),
403
439
  });
440
+ })
441
+ .finally(() => {
442
+ initialBuildDone = true;
443
+ isBuilding = false;
444
+ if (pendingRefresh)
445
+ refresh();
404
446
  });
405
- let debounceTimer = null;
406
- fs.watch(absInput, () => {
407
- if (debounceTimer)
408
- clearTimeout(debounceTimer);
409
- debounceTimer = setTimeout(refresh, 100);
410
- });
447
+ watchInputFile(absInput, refresh);
411
448
  const html = buildPreviewHtml(path.basename(absInput));
412
449
  const server = http.createServer((req, res) => {
413
450
  if (req.url === "/_sse") {
@@ -446,8 +483,12 @@ export function runPreview(inputFile, port = DEFAULT_PORT, options = {}) {
446
483
  process.exit(1);
447
484
  });
448
485
  server.listen(port, () => {
449
- console.log(`Preview server: http://localhost:${port}`);
486
+ const url = `http://localhost:${port}`;
487
+ console.log(`Preview server: ${url}`);
450
488
  console.log(`Watching: ${absInput}`);
451
489
  console.log("Press Ctrl+C to stop");
490
+ if (options.open ?? true) {
491
+ openBrowser(url);
492
+ }
452
493
  });
453
494
  }
@@ -0,0 +1,3 @@
1
+ import fs from "fs";
2
+ export declare function watchInputFile(absPath: string, onChange: () => void): fs.FSWatcher;
3
+ //# sourceMappingURL=watch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAOpB,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,IAAI,GACnB,EAAE,CAAC,SAAS,CA4Bd"}
package/dist/watch.js ADDED
@@ -0,0 +1,37 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ const DEBOUNCE_MS = 100;
4
+ // Watches the parent directory instead of the file itself so the watcher
5
+ // survives atomic saves (rename + replace) performed by editors like Vim.
6
+ export function watchInputFile(absPath, onChange) {
7
+ const dir = path.dirname(absPath);
8
+ const base = path.basename(absPath);
9
+ let debounceTimer = null;
10
+ let lastMtimeMs = -1;
11
+ try {
12
+ lastMtimeMs = fs.statSync(absPath).mtimeMs;
13
+ }
14
+ catch {
15
+ // 監視開始時点でファイルが無くても、作成され次第のイベントで拾う
16
+ }
17
+ return fs.watch(dir, (_eventType, filename) => {
18
+ // filename can be null on some platforms; treat it as a potential match
19
+ if (filename !== null && filename !== base)
20
+ return;
21
+ // 同一ディレクトリの別ファイル変更 (filename が null の環境) や
22
+ // 自前の出力ファイル書き込みで発火しないよう、mtime の実変化を確認する
23
+ let mtimeMs;
24
+ try {
25
+ mtimeMs = fs.statSync(absPath).mtimeMs;
26
+ }
27
+ catch {
28
+ return;
29
+ }
30
+ if (mtimeMs === lastMtimeMs)
31
+ return;
32
+ lastMtimeMs = mtimeMs;
33
+ if (debounceTimer)
34
+ clearTimeout(debounceTimer);
35
+ debounceTimer = setTimeout(onChange, DEBOUNCE_MS);
36
+ });
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hirokisakabe/pom-cli",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "CLI tool for pom — preview and build presentations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,14 +30,14 @@
30
30
  "node": ">=18"
31
31
  },
32
32
  "dependencies": {
33
- "@hirokisakabe/pom": "^8.2.1",
33
+ "@hirokisakabe/pom": "^8.3.0",
34
34
  "@hirokisakabe/pom-md": "^3.0.0",
35
35
  "commander": "^15.0.0",
36
- "pptx-glimpse": "^0.11.2"
36
+ "pptx-glimpse": "^1.0.1"
37
37
  },
38
38
  "devDependencies": {
39
- "@types/node": "^25.9.1",
40
- "typescript-eslint": "^8.59.4"
39
+ "@types/node": "^25.9.2",
40
+ "typescript-eslint": "^8.60.1"
41
41
  },
42
42
  "scripts": {
43
43
  "build": "tsc && node -e \"require('fs').chmodSync('dist/cli.js', '755')\"",