takt-marp 0.2.1 → 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.ja.md CHANGED
@@ -34,21 +34,21 @@ Output Requirementsの例:
34
34
  ### 2. workflowを実行する
35
35
 
36
36
  ```bash
37
- npm run slide:plan -- "slides/<deck>"
38
- npm run slide:approve -- "slides/<deck>" plan --by <name>
39
- npm run slide:compose -- "slides/<deck>"
40
- npm run slide:approve -- "slides/<deck>" compose --by <name>
41
- npm run slide:polish -- "slides/<deck>"
42
- npm run slide:deliver -- "slides/<deck>"
37
+ takt-marp plan "slides/<deck>"
38
+ takt-marp approve "slides/<deck>" plan --by <name>
39
+ takt-marp compose "slides/<deck>"
40
+ takt-marp approve "slides/<deck>" compose --by <name>
41
+ takt-marp polish "slides/<deck>"
42
+ takt-marp deliver "slides/<deck>"
43
43
  ```
44
44
 
45
45
  targetは `slides/<deck>` を指定します。
46
46
 
47
47
  ```bash
48
- npm run slide:plan -- "slides/<deck>"
48
+ takt-marp plan "slides/<deck>"
49
49
  ```
50
50
 
51
- 人間承認は `plan` と `compose` に対してのみ `slide:approve` で記録します。`review`、`revise`、`qa`、`build-qa` はworkflow内部の責務であり、トップレベルコマンドではありません。
51
+ 人間承認は `plan` と `compose` に対してのみ `takt-marp approve` で記録します。`review`、`revise`、`qa`、`build-qa` はworkflow内部の責務であり、トップレベルコマンドではありません。
52
52
 
53
53
  ### 3. 生成されるファイル
54
54
 
@@ -75,24 +75,21 @@ slides/<deck>/
75
75
  - spatial balance: 上寄り、左寄り、大きな意図しない余白、視覚重心
76
76
  - design-system usage: token化されたCSS、スライドごとのstyle drift防止
77
77
 
78
- `deliver` は要求された成果物を生成します。PDF生成は対象deckの `SLIDES.md` だけをbuildします。
78
+ `deliver` は要求された成果物を生成し、delivery verification と supervision まで行います。
79
+ 単純なローカル生成や確認だけなら、workflow state を変更しない utility command を使います。
79
80
 
80
81
  ```bash
81
- npm run build:pdf -- <deck>
82
+ takt-marp build:html <deck>
83
+ takt-marp build:pdf <deck>
84
+ takt-marp preview <deck>
82
85
  ```
83
86
 
84
87
  ### 5. 検証
85
88
 
86
- 軽量なローカル確認には foundation validation を使います。
87
-
88
- ```bash
89
- npm test
90
- ```
91
-
92
89
  workflow routing、state gate、render evidence、delivery verification、approval handling を変更した場合は smoke validation を実行します。
93
90
 
94
91
  ```bash
95
- npm run slide:smoke -- --keep
92
+ takt-marp smoke --keep
96
93
  ```
97
94
 
98
95
  smoke validation は fixture から一時的な `_workflow-smoke` deck を作成し、invalid target、approval failure path、`plan` -> `compose` -> `polish` -> `deliver` の一連の実行、render evidence metadata、delivery artifact、rerun/force behavior を検証します。`--keep` を付けると、生成された deck と report を `slides/_workflow-smoke/` に残して確認できます。
package/README.md CHANGED
@@ -34,21 +34,21 @@ Example output requirement:
34
34
  ### 2. Run the workflows
35
35
 
36
36
  ```bash
37
- npm run slide:plan -- "slides/<deck>"
38
- npm run slide:approve -- "slides/<deck>" plan --by <name>
39
- npm run slide:compose -- "slides/<deck>"
40
- npm run slide:approve -- "slides/<deck>" compose --by <name>
41
- npm run slide:polish -- "slides/<deck>"
42
- npm run slide:deliver -- "slides/<deck>"
37
+ takt-marp plan "slides/<deck>"
38
+ takt-marp approve "slides/<deck>" plan --by <name>
39
+ takt-marp compose "slides/<deck>"
40
+ takt-marp approve "slides/<deck>" compose --by <name>
41
+ takt-marp polish "slides/<deck>"
42
+ takt-marp deliver "slides/<deck>"
43
43
  ```
