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 +14 -17
- package/README.md +14 -17
- package/package.json +5 -5
- package/scripts/lib/takt-marp-cli.mjs +61 -3
- package/scripts/lib/takt-marp-slide-artifact-target.mjs +70 -0
- package/scripts/takt-marp-build-slide-artifact.mjs +9 -69
- package/scripts/takt-marp-preview-slide.mjs +91 -0
- package/scripts/takt-marp-validate-global-install.mjs +67 -3
package/README.ja.md
CHANGED
|
@@ -34,21 +34,21 @@ Output Requirementsの例:
|
|
|
34
34
|
### 2. workflowを実行する
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
48
|
+
takt-marp plan "slides/<deck>"
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
人間承認は `plan` と `compose` に対してのみ `
|
|
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`
|
|
78
|
+
`deliver` は要求された成果物を生成し、delivery verification と supervision まで行います。
|
|
79
|
+
単純なローカル生成や確認だけなら、workflow state を変更しない utility command を使います。
|
|
79
80
|
|
|
80
81
|
```bash
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
48
|
+
takt-marp plan "slides/<deck>"
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
Human approval is recorded by `
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
35
|
-
"build:pptx": "
|
|
36
|
-
"build:pdf": "
|
|
37
|
-
"preview": "
|
|
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
|
|
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
|
-
|
|
80
|
-
|
|
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 {
|
|
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
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
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
|
|
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 = [
|
|
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
|
|
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
|
|
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"] },
|