takt-marp 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.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/templates/project/facets/instructions/takt-marp-visual-generate.md +8 -6
- package/templates/project/facets/policies/takt-marp-svg-first-visual.md +4 -2
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"] },
|
|
@@ -9,17 +9,19 @@ planとSLIDES.mdに必要なSVG sourceを生成してください。
|
|
|
9
9
|
- inline SVG: `SLIDES.md` の該当スライドの図版領域(placeholder 部分)だけを編集し、SVG markup を直接記述します。front matter に `html: true` が未設定の場合は追加してください。
|
|
10
10
|
- `Visual: existing: ...`: 明示された既存画像への参照を `SLIDES.md` の該当スライドの placeholder 部分に挿入します。
|
|
11
11
|
3. SVGはSVGファーストVisualポリシーに従ってください(外部ファイル・inline 両形式に適用)。
|
|
12
|
-
4.
|
|
13
|
-
5.
|
|
14
|
-
6. SVG
|
|
15
|
-
7.
|
|
16
|
-
8.
|
|
17
|
-
9.
|
|
12
|
+
4. SVGキャンバスの約90%を図形・矢印・ラベルなどの意味のある要素で使い、余白は約10%以下に抑えてください。`viewBox 0 0 1100 540` では外周の安全域を左右20-30px、上下16-24px程度にし、6:4程度に余白が目立つ構図を作らないでください。
|
|
13
|
+
5. 図形内テキストが図形からはみ出していないか確認してください。長いファイル名や英字ラベルは `tspan` で分割し、左右20px以上の余白を残してください。
|
|
14
|
+
6. Marp配置時にスライド枠からSVGがはみ出しにくいよう、`SLIDES.md` の画像指定は個別サイズではなく `visual`、`visual-dense`、`visual-full` classのCSSで制御してください。inline SVGの場合も同様に、親classのcontainment(`--visual-max-height`系token)でサイズを制御してください。
|
|
15
|
+
7. SVGをもう少し大きくしたい場合は、個別スライドの `![h:...]` を増やすのではなく、class側の `--visual-max-height` を調整してください。render evidence による確認は polish command に委譲してください。
|
|
16
|
+
8. 外部ファイル形式の場合は `SLIDES.md`の参照パスが実在するSVGを指すようにしてください。
|
|
17
|
+
9. 既存画像は明示されたものだけ使ってください。
|
|
18
|
+
10. render output の生成や表示品質の最終判定は行わず、source artifact の作成に集中してください。
|
|
18
19
|
|
|
19
20
|
**必須出力**
|
|
20
21
|
## Visual Result
|
|
21
22
|
- Status: generated / needs_input
|
|
22
23
|
- SVG files: (外部ファイル形式・inline形式の両方を記載)
|
|
24
|
+
- Canvas usage checks:
|
|
23
25
|
- Text fit checks:
|
|
24
26
|
- Source reference checks:
|
|
25
27
|
- Files changed:
|
|
@@ -11,6 +11,7 @@ Marpスライド内の図解は、レビュー可能で修正しやすいSVGを
|
|
|
11
11
|
| SVG-first | 図解、比較、フロー、概念絵は原則SVGで作る |
|
|
12
12
|
| 別ファイル管理(既定) | SVGは原則`images/*.svg`として保存しMarpから参照する。スライド固有の図版は使い分け基準に従いinlineも可 |
|
|
13
13
|
| 1 SVG 1 message | 1枚のSVGに複数の主張を詰め込まない |
|
|
14
|
+
| 9:1 の面積配分 | SVGキャンバスは図版が約9割、余白が約1割になるように使う |
|
|
14
15
|
| 可読性優先 | 900-1080px幅で読める文字量とサイズにする |
|
|
15
16
|
| テキスト収まり優先 | 文字列は必ず図形内の余白に収め、長いラベルは折り返す |
|
|
16
17
|
| 差分レビュー可能 | 意味のある要素名、整った構造、過度なminifyを避ける |
|
|
@@ -20,7 +21,8 @@ Marpスライド内の図解は、レビュー可能で修正しやすいSVGを
|
|
|
20
21
|
- `viewBox` は原則 `0 0 1100 540`。
|
|
21
22
|
- font-family は `'Noto Sans JP','Hiragino Sans','Yu Gothic',sans-serif`(knowledge の日本語優先フォールバックスタックと同順)。
|
|
22
23
|
- 背景は白または透明。
|
|
23
|
-
-
|
|
24
|
+
- SVG全体の面積配分は「図版9:余白1」を目安にし、図形・矢印・ラベルなどの意味のある要素がキャンバスの約90%を使うように配置する。
|
|
25
|
+
- 外周余白は安全域として最小限にし、`viewBox 0 0 1100 540` では左右20-30px、上下16-24pxを目安にする。余白を広く取って図版が6:4程度に小さく見える構図は禁止する。
|
|
24
26
|
- 図形内テキストは左右20px以上、上下12px以上の内側余白を残す。
|
|
25
27
|
- ファイル名、URL、長い英字ラベルは1行で入れず、`tspan`で2行以上に分割する。
|
|
26
28
|
- 箱の幅が160px未満の場合、中央ラベルは短語だけにする。`plan.md` のような短い名前でも幅200px以上を優先する。
|
|
@@ -37,7 +39,7 @@ Marpスライド内の図解は、レビュー可能で修正しやすいSVGを
|
|
|
37
39
|
- 標準classは `visual`、本文が多い場合は `visual-dense`、図だけを大きく見せる場合は `visual-full` を使う。
|
|
38
40
|
- `visual` の目安は `--visual-max-height: 280px` から `300px`。`visual-dense` は `240px` から `260px`、`visual-full` は `360px` から `410px` を目安にする。
|
|
39
41
|
- `![w:900]` 以上の幅指定は、本文がほぼないスライドに限定する。
|
|
40
|
-
- SVG
|
|
42
|
+
- SVG内の重要要素は、端で切れない範囲で外周近くまで広げる。`viewBox 0 0 1100 540` では左右20-30px、上下16-24px程度の安全域を残し、意味のある図版領域を約90%まで拡大する。
|
|
41
43
|
- SVG単体の `viewBox` 内に収まっていても、Marpに配置した結果スライド枠からはみ出す場合は失敗として扱う。
|
|
42
44
|
|
|
43
45
|
## 外部SVGとinline SVGの使い分け基準
|