44
44
 
45
45
  Use `slides/<deck>` as the target:
46
46
 
47
47
  ```bash
48
- npm run slide:plan -- "slides/<deck>"
48
+ takt-marp plan "slides/<deck>"
49
49
  ```
50
50
 
51
- Human approval is recorded by `slide:approve` for `plan` and `compose` only. `review`, `revise`, `qa`, and `build-qa` are internal workflow responsibilities, not top-level commands.
51
+ Human approval is recorded by `takt-marp approve` for `plan` and `compose` only. `review`, `revise`, `qa`, and `build-qa` are internal workflow responsibilities, not top-level commands.
52
52
 
53
53
  ### 3. Generated files
54
54
 
@@ -75,24 +75,21 @@ slides/<deck>/
75
75
  - spatial balance: top/left bias, large unintended blank areas, visual center of gravity
76
76
  - design-system usage: tokenized CSS, no per-slide style drift
77
77
 
78
- `deliver` is responsible for requested artifacts. PDF generation builds only the target deck's `SLIDES.md`:
78
+ `deliver` is responsible for requested artifacts, delivery verification, and final supervision.
79
+ For simple local generation or inspection, use utility commands that do not change workflow state:
79
80
 
80
81
  ```bash
81
- npm run build:pdf -- <deck>
82
+ takt-marp build:html <deck>
83
+ takt-marp build:pdf <deck>
84
+ takt-marp preview <deck>
82
85
  ```
83
86
 
84
87
  ### 5. Validation
85
88
 
86
- Use the foundation validation for fast local checks:
87
-
88
- ```bash
89
- npm test
90
- ```
91
-
92
89
  Use the smoke validation when changing workflow routing, state gates, render evidence, delivery verification, or approval handling:
93
90
 
94
91
  ```bash
95
- npm run slide:smoke -- --keep
92
+ takt-marp smoke --keep
96
93
  ```
97
94
 
98
95
  The smoke validation creates a temporary `_workflow-smoke` deck from the fixture, exercises invalid target and approval failure paths, runs the `plan` -> `compose` -> `polish` -> `deliver` sequence, verifies render evidence metadata, checks delivery artifacts, and covers rerun/force behavior. `--keep` leaves the generated deck and reports under `slides/_workflow-smoke/` for inspection.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "takt-marp",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "takt-marp",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,10 +31,10 @@
31
31
  "slide:smoke": "node scripts/takt-marp-validate-slide-workflow-smoke.mjs",
32
32
  "test": "npm run slide:validate-foundation",
33
33
  "build": "npm run build:html && npm run build:pdf",
34
- "build:html": "marp slides/takt-sdd/SLIDES.md --html --allow-local-files -o dist/takt-sdd/SLIDES.html",
35
- "build:pptx": "marp slides/takt-sdd/SLIDES.md --html --allow-local-files --pptx -o dist/takt-sdd/SLIDES.pptx",
36
- "build:pdf": "marp slides/takt-sdd/SLIDES.md --html --allow-local-files --pdf -o dist/takt-sdd/SLIDES.pdf",
37
- "preview": "marp -s . --html",
34
+ "build:html": "node scripts/takt-marp-build-slide-artifact.mjs html",
35
+ "build:pptx": "node scripts/takt-marp-build-slide-artifact.mjs pptx",
36
+ "build:pdf": "node scripts/takt-marp-build-slide-artifact.mjs pdf",
37
+ "preview": "node scripts/takt-marp-preview-slide.mjs takt-sdd",
38
38
  "installer:sync-templates": "node scripts/takt-marp-sync-project-templates.mjs --write",
