takt-marp 0.1.0 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "takt-marp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "takt-marp",
5
5
  "repository": {
6
6
  "type": "git",
@@ -6,11 +6,16 @@ import path from "node:path";
6
6
  import { parseArgs } from "node:util";
7
7
  import { initializeProject } from "./takt-marp-project-init.mjs";
8
8
  import { packageScriptPath } from "./takt-marp-runtime-context.mjs";
9
- import { formatError, SlideWorkflowError } from "./takt-marp-slide-workflow.mjs";
9
+ import {
10
+ APPROVAL_COMMANDS,
11
+ formatError,
12
+ SlideWorkflowError,
13
+ } from "./takt-marp-slide-workflow.mjs";
10
14
 
11
15
  const WORKFLOW_COMMANDS = ["plan", "compose", "polish", "deliver"];
12
- const VALID_COMMANDS = ["init", ...WORKFLOW_COMMANDS, "smoke"];
16
+ const VALID_COMMANDS = ["init", ...WORKFLOW_COMMANDS, "approve", "smoke"];
13
17
  const RUNNER_SCRIPT = "scripts/takt-marp-run-slide-workflow.mjs";
18
+ const APPROVE_SCRIPT = "scripts/takt-marp-approve-slide-workflow-state.mjs";
14
19
  const SMOKE_SCRIPT = "scripts/takt-marp-validate-slide-workflow-smoke.mjs";
15
20
  const REQUIRED_PROJECT_DIRS = [".takt/workflows", ".takt/facets"];
16
21
 
@@ -24,6 +29,8 @@ function usage() {
24
29
  " compose <slides/deck> [options] Run the compose workflow for a deck in the current project",
25
30
  " polish <slides/deck> [options] Run the polish workflow for a deck in the current project",
26
31
  " deliver <slides/deck> [options] Run the deliver workflow for a deck in the current project",
32
+ " approve <slides/deck> <command> --by <name> [--force]",
33
+ " Approve a workflow state (command: plan or compose)",
27
34
  " smoke [--provider <name>] Run smoke validation in a temporary project (default provider: mock)",
28
35
  "",
29
36
  "Workflow options (passed through to the workflow runner unchanged):",
@@ -171,6 +178,62 @@ async function runSmoke(args) {
171
178
  return exitCode;
172
179
  }
173
180
 
181
+ function approveUsage() {
182
+ return [
183
+ "Usage: takt-marp approve <slides/deck> <command> --by <name> [--force]",
184
+ "",
185
+ `Commands: ${APPROVAL_COMMANDS.join(", ")}`,
186
+ "",
187
+ "Options:",
188
+ " --by <name> Approver identifier (required)",
189
+ " --force Overwrite an existing approval",
190
+ " --help, -h Show this message",
191
+ ].join("\n");
192
+ }
193
+
194
+ async function runApprove(args) {
195
+ let parsed;
196
+ try {
197
+ parsed = parseArgs({
198
+ args,
199
+ allowPositionals: true,
200
+ options: {
201
+ by: { type: "string" },
202
+ force: { type: "boolean", default: false },
203
+ help: { type: "boolean", short: "h", default: false },
204
+ },
205
+ });
206
+ } catch (error) {
207
+ throw new SlideWorkflowError(
208
+ `Invalid approve arguments: ${error.message}. Run 'takt-marp approve --help' for usage.`,
209
+ "INVALID_ARGS",
210
+ );
211
+ }
212
+ if (parsed.values.help) {
213
+ console.log(approveUsage());
214
+ return 0;
215
+ }
216
+ assertProjectInitialized();
217
+ if (parsed.positionals.length !== 2) {
218
+ throw new SlideWorkflowError(
219
+ `Expected <slides/deck> and <command>. Run 'takt-marp approve --help' for usage.`,
220
+ "INVALID_ARGS",
221
+ );
222
+ }
223
+ const [target, command] = parsed.positionals;
224
+ if (!parsed.values.by) {
225
+ throw new SlideWorkflowError(
226
+ `Missing required --by <name>. Run 'takt-marp approve --help' for usage.`,
227
+ "INVALID_ARGS",
228
+ );
229
+ }
230
+ const approveArgs = [target, command, "--by", parsed.values.by];
231
+ if (parsed.values.force) {
232
+ approveArgs.push("--force");
233
+ }
234
+ return runPackageScript(APPROVE_SCRIPT, approveArgs);
235
+ }
236
+
174
237
  export async function runCli(argv) {
175
238
  const [command, ...rest] = argv;
176
239
  if (command === undefined || command === "--help" || command === "-h" || command === "help") {
@@ -188,6 +251,9 @@ export async function runCli(argv) {
188
251
  if (command === "init") {
189
252
  return await runInit(rest);
190
253
  }
254
+ if (command === "approve") {
255
+ return await runApprove(rest);
256
+ }
191
257
  if (command === "smoke") {
192
258
  return await runSmoke(rest);
193
259
  }
@@ -20,6 +20,7 @@ import {
20
20
  supervisionPath,
21
21
  writeApproval,
22
22
  } from "./lib/takt-marp-slide-workflow.mjs";
23
+ import { runCli } from "./lib/takt-marp-cli.mjs";
23
24
 
24
25
  const checks = [];
25
26
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
@@ -306,6 +307,47 @@ async function main() {
306
307
  assert(verifyResult.stdout.includes("skipped for non-successful verify report"), `verify verifier did not report skip: ${verifyResult.stdout}`);
307
308
  });
308
309
 
310
+ await check("approve command shows help without initialized project", async () => {
311
+ const root = await fixtureRoot();
312
+ const originalCwd = process.cwd();
313
+ let output;
314
+ try {
315
+ process.chdir(root);
316
+ output = await captureStdout(() => runCli(["approve", "--help"]));
317
+ } finally {
318
+ process.chdir(originalCwd);
319
+ }
320
+ assert(output.includes("Usage: takt-marp approve"), `approve help missing usage: ${output}`);
321
+ });
322
+
323
+ await check("approve command requires initialized project", async () => {
324
+ const root = await fixtureRoot();
325
+ const originalCwd = process.cwd();
326
+ try {
327
+ process.chdir(root);
328
+ const code = await runCli(["approve", "slides/demo", "plan", "--by", "foundation-test"]);
329
+ assert(code !== 0, "approve unexpectedly succeeded in uninitialized project");
330
+ } finally {
331
+ process.chdir(originalCwd);
332
+ }
333
+ });
334
+
335
+ await check("approve command writes approval file", async () => {
336
+ const root = await initializedFixtureRoot();
337
+ const targetInfo = await makeDeck(root, "demo");
338
+ await writeSupervision(targetInfo, "plan", "planned", "passed", "run-plan-1");
339
+ const originalCwd = process.cwd();
340
+ try {
341
+ process.chdir(root);
342
+ const code = await runCli(["approve", targetInfo.target, "plan", "--by", "foundation-test"]);
343
+ assert(code === 0, `approve command failed: check stderr`);
344
+ } finally {
345
+ process.chdir(originalCwd);
346
+ }
347
+ const approval = await readFile(path.join(targetInfo.reviewPath, "plan-approval.md"), "utf8");
348
+ assert(approval.includes("approved_by: foundation-test"), `approval file missing approver: ${approval}`);
349
+ });
350
+
309
351
  await check("package scripts expose canonical entrypoints only", async () => {
310
352
  const pkg = JSON.parse(await readFile(path.join(process.cwd(), "package.json"), "utf8"));
311
353
  const scripts = pkg.scripts ?? {};
@@ -348,6 +390,32 @@ async function fixtureRoot() {
348
390
  return root;
349
391
  }
350
392
 
393
+ async function initializedFixtureRoot() {
394
+ const root = await fixtureRoot();
395
+ await mkdir(path.join(root, ".takt", "facets"), { recursive: true });
396
+ return root;
397
+ }
398
+
399
+ async function captureStdout(fn) {
400
+ const originalWrite = process.stdout.write.bind(process.stdout);
401
+ const chunks = [];
402
+ process.stdout.write = (chunk, encoding, callback) => {
403
+ chunks.push(typeof chunk === "string" ? chunk : chunk.toString());
404
+ if (typeof encoding === "function") {
405
+ encoding();
406
+ } else if (typeof callback === "function") {
407
+ callback();
408
+ }
409
+ return true;
410
+ };
411
+ try {
412
+ await fn();
413
+ } finally {
414
+ process.stdout.write = originalWrite;
415
+ }
416
+ return chunks.join("");
417
+ }
418
+
351
419
  async function makeDeck(root, deckName) {
352
420
  const deckPath = path.join(root, "slides", deckName);
353
421
  await mkdir(path.join(deckPath, "review"), { recursive: true });