39
39
  "installer:check-templates": "node scripts/takt-marp-sync-project-templates.mjs",
40
40
  "installer:check-package": "node scripts/takt-marp-validate-package-boundary.mjs",
@@ -13,11 +13,20 @@ import {
13
13
  } from "./takt-marp-slide-workflow.mjs";
14
14
 
15
15
  const WORKFLOW_COMMANDS = ["plan", "compose", "polish", "deliver"];
16
- const VALID_COMMANDS = ["init", ...WORKFLOW_COMMANDS, "approve", "smoke"];
16
+ const BUILD_COMMANDS = Object.freeze({
17
+ "build:html": "html",
18
+ "build:pdf": "pdf",
19
+ "build:pptx": "pptx",
20
+ });
21
+ const UTILITY_COMMANDS = [...Object.keys(BUILD_COMMANDS), "preview"];
22
+ const VALID_COMMANDS = ["init", ...WORKFLOW_COMMANDS, ...UTILITY_COMMANDS, "approve", "smoke"];
17
23
  const RUNNER_SCRIPT = "scripts/takt-marp-run-slide-workflow.mjs";
18
24
  const APPROVE_SCRIPT = "scripts/takt-marp-approve-slide-workflow-state.mjs";
19
25
  const SMOKE_SCRIPT = "scripts/takt-marp-validate-slide-workflow-smoke.mjs";
26
+ const BUILD_SCRIPT = "scripts/takt-marp-build-slide-artifact.mjs";
27
+ const PREVIEW_SCRIPT = "scripts/takt-marp-preview-slide.mjs";
20
28
  const REQUIRED_PROJECT_DIRS = [".takt/workflows", ".takt/facets"];
29
+ const FORWARDED_SIGNALS = new Set(["SIGINT", "SIGTERM"]);
21
30
 
22
31
  function usage() {
23
32
  return [
@@ -29,6 +38,14 @@ function usage() {
29
38
  " compose <slides/deck> [options] Run the compose workflow for a deck in the current project",
30
39
  " polish <slides/deck> [options] Run the polish workflow for a deck in the current project",
31
40
  " deliver <slides/deck> [options] Run the deliver workflow for a deck in the current project",
41
+ " build:html [deck|slides/<deck>|slides/<deck>/SLIDES.md]",
42
+ " Build HTML artifact without changing workflow state",
43
+ " build:pdf [deck|slides/<deck>|slides/<deck>/SLIDES.md]",
44
+ " Build PDF artifact without changing workflow state",
45
+ " build:pptx [deck|slides/<deck>|slides/<deck>/SLIDES.md]",
46
+ " Build PPTX artifact without changing workflow state",
47
+ " preview <deck|slides/<deck>|slides/<deck>/SLIDES.md>",
48
+ " Start Marp server mode without changing workflow state",
32
49
  " approve <slides/deck> <command> --by <name> [--force]",
33
50
  " Approve a workflow state (command: plan or compose)",
34
51
  " smoke [--provider <name>] Run smoke validation in a temporary project (default provider: mock)",
@@ -72,12 +89,39 @@ function assertProjectInitialized() {
72
89
 
73
90
  function runPackageScript(relativeScriptPath, args, options = {}) {
74
91
  return new Promise((resolve, reject) => {
92
+ let stopping = false;
75
93
  const child = spawn(process.execPath, [packageScriptPath(relativeScriptPath), ...args], {
76
94
  cwd: options.cwd ?? process.cwd(),
77
95
  stdio: "inherit",
78
96
  });
79
- child.on("error", reject);
80
- child.on("close", (code) => {
97
+ const signalHandlers = [];
98
+ if (options.successOnSignal) {
99
+ for (const signal of FORWARDED_SIGNALS) {
100
+ const handler = () => {
101
+ stopping = true;
102
+ if (!child.killed) {
103
+ child.kill(signal);
104
+ }
105
+ };
106
+ process.once(signal, handler);
107
+ signalHandlers.push([signal, handler]);
108
+ }
109
+ }
110
+ const cleanup = () => {
111
+ for (const [signal, handler] of signalHandlers) {
112
+ process.off(signal, handler);
113
+ }
114
+ };
115
+ child.on("error", (error) => {
116
+ cleanup();
117
+ reject(error);
118
+ });
119
+ child.on("close", (code, signal) => {
120
+ cleanup();
121
+ if (options.successOnSignal && (stopping || FORWARDED_SIGNALS.has(signal))) {
122
+ resolve(0);
123
+ return;
124
+ }
81
125
  resolve(code ?? 1);
82
126
  });
83
127
  });
@@ -88,6 +132,14 @@ async function runWorkflowCommand(command, args) {
88
132
  return runPackageScript(RUNNER_SCRIPT, [command, ...args]);
89
133
  }
90
134
 
135
+ async function runBuildCommand(command, args) {
136
+ return runPackageScript(BUILD_SCRIPT, [BUILD_COMMANDS[command], ...args]);
137
+ }
138
+
139
+ async function runPreview(args) {
140
+ return runPackageScript(PREVIEW_SCRIPT, args, { successOnSignal: true });
141
+ }
142
+
91
143
  async function runInit(args) {
92
144
  let parsed;
93
145
  try {
@@ -257,6 +309,12 @@ export async function runCli(argv) {
257
309
  if (command === "smoke") {
258
310
  return await runSmoke(rest);
259
311
  }
312
+ if (Object.hasOwn(BUILD_COMMANDS, command)) {
313
+ return await runBuildCommand(command, rest);
314
+ }
315
+ if (command === "preview") {
316
+ return await runPreview(rest);
317
+ }
260
318
  return await runWorkflowCommand(command, rest);
261
319
  } catch (error) {
262
320
  console.error(formatError(error));
@@ -0,0 +1,70 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { SlideWorkflowError } from "./takt-marp-slide-workflow.mjs";
5
+
6
+ export async function resolveSlideArtifactTargets(target, options = {}) {
7
+ if (!target) {
8
+ return listSlideArtifactTargets(options);
9
+ }
10
+ return [resolveSlideArtifactTarget(target, options)];
11
+ }
12
+
13
+ export async function listSlideArtifactTargets(options = {}) {
14
+ const root = options.root ?? process.cwd();
15
+ const slidesRoot = path.join(root, "slides");
16
+ const entries = await readdir(slidesRoot, { withFileTypes: true });
17
+ const targets = entries
18
+ .filter((entry) => entry.isDirectory())
19
+ .filter((entry) => existsSync(path.join(slidesRoot, entry.name, "SLIDES.md")))
20
+ .map((entry) => resolveSlideArtifactTarget(entry.name, options))
21
+ .sort((left, right) => left.deckName.localeCompare(right.deckName));
22
+
23
+ if (targets.length === 0) {
24
+ throw new SlideWorkflowError("No slides/*/SLIDES.md files found.", "SLIDES_NOT_FOUND");
25
+ }
26
+ return targets;
27
+ }
28
+
29
+ export function resolveSlideArtifactTarget(target, options = {}) {
30
+ const root = options.root ?? process.cwd();
31
+ if (!target || path.isAbsolute(target)) {
32
+ throw new SlideWorkflowError(`Invalid deck target '${target}'.`, "INVALID_TARGET");
33
+ }
34
+
35
+ const normalized = path.posix.normalize(target.replaceAll(path.sep, "/"));
36
+ if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
37
+ throw new SlideWorkflowError(`Invalid deck target '${target}'.`, "INVALID_TARGET");
38
+ }
39
+
40
+ const parts = normalized.split("/");
41
+ const deckName = deckNameFromParts(parts, target);
42
+ const deckPath = path.join(root, "slides", deckName);
43
+ const slidesPath = path.join(deckPath, "SLIDES.md");
44
+ if (!existsSync(slidesPath)) {
45
+ throw new SlideWorkflowError(`SLIDES.md not found: slides/${deckName}/SLIDES.md`, "SLIDES_NOT_FOUND");
46
+ }
47
+
48
+ return Object.freeze({
49
+ deckName,
50
+ deckPath,
51
+ slidesPath,
52
+ distPath: path.join(root, "dist", deckName),
53
+ });
54
+ }
55
+
56
+ function deckNameFromParts(parts, original) {
57
+ if (parts.length === 1 && parts[0] && !parts[0].endsWith(".md")) {
58
+ return parts[0];
59
+ }
60
+ if (parts.length === 2 && parts[0] === "slides" && parts[1] && !parts[1].endsWith(".md")) {
61
+ return parts[1];
62
+ }
63
+ if (parts.length === 3 && parts[0] === "slides" && parts[1] && parts[2] === "SLIDES.md") {
64
+ return parts[1];
65
+ }
66
+ throw new SlideWorkflowError(
67
+ `Invalid deck target '${original}'. Expected deck, slides/<deck>, or slides/<deck>/SLIDES.md.`,
68
+ "INVALID_TARGET",
69
+ );
70
+ }
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
- import { existsSync } from "node:fs";
4
- import { mkdir, readdir } from "node:fs/promises";
3
+ import { mkdir } from "node:fs/promises";
5
4
  import path from "node:path";
6
5
  import {
7
6
  formatError,
8
7
  parseArgs,
9
8
  SlideWorkflowError,
10
9
  } from "./lib/takt-marp-slide-workflow.mjs";
10
+ import { resolveSlideArtifactTargets } from "./lib/takt-marp-slide-artifact-target.mjs";
11
11
  import { runtimeExecutablePath } from "./lib/takt-marp-runtime-context.mjs";
12
12
 
13
13
  const ARTIFACT_OPTIONS = Object.freeze({
@@ -23,9 +23,9 @@ function usage() {
23
23
  "When deck is omitted, all slides/*/SLIDES.md files are built.",
24
24
  "",
25
25
  "Examples:",
26
- " npm run build:pdf -- my-talk",
27
- " npm run build:html -- slides/my-talk",
28
- " npm run build:pptx -- slides/my-talk/SLIDES.md",
26
+ " takt-marp build:pdf my-talk",
27
+ " takt-marp build:html slides/my-talk",
28
+ " takt-marp build:pptx slides/my-talk/SLIDES.md",
29
29
  ].join("\n");
30
30
  }
31
31
 
@@ -37,79 +37,19 @@ async function main() {
37
37
  }
38
38
 
39
39
  const [artifact, target] = positional;
40
+ if (positional.length > 2) {
41
+ throw new SlideWorkflowError(usage(), "INVALID_ARGS");
42
+ }
40
43
  if (!ARTIFACT_OPTIONS[artifact]) {
41
44
  throw new SlideWorkflowError(usage(), "INVALID_ARGS");
42
45
  }
43
46
 
44
- const targets = await resolveBuildTargets(target);
47
+ const targets = await resolveSlideArtifactTargets(target);
45
48
  for (const item of targets) {
46
49
  await buildArtifact(artifact, item);
47
50
  }
48
51
  }
49
52
 
50
- async function resolveBuildTargets(target) {
51
- if (!target) {
52
- return listDecksWithSlides();
53
- }
54
- return [resolveBuildTarget(target)];
55
- }
56
-
57
- async function listDecksWithSlides() {
58
- const slidesRoot = path.join(process.cwd(), "slides");
59
- const entries = await readdir(slidesRoot, { withFileTypes: true });
60
- const targets = entries
61
- .filter((entry) => entry.isDirectory())
62
- .filter((entry) => existsSync(path.join(slidesRoot, entry.name, "SLIDES.md")))
63
- .map((entry) => resolveBuildTarget(entry.name))
64
- .sort((left, right) => left.deckName.localeCompare(right.deckName));
65
-
66
- if (targets.length === 0) {
67
- throw new SlideWorkflowError("No slides/*/SLIDES.md files found.", "SLIDES_NOT_FOUND");
68
- }
69
- return targets;
70
- }
71
-
72
- function resolveBuildTarget(target) {
73
- if (!target || path.isAbsolute(target)) {
74
- throw new SlideWorkflowError(`Invalid deck target '${target}'.`, "INVALID_TARGET");
75
- }
76
-
77
- const normalized = path.posix.normalize(target.replaceAll(path.sep, "/"));
78
- if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
79
- throw new SlideWorkflowError(`Invalid deck target '${target}'.`, "INVALID_TARGET");
80
- }
81
-
82
- const parts = normalized.split("/");
83
- const deckName = deckNameFromParts(parts, target);
84
- const deckPath = path.join(process.cwd(), "slides", deckName);
85
- const slidesPath = path.join(deckPath, "SLIDES.md");
86
- if (!existsSync(slidesPath)) {
87
- throw new SlideWorkflowError(`SLIDES.md not found: slides/${deckName}/SLIDES.md`, "SLIDES_NOT_FOUND");
88
- }
89
-
90
- return Object.freeze({
91
- deckName,
92
- slidesPath,
93
- distPath: path.join(process.cwd(), "dist", deckName),
94
- });
95
- }
96
-
97
- function deckNameFromParts(parts, original) {
98
- if (parts.length === 1 && parts[0] && !parts[0].endsWith(".md")) {
99
- return parts[0];
100
- }
101
- if (parts.length === 2 && parts[0] === "slides" && parts[1] && !parts[1].endsWith(".md")) {
102
- return parts[1];
103
- }
104
- if (parts.length === 3 && parts[0] === "slides" && parts[1] && parts[2] === "SLIDES.md") {
105
- return parts[1];
106
- }
107
- throw new SlideWorkflowError(
108
- `Invalid deck target '${original}'. Expected deck, slides/<deck>, or slides/<deck>/SLIDES.md.`,
109
- "INVALID_TARGET",
110
- );
111
- }
112
-
113
53
  async function buildArtifact(artifact, target) {
114
54
  await mkdir(target.distPath, { recursive: true });
115
55
  const outputPath = path.join(target.distPath, `SLIDES.${artifact}`);
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import {
4
+ formatError,
5
+ parseArgs,
6
+ SlideWorkflowError,
7
+ } from "./lib/takt-marp-slide-workflow.mjs";
8
+ import { resolveSlideArtifactTarget } from "./lib/takt-marp-slide-artifact-target.mjs";
9
+ import { runtimeExecutablePath } from "./lib/takt-marp-runtime-context.mjs";
10
+
11
+ const PREVIEW_STOP_SIGNALS = new Set(["SIGINT", "SIGTERM"]);
12
+
13
+ function usage() {
14
+ return [
15
+ "Usage: takt-marp preview <deck|slides/<deck>|slides/<deck>/SLIDES.md>",
16
+ "",
17
+ "Starts Marp server mode for the target deck directory without running a TAKT workflow.",
18
+ "",
19
+ "Examples:",
20
+ " takt-marp preview my-talk",
21
+ " takt-marp preview slides/my-talk",
22
+ " takt-marp preview slides/my-talk/SLIDES.md",
23
+ ].join("\n");
24
+ }
25
+
26
+ async function main() {
27
+ const { positional, flags } = parseArgs(process.argv.slice(2));
28
+ if (flags.help) {
29
+ console.log(usage());
30
+ return;
31
+ }
32
+ if (positional.length !== 1) {
33
+ throw new SlideWorkflowError(usage(), "INVALID_ARGS");
34
+ }
35
+
36
+ const target = resolveSlideArtifactTarget(positional[0]);
37
+ const marpPath = runtimeExecutablePath("marp");
38
+ const result = await run(marpPath, [
39
+ target.deckPath,
40
+ "--server",
41
+ "--html",
42
+ "--allow-local-files",
43
+ ]);
44
+ if (!result.ok) {
45
+ throw new SlideWorkflowError(`Marp preview failed for slides/${target.deckName}/SLIDES.md`, "MARP_PREVIEW_FAILED");
46
+ }
47
+ }
48
+
49
+ function run(command, args) {
50
+ return new Promise((resolve, reject) => {
51
+ let stopping = false;
52
+ const child = spawn(command, args, {
53
+ shell: process.platform === "win32",
54
+ stdio: "inherit",
55
+ });
56
+ const signalHandlers = [];
57
+ for (const signal of PREVIEW_STOP_SIGNALS) {
58
+ const handler = () => {
59
+ stopping = true;
60
+ if (!child.killed) {
61
+ child.kill(signal);
62
+ }
63
+ };
64
+ process.once(signal, handler);
65
+ signalHandlers.push([signal, handler]);
66
+ }
67
+ const cleanup = () => {
68
+ for (const [signal, handler] of signalHandlers) {
69
+ process.off(signal, handler);
70
+ }
71
+ };
72
+ child.on("error", (error) => {
73
+ cleanup();
74
+ reject(
75
+ new SlideWorkflowError(
76
+ `Failed to start Marp executable: ${command}. Reinstall takt-marp and verify the @marp-team/marp-cli dependency. ${error.message}`,
77
+ "MARP_EXECUTABLE_MISSING",
78
+ ),
79
+ );
80
+ });
81
+ child.on("close", (code, signal) => {
82
+ cleanup();
83
+ resolve({ ok: stopping || PREVIEW_STOP_SIGNALS.has(signal) || code === 0 });
84
+ });
85
+ });
86
+ }
87
+
88
+ main().catch((error) => {
89
+ console.error(formatError(error));
90
+ process.exit(1);
91
+ });
@@ -23,7 +23,19 @@ import { listTemplateEntries } from "./lib/takt-marp-project-templates.mjs";
23
23
  import { resolveRuntimeContext } from "./lib/takt-marp-runtime-context.mjs";
24
24
  import { SlideWorkflowError, formatError } from "./lib/takt-marp-slide-workflow.mjs";
25
25
 
26
- const CLI_COMMANDS = ["init", "plan", "compose", "polish", "deliver", "smoke"];
26
+ const CLI_COMMANDS = [
27
+ "init",
28
+ "plan",
29
+ "compose",
30
+ "polish",
31
+ "deliver",
32
+ "build:html",
33
+ "build:pdf",
34
+ "build:pptx",
35
+ "preview",
36
+ "approve",
37
+ "smoke",
38
+ ];
27
39
  // Runtime state / provider configuration that init must never generate (8.2).
28
40
  const RUNTIME_STATE_NAMES = [
29
41
  "config.yaml",
@@ -148,6 +160,12 @@ async function assertFileContent(filePath, expectedContent, label) {
148
160
  check(actual === expectedContent, `${label}: file content changed: ${filePath}`);
149
161
  }
150
162
 
163
+ async function assertNonEmptyFile(filePath, label) {
164
+ check(existsSync(filePath), `${label}: file is missing: ${filePath}`);
165
+ const actual = await readFile(filePath);
166
+ check(actual.length > 0, `${label}: file is empty: ${filePath}`);
167
+ }
168
+
151
169
  // Phase 1 (8.1): real tarball -> temp global prefix; later phases run the CLI
152
170
  // from that prefix via PATH.
153
171
  async function phasePackInstall(ctx) {
@@ -172,7 +190,7 @@ async function phasePackInstall(ctx) {
172
190
  return `tarball ${tarballs[0]} installed into temp prefix`;
173
191
  }
174
192
 
175
- // Phase 2 (1.1, 1.2, 1.3): help lists all six commands; slide:* is rejected.
193
+ // Phase 2 (1.1, 1.2, 1.3): help lists all public commands; slide:* is rejected.
176
194
  async function phaseSurface(ctx) {
177
195
  const help = await runTaktMarp(ctx, ["--help"], { cwd: ctx.workDir });
178
196
  check(help.code === 0, `takt-marp --help must exit 0.\n${commandSummary(help)}`);
@@ -189,7 +207,52 @@ async function phaseSurface(ctx) {
189
207
  unknown.output.includes("UNKNOWN_COMMAND"),
190
208
  `takt-marp slide:plan must be rejected with UNKNOWN_COMMAND.\n${commandSummary(unknown)}`,
191
209
  );
192
- return "help lists 6 commands; slide:* rejected with UNKNOWN_COMMAND";
210
+ return "help lists public commands; slide:* rejected with UNKNOWN_COMMAND";
211
+ }
212
+
213
+ // Phase 2b: utility commands must work from the installed CLI without requiring
214
+ // the target project to own package.json, node_modules, or .takt workflow state.
215
+ async function phaseUtilityCommands(ctx) {
216
+ const projectDir = path.join(ctx.workDir, "utility-project");
217
+ const deckDir = path.join(projectDir, "slides", "sample");
218
+ await mkdir(deckDir, { recursive: true });
219
+ await writeFile(
220
+ path.join(deckDir, "SLIDES.md"),
221
+ [
222
+ "---",
223
+ "marp: true",
224
+ "html: true",
225
+ "---",
226
+ "",
227
+ "# Utility Build",
228
+ "",
229
+ "<strong>HTML enabled</strong>",
230
+ "",
231
+ ].join("\n"),
232
+ "utf8",
233
+ );
234
+
235
+ check(!existsSync(path.join(projectDir, "package.json")), "precondition: utility project must not have package.json");
236
+ check(!existsSync(path.join(projectDir, "node_modules")), "precondition: utility project must not have node_modules");
237
+ check(!existsSync(path.join(projectDir, ".takt")), "precondition: utility project must not have .takt");
238
+
239
+ const html = await runTaktMarp(ctx, ["build:html", "slides/sample"], { cwd: projectDir });
240
+ check(html.code === 0, `takt-marp build:html must exit 0 in a non-npm project.\n${commandSummary(html)}`);
241
+ await assertNonEmptyFile(path.join(projectDir, "dist", "sample", "SLIDES.html"), "build:html output");
242
+
243
+ const pdf = await runTaktMarp(ctx, ["build:pdf", "sample"], { cwd: projectDir });
244
+ check(pdf.code === 0, `takt-marp build:pdf must exit 0 in a non-npm project.\n${commandSummary(pdf)}`);
245
+ await assertNonEmptyFile(path.join(projectDir, "dist", "sample", "SLIDES.pdf"), "build:pdf output");
246
+
247
+ const previewHelp = await runTaktMarp(ctx, ["preview", "--help"], { cwd: projectDir });
248
+ check(previewHelp.code === 0, `takt-marp preview --help must exit 0.\n${commandSummary(previewHelp)}`);
249
+ check(
250
+ previewHelp.stdout.includes("Usage: takt-marp preview"),
251
+ `preview help must show preview usage.\n${commandSummary(previewHelp)}`,
252
+ );
253
+
254
+ check(!existsSync(path.join(projectDir, ".takt")), "utility commands must not create .takt workflow state");
255
+ return "build utilities ran without project package.json/node_modules/.takt; preview help is available";
193
256
  }
194
257
 
195
258
  // Phase 3 (8.2): init generates exactly workflows/** + facets/** under .takt,
@@ -327,6 +390,7 @@ async function phaseMockSmoke(ctx) {
327
390
  const PHASES = [
328
391
  { name: "pack-install", run: phasePackInstall, deps: [] },
329
392
  { name: "surface", run: phaseSurface, deps: [] },
393
+ { name: "utility-commands", run: phaseUtilityCommands, deps: [] },
330
394
  { name: "init-boundary", run: phaseInitBoundary, deps: [] },
331
395
  { name: "conflict-force", run: phaseConflictForce, deps: ["init-boundary"] },
332
396
  { name: "workflow-command-modes", run: phaseWorkflowCommandModes, deps: ["init-boundary"] },