@punk6529/playbook 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,68 @@
1
1
  # @punk6529/playbook
2
2
 
3
- Placeholder release to reserve the package name.
3
+ CLI for initializing and updating Playbook workflow assets.
4
4
 
5
- The real implementation will be published in a later version.
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @punk6529/playbook
9
+ ```
10
+
11
+ Or run without global install:
12
+
13
+ ```bash
14
+ npx @punk6529/playbook --help
15
+ ```
16
+
17
+ ## Commands
18
+
19
+ ```bash
20
+ playbook init [path] [--tools all|none|codex,claude] [--force]
21
+ playbook global init [--force]
22
+ playbook update [path] [--tools all|none|codex,claude] [--force]
23
+ ```
24
+
25
+ ## Scope Boundaries
26
+
27
+ - `playbook init` is project-scoped:
28
+ - creates `docs/playbook/{cases,patterns,checklists}` and `docs/playbook/INDEX.json`
29
+ - writes managed skill templates based on `--tools`:
30
+ - Codex: `.codex/skills/{playbook-query,playbook-case}/SKILL.md`
31
+ - Claude: `.claude/skills/{playbook-query,playbook-case}/SKILL.md`
32
+ - `playbook global init` is global-scoped:
33
+ - creates `~/.playbook/repo/{cases,patterns,checklists}`
34
+ - creates `~/.playbook/repo/INDEX.json` and `~/.playbook/repo/tags.yml`
35
+ - does not support `--tools`
36
+ - `playbook update` is project-scoped:
37
+ - updates only managed project templates
38
+ - does not modify user business content (for example case markdown files)
39
+ - V1 does not include `playbook global update`
40
+
41
+ ## Skill Delivery
42
+
43
+ - This release installs namespaced skills for supported tools (`codex`, `claude`):
44
+ - `playbook-query`
45
+ - `playbook-case`
46
+ - CLI responsibility is asset delivery only (`init`/`update`). Runtime skill execution is handled by the host AI tool.
47
+
48
+ ## Conflict Policy
49
+
50
+ - `playbook init` and `playbook global init` are non-destructive by default:
51
+ - conflicting managed files are skipped and reported as `conflict`
52
+ - `playbook update` uses split behavior:
53
+ - managed skill templates are refreshed by default when content differs
54
+ - `docs/playbook/INDEX.json` remains conflict-safe unless `--force` is provided
55
+ - Use `--force` to explicitly overwrite conflicting protected managed files.
56
+
57
+ ## Error Handling
58
+
59
+ - Unknown command or invalid option fails with actionable guidance.
60
+ - Managed file read/write failures include source path and reason.
61
+
62
+ ## Packaging Notes
63
+
64
+ Before publish, verify exact package contents:
65
+
66
+ ```bash
67
+ npm pack --dry-run
68
+ ```
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { runCli } = require("../src/cli");
4
+
5
+ async function main() {
6
+ const exitCode = await runCli(process.argv.slice(2), process);
7
+ process.exitCode = exitCode;
8
+ }
9
+
10
+ main().catch((error) => {
11
+ process.stderr.write(`Unexpected error: ${error.message}\n`);
12
+ process.exitCode = 1;
13
+ });
package/package.json CHANGED
@@ -1,18 +1,26 @@
1
1
  {
2
2
  "name": "@punk6529/playbook",
3
- "version": "0.0.0",
4
- "main": "index.js",
3
+ "version": "0.1.0",
4
+ "description": "CLI for initializing and updating Playbook & Case workflows.",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "playbook": "bin/playbook.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
5
12
  "scripts": {
6
- "test": "echo \"Error: no test specified\" && exit 1"
13
+ "test": "node --test"
7
14
  },
8
- "keywords": [],
15
+ "keywords": ["playbook", "knowledge-base", "cli", "ai-skills", "case-management"],
9
16
  "author": "",
10
17
  "license": "MIT",
11
- "description": "Placeholder package. Real implementation coming soon.",
12
18
  "publishConfig": {
13
19
  "access": "public"
14
20
  },
15
21
  "files": [
16
- "index.js"
22
+ "bin",
23
+ "src",
24
+ "templates"
17
25
  ]
18
26
  }
package/src/cli.js ADDED
@@ -0,0 +1,129 @@
1
+ const { CliError } = require("./errors");
2
+ const { parseProjectCommandArgs, parseGlobalInitArgs } = require("./options");
3
+ const { initProject } = require("./commands/init-project");
4
+ const { initGlobal } = require("./commands/global-init");
5
+ const { updateProject } = require("./commands/update-project");
6
+ const { printSummary } = require("./reporting");
7
+
8
+ function usageText() {
9
+ return [
10
+ "Usage:",
11
+ " playbook init [path] [--tools all|none|codex,claude] [--force]",
12
+ " playbook global init [--force]",
13
+ " playbook update [path] [--tools all|none|codex,claude] [--force]",
14
+ "",
15
+ "Notes:",
16
+ " - `init` and `update` are project-scoped commands.",
17
+ " - `global init` initializes ~/.playbook/repo and does not support --tools.",
18
+ " - V1 does not support `playbook global update`.",
19
+ ].join("\n");
20
+ }
21
+
22
+ function writeStdout(io, line) {
23
+ io.stdout.write(`${line}\n`);
24
+ }
25
+
26
+ function writeStderr(io, line) {
27
+ io.stderr.write(`${line}\n`);
28
+ }
29
+
30
+ function ensureNoHelpRequest(options, io) {
31
+ if (options.help) {
32
+ writeStdout(io, usageText());
33
+ return true;
34
+ }
35
+
36
+ return false;
37
+ }
38
+
39
+ function runInit(args, io) {
40
+ const options = parseProjectCommandArgs(args, "playbook init");
41
+ if (ensureNoHelpRequest(options, io)) {
42
+ return 0;
43
+ }
44
+
45
+ const result = initProject(options);
46
+ printSummary("playbook init completed", result.projectRoot, result.results, (line) =>
47
+ writeStdout(io, line)
48
+ );
49
+ return 0;
50
+ }
51
+
52
+ function runUpdate(args, io) {
53
+ const options = parseProjectCommandArgs(args, "playbook update");
54
+ if (ensureNoHelpRequest(options, io)) {
55
+ return 0;
56
+ }
57
+
58
+ const result = updateProject(options);
59
+ printSummary("playbook update completed", result.projectRoot, result.results, (line) =>
60
+ writeStdout(io, line)
61
+ );
62
+ return 0;
63
+ }
64
+
65
+ function runGlobal(args, io) {
66
+ const subcommand = args[0];
67
+ if (!subcommand) {
68
+ throw new CliError("Missing subcommand for `playbook global`. Supported: init");
69
+ }
70
+
71
+ if (subcommand === "update") {
72
+ throw new CliError(
73
+ "Unsupported command: `playbook global update` (V1). Use `playbook global init` or `playbook update`."
74
+ );
75
+ }
76
+
77
+ if (subcommand !== "init") {
78
+ throw new CliError(`Unknown command: playbook global ${subcommand}`);
79
+ }
80
+
81
+ const options = parseGlobalInitArgs(args.slice(1));
82
+ if (ensureNoHelpRequest(options, io)) {
83
+ return 0;
84
+ }
85
+
86
+ const result = initGlobal(options);
87
+ printSummary("playbook global init completed", result.globalRoot, result.results, (line) =>
88
+ writeStdout(io, line)
89
+ );
90
+ writeStdout(io, "Next: run `playbook init` inside a project to scaffold project assets.");
91
+ return 0;
92
+ }
93
+
94
+ async function runCli(argv, io = process) {
95
+ try {
96
+ if (argv.length === 0 || argv[0] === "-h" || argv[0] === "--help") {
97
+ writeStdout(io, usageText());
98
+ return 0;
99
+ }
100
+
101
+ const command = argv[0];
102
+ if (command === "init") {
103
+ return runInit(argv.slice(1), io);
104
+ }
105
+
106
+ if (command === "update") {
107
+ return runUpdate(argv.slice(1), io);
108
+ }
109
+
110
+ if (command === "global") {
111
+ return runGlobal(argv.slice(1), io);
112
+ }
113
+
114
+ throw new CliError(`Unknown command: ${command}. Supported: init, global, update`);
115
+ } catch (error) {
116
+ if (error instanceof CliError) {
117
+ writeStderr(io, `Error: ${error.message}`);
118
+ return error.exitCode;
119
+ }
120
+
121
+ writeStderr(io, `Unexpected error: ${error.message}`);
122
+ return 1;
123
+ }
124
+ }
125
+
126
+ module.exports = {
127
+ runCli,
128
+ usageText,
129
+ };
@@ -0,0 +1,31 @@
1
+ const path = require("path");
2
+ const { resolveGlobalRoot } = require("../core/context");
3
+ const { GLOBAL_SKELETON_DIRS, GLOBAL_MANAGED_FILES } = require("../manifest");
4
+ const { readTemplate } = require("../template-store");
5
+ const { ensureDirectory, writeManagedFile } = require("../file-ops");
6
+
7
+ function initGlobal(options) {
8
+ const globalRoot = resolveGlobalRoot();
9
+ const results = [];
10
+
11
+ for (const relativeDirectory of GLOBAL_SKELETON_DIRS) {
12
+ const directoryPath = path.join(globalRoot, relativeDirectory);
13
+ results.push(ensureDirectory(directoryPath));
14
+ }
15
+
16
+ for (const managedFile of GLOBAL_MANAGED_FILES) {
17
+ const targetPath = path.join(globalRoot, managedFile.relativePath);
18
+ const content = readTemplate(managedFile.templatePath);
19
+ results.push(writeManagedFile(targetPath, content, options.force));
20
+ }
21
+
22
+ return {
23
+ globalRoot,
24
+ results,
25
+ };
26
+ }
27
+
28
+ module.exports = {
29
+ initGlobal,
30
+ resolveGlobalRoot,
31
+ };
@@ -0,0 +1,30 @@
1
+ const path = require("path");
2
+ const { PROJECT_SKELETON_DIRS, getProjectManagedFiles } = require("../manifest");
3
+ const { readTemplate } = require("../template-store");
4
+ const { ensureDirectory, writeManagedFile } = require("../file-ops");
5
+
6
+ function initProject(options) {
7
+ const projectRoot = path.resolve(options.targetPath);
8
+ const results = [];
9
+
10
+ for (const relativeDirectory of PROJECT_SKELETON_DIRS) {
11
+ const directoryPath = path.join(projectRoot, relativeDirectory);
12
+ results.push(ensureDirectory(directoryPath));
13
+ }
14
+
15
+ const managedFiles = getProjectManagedFiles(options.tools);
16
+ for (const managedFile of managedFiles) {
17
+ const targetPath = path.join(projectRoot, managedFile.relativePath);
18
+ const content = readTemplate(managedFile.templatePath);
19
+ results.push(writeManagedFile(targetPath, content, options.force));
20
+ }
21
+
22
+ return {
23
+ projectRoot,
24
+ results,
25
+ };
26
+ }
27
+
28
+ module.exports = {
29
+ initProject,
30
+ };
@@ -0,0 +1,30 @@
1
+ const path = require("path");
2
+ const { getProjectManagedFiles } = require("../manifest");
3
+ const { readTemplate } = require("../template-store");
4
+ const { writeManagedFile } = require("../file-ops");
5
+
6
+ function updateProject(options) {
7
+ const projectRoot = path.resolve(options.targetPath);
8
+ const results = [];
9
+
10
+ const managedFiles = getProjectManagedFiles(options.tools);
11
+ for (const managedFile of managedFiles) {
12
+ const targetPath = path.join(projectRoot, managedFile.relativePath);
13
+ const content = readTemplate(managedFile.templatePath);
14
+ const defaultOverwrite = managedFile.overwriteOnUpdate === true;
15
+ const effectiveForce = options.force || defaultOverwrite;
16
+ const overwriteReason = !options.force && defaultOverwrite
17
+ ? "overwritten by default managed skill refresh"
18
+ : undefined;
19
+ results.push(writeManagedFile(targetPath, content, effectiveForce, overwriteReason));
20
+ }
21
+
22
+ return {
23
+ projectRoot,
24
+ results,
25
+ };
26
+ }
27
+
28
+ module.exports = {
29
+ updateProject,
30
+ };
@@ -0,0 +1,5 @@
1
+ const SUPPORTED_TOOLS = ["codex", "claude"];
2
+
3
+ module.exports = {
4
+ SUPPORTED_TOOLS,
5
+ };
@@ -0,0 +1,24 @@
1
+ const os = require("os");
2
+ const path = require("path");
3
+ const { CliError } = require("../errors");
4
+
5
+ function resolveGlobalRoot() {
6
+ const home = os.homedir();
7
+ if (!home) {
8
+ throw new CliError("Cannot resolve home directory for global playbook path");
9
+ }
10
+
11
+ return path.join(home, ".playbook", "repo");
12
+ }
13
+
14
+ function resolveRuntimeContext(projectRoot = ".") {
15
+ return {
16
+ projectRoot: path.resolve(projectRoot),
17
+ globalRoot: resolveGlobalRoot(),
18
+ };
19
+ }
20
+
21
+ module.exports = {
22
+ resolveRuntimeContext,
23
+ resolveGlobalRoot,
24
+ };
package/src/errors.js ADDED
@@ -0,0 +1,11 @@
1
+ class CliError extends Error {
2
+ constructor(message, exitCode = 1) {
3
+ super(message);
4
+ this.name = "CliError";
5
+ this.exitCode = exitCode;
6
+ }
7
+ }
8
+
9
+ module.exports = {
10
+ CliError,
11
+ };
@@ -0,0 +1,91 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { CliError } = require("./errors");
4
+
5
+ function withFsError(action, targetPath, error) {
6
+ throw new CliError(`Failed to ${action} '${targetPath}': ${error.code || error.message}`);
7
+ }
8
+
9
+ function ensureDirectory(directoryPath) {
10
+ const existed = fs.existsSync(directoryPath);
11
+
12
+ if (existed) {
13
+ return {
14
+ action: "skipped",
15
+ path: directoryPath,
16
+ reason: "directory already exists",
17
+ type: "directory",
18
+ };
19
+ }
20
+
21
+ try {
22
+ fs.mkdirSync(directoryPath, { recursive: true });
23
+ return {
24
+ action: "added",
25
+ path: directoryPath,
26
+ reason: "directory created",
27
+ type: "directory",
28
+ };
29
+ } catch (error) {
30
+ withFsError("create directory", directoryPath, error);
31
+ }
32
+ }
33
+
34
+ function writeManagedFile(targetPath, content, force, overwriteReason) {
35
+ try {
36
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
37
+ } catch (error) {
38
+ withFsError("prepare parent directory for", targetPath, error);
39
+ }
40
+
41
+ let exists = false;
42
+ let currentContent = "";
43
+
44
+ try {
45
+ exists = fs.existsSync(targetPath);
46
+ if (exists) {
47
+ currentContent = fs.readFileSync(targetPath, "utf8");
48
+ }
49
+ } catch (error) {
50
+ withFsError("read file", targetPath, error);
51
+ }
52
+
53
+ if (!exists) {
54
+ try {
55
+ fs.writeFileSync(targetPath, content, "utf8");
56
+ return { action: "added", path: targetPath, reason: "file created", type: "file" };
57
+ } catch (error) {
58
+ withFsError("write file", targetPath, error);
59
+ }
60
+ }
61
+
62
+ if (currentContent === content) {
63
+ return { action: "skipped", path: targetPath, reason: "unchanged", type: "file" };
64
+ }
65
+
66
+ if (!force) {
67
+ return {
68
+ action: "conflict",
69
+ path: targetPath,
70
+ reason: "content differs (rerun with --force to overwrite)",
71
+ type: "file",
72
+ };
73
+ }
74
+
75
+ try {
76
+ fs.writeFileSync(targetPath, content, "utf8");
77
+ return {
78
+ action: "updated",
79
+ path: targetPath,
80
+ reason: overwriteReason || "overwritten by --force",
81
+ type: "file",
82
+ };
83
+ } catch (error) {
84
+ withFsError("overwrite file", targetPath, error);
85
+ }
86
+ }
87
+
88
+ module.exports = {
89
+ ensureDirectory,
90
+ writeManagedFile,
91
+ };
package/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ const { runCli } = require("./cli");
2
+
3
+ module.exports = {
4
+ runCli,
5
+ };
@@ -0,0 +1,73 @@
1
+ const PROJECT_SKELETON_DIRS = [
2
+ "docs/playbook/cases",
3
+ "docs/playbook/patterns",
4
+ "docs/playbook/checklists",
5
+ ];
6
+
7
+ const PROJECT_MANAGED_FILES = [
8
+ {
9
+ relativePath: "docs/playbook/INDEX.json",
10
+ templatePath: "project/docs/playbook/INDEX.json",
11
+ },
12
+ {
13
+ relativePath: "docs/playbook/cases/_example-001-delete-me.md",
14
+ templatePath: "project/docs/playbook/cases/_example-001-delete-me.md",
15
+ },
16
+ {
17
+ relativePath: "docs/playbook/patterns/_example-001-delete-me.md",
18
+ templatePath: "project/docs/playbook/patterns/_example-001-delete-me.md",
19
+ },
20
+ {
21
+ relativePath: "docs/playbook/checklists/_example-001-delete-me.md",
22
+ templatePath: "project/docs/playbook/checklists/_example-001-delete-me.md",
23
+ },
24
+ {
25
+ relativePath: ".codex/skills/playbook-query/SKILL.md",
26
+ templatePath: "project/skills/playbook-query/SKILL.md",
27
+ tool: "codex",
28
+ overwriteOnUpdate: true,
29
+ },
30
+ {
31
+ relativePath: ".codex/skills/playbook-case/SKILL.md",
32
+ templatePath: "project/skills/playbook-case/SKILL.md",
33
+ tool: "codex",
34
+ overwriteOnUpdate: true,
35
+ },
36
+ {
37
+ relativePath: ".claude/skills/playbook-query/SKILL.md",
38
+ templatePath: "project/skills/playbook-query/SKILL.md",
39
+ tool: "claude",
40
+ overwriteOnUpdate: true,
41
+ },
42
+ {
43
+ relativePath: ".claude/skills/playbook-case/SKILL.md",
44
+ templatePath: "project/skills/playbook-case/SKILL.md",
45
+ tool: "claude",
46
+ overwriteOnUpdate: true,
47
+ },
48
+ ];
49
+
50
+ const GLOBAL_SKELETON_DIRS = ["cases", "patterns", "checklists"];
51
+
52
+ const GLOBAL_MANAGED_FILES = [
53
+ {
54
+ relativePath: "INDEX.json",
55
+ templatePath: "global/INDEX.json",
56
+ },
57
+ {
58
+ relativePath: "tags.yml",
59
+ templatePath: "global/tags.yml",
60
+ },
61
+ ];
62
+
63
+ function getProjectManagedFiles(tools) {
64
+ return PROJECT_MANAGED_FILES.filter((file) => !file.tool || tools.includes(file.tool));
65
+ }
66
+
67
+ module.exports = {
68
+ PROJECT_SKELETON_DIRS,
69
+ PROJECT_MANAGED_FILES,
70
+ GLOBAL_SKELETON_DIRS,
71
+ GLOBAL_MANAGED_FILES,
72
+ getProjectManagedFiles,
73
+ };
package/src/options.js ADDED
@@ -0,0 +1,113 @@
1
+ const { CliError } = require("./errors");
2
+ const { SUPPORTED_TOOLS } = require("./constants");
3
+
4
+ function parseToolsValue(rawValue) {
5
+ if (!rawValue) {
6
+ throw new CliError("Missing value for --tools");
7
+ }
8
+
9
+ const normalized = rawValue.trim().toLowerCase();
10
+ if (normalized === "all") {
11
+ return SUPPORTED_TOOLS.slice();
12
+ }
13
+
14
+ if (normalized === "none") {
15
+ return [];
16
+ }
17
+
18
+ const values = normalized
19
+ .split(",")
20
+ .map((item) => item.trim())
21
+ .filter(Boolean);
22
+
23
+ if (values.length === 0) {
24
+ throw new CliError("Invalid --tools value. Use all, none, or codex,claude");
25
+ }
26
+
27
+ const invalid = values.filter((value) => !SUPPORTED_TOOLS.includes(value));
28
+ if (invalid.length > 0) {
29
+ throw new CliError(
30
+ `Unsupported tool(s): ${invalid.join(", ")}. Supported: ${SUPPORTED_TOOLS.join(", ")}`
31
+ );
32
+ }
33
+
34
+ return SUPPORTED_TOOLS.filter((tool) => values.includes(tool));
35
+ }
36
+
37
+ function parseProjectCommandArgs(args, commandName) {
38
+ let force = false;
39
+ let tools = SUPPORTED_TOOLS.slice();
40
+ const positionals = [];
41
+
42
+ for (let i = 0; i < args.length; i += 1) {
43
+ const arg = args[i];
44
+
45
+ if (arg === "--force") {
46
+ force = true;
47
+ continue;
48
+ }
49
+
50
+ if (arg === "--tools") {
51
+ const next = args[i + 1];
52
+ tools = parseToolsValue(next);
53
+ i += 1;
54
+ continue;
55
+ }
56
+
57
+ if (arg.startsWith("--tools=")) {
58
+ const value = arg.slice("--tools=".length);
59
+ tools = parseToolsValue(value);
60
+ continue;
61
+ }
62
+
63
+ if (arg === "-h" || arg === "--help") {
64
+ return { help: true };
65
+ }
66
+
67
+ if (arg.startsWith("-")) {
68
+ throw new CliError(`Unknown option for ${commandName}: ${arg}`);
69
+ }
70
+
71
+ positionals.push(arg);
72
+ }
73
+
74
+ if (positionals.length > 1) {
75
+ throw new CliError(`Too many path arguments for ${commandName}`);
76
+ }
77
+
78
+ return {
79
+ help: false,
80
+ force,
81
+ tools,
82
+ targetPath: positionals[0] || ".",
83
+ };
84
+ }
85
+
86
+ function parseGlobalInitArgs(args) {
87
+ let force = false;
88
+
89
+ for (const arg of args) {
90
+ if (arg === "--force") {
91
+ force = true;
92
+ continue;
93
+ }
94
+
95
+ if (arg === "-h" || arg === "--help") {
96
+ return { help: true };
97
+ }
98
+
99
+ if (arg === "--tools" || arg.startsWith("--tools=")) {
100
+ throw new CliError("`playbook global init` does not support --tools");
101
+ }
102
+
103
+ throw new CliError(`Unknown option for playbook global init: ${arg}`);
104
+ }
105
+
106
+ return { help: false, force };
107
+ }
108
+
109
+ module.exports = {
110
+ parseProjectCommandArgs,
111
+ parseGlobalInitArgs,
112
+ parseToolsValue,
113
+ };
@@ -0,0 +1,42 @@
1
+ const path = require("path");
2
+
3
+ function formatPath(basePath, absolutePath) {
4
+ const relative = path.relative(basePath, absolutePath);
5
+ return relative || ".";
6
+ }
7
+
8
+ function countByAction(results) {
9
+ const counters = {
10
+ added: 0,
11
+ updated: 0,
12
+ skipped: 0,
13
+ conflict: 0,
14
+ };
15
+
16
+ for (const result of results) {
17
+ if (Object.prototype.hasOwnProperty.call(counters, result.action)) {
18
+ counters[result.action] += 1;
19
+ }
20
+ }
21
+
22
+ return counters;
23
+ }
24
+
25
+ function printSummary(title, basePath, results, writeLine) {
26
+ writeLine(`\n${title}`);
27
+
28
+ for (const result of results) {
29
+ const displayPath = formatPath(basePath, result.path);
30
+ const reasonSuffix = result.reason ? ` (${result.reason})` : "";
31
+ writeLine(`- [${result.action}] ${displayPath}${reasonSuffix}`);
32
+ }
33
+
34
+ const counts = countByAction(results);
35
+ writeLine(
36
+ `Summary: added=${counts.added}, updated=${counts.updated}, skipped=${counts.skipped}, conflict=${counts.conflict}`
37
+ );
38
+ }
39
+
40
+ module.exports = {
41
+ printSummary,
42
+ };
@@ -0,0 +1,22 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { CliError } = require("./errors");
4
+
5
+ const TEMPLATE_ROOT = path.resolve(__dirname, "..", "templates");
6
+
7
+ function readTemplate(templatePath) {
8
+ const absolutePath = path.join(TEMPLATE_ROOT, templatePath);
9
+
10
+ try {
11
+ return fs.readFileSync(absolutePath, "utf8");
12
+ } catch (error) {
13
+ throw new CliError(
14
+ `Failed to read template '${templatePath}': ${error.code || error.message}`
15
+ );
16
+ }
17
+ }
18
+
19
+ module.exports = {
20
+ TEMPLATE_ROOT,
21
+ readTemplate,
22
+ };
@@ -0,0 +1,9 @@
1
+ {
2
+ "_example-001-delete-me": {
3
+ "type": "case",
4
+ "title": "Example entry — delete this and add real entries via playbook-case skill",
5
+ "tags": ["workflow"],
6
+ "path": "cases/_example-001-delete-me.md",
7
+ "created": "2026-01-01"
8
+ }
9
+ }
@@ -0,0 +1,34 @@
1
+ tags:
2
+ - id: auth
3
+ desc: Authentication and authorization issues.
4
+ aliases: [login, oauth, permission]
5
+ - id: web
6
+ desc: Frontend and browser behavior.
7
+ aliases: [frontend, browser, ui]
8
+ - id: db
9
+ desc: Database schema, query, and migration issues.
10
+ aliases: [database, sql, migration]
11
+ - id: env
12
+ desc: Environment variables and local setup problems.
13
+ aliases: [environment, dotenv, config]
14
+ - id: proxy
15
+ desc: Networking, proxy, and request forwarding issues.
16
+ aliases: [gateway, reverse-proxy, tunnel]
17
+ - id: ci
18
+ desc: Continuous integration and pipeline failures.
19
+ aliases: [pipeline, github-actions, build]
20
+ - id: ios
21
+ desc: iOS build, signing, and simulator issues.
22
+ aliases: [xcode, swift, mobile]
23
+ - id: testing
24
+ desc: Test failures, flaky tests, and test infra.
25
+ aliases: [test, qa, flaky]
26
+ - id: workflow
27
+ desc: Engineering process and development workflow issues.
28
+ aliases: [process, collaboration, handoff]
29
+ - id: api
30
+ desc: API contract, integration, and compatibility issues.
31
+ aliases: [endpoint, contract, integration]
32
+ - id: cli
33
+ desc: Command-line interface behavior and ergonomics.
34
+ aliases: [command, terminal, shell]
@@ -0,0 +1,23 @@
1
+ {
2
+ "case-001-example-node-version-mismatch": {
3
+ "type": "case",
4
+ "title": "Deployment failed due to Node.js version mismatch",
5
+ "tags": ["env", "ci"],
6
+ "path": "cases/_example-001-delete-me.md",
7
+ "created": "2026-01-01"
8
+ },
9
+ "pattern-001-example-pin-runtime-versions": {
10
+ "type": "pattern",
11
+ "title": "Pin runtime versions before upgrading dependencies",
12
+ "tags": ["env", "workflow"],
13
+ "path": "patterns/_example-001-delete-me.md",
14
+ "created": "2026-01-01"
15
+ },
16
+ "checklist-001-example-pre-publish": {
17
+ "type": "checklist",
18
+ "title": "Pre-publish checklist",
19
+ "tags": ["workflow", "ci"],
20
+ "path": "checklists/_example-001-delete-me.md",
21
+ "created": "2026-01-01"
22
+ }
23
+ }
@@ -0,0 +1,24 @@
1
+ # Deployment failed due to Node.js version mismatch
2
+
3
+ > Delete this example file and create real cases using the playbook-case skill.
4
+
5
+ ## Problem
6
+
7
+ Production deployment failed silently. The build succeeded locally but crashed on startup in CI with `SyntaxError: Unexpected token ??=`.
8
+
9
+ ## Context
10
+
11
+ - Local dev environment: Node 20
12
+ - CI runner: Node 16 (inherited from base image, not pinned)
13
+ - The `??=` operator (logical nullish assignment) requires Node 15+
14
+ - No `engines` field in package.json to catch this earlier
15
+
16
+ ## Solution
17
+
18
+ 1. Added `"engines": { "node": ">=20" }` to package.json
19
+ 2. Updated CI base image to `node:20-slim`
20
+ 3. Added `engine-strict=true` to `.npmrc` so mismatches fail fast on `npm install`
21
+
22
+ ## Takeaway
23
+
24
+ Always pin the Node.js version in `engines` and enforce it. Silent version mismatches cause the worst kind of debugging — everything works locally.
@@ -0,0 +1,10 @@
1
+ # Pre-publish checklist
2
+
3
+ > Delete this example file and create real checklists using the playbook-case skill.
4
+
5
+ - [ ] Run `npm pack --dry-run` and verify included files
6
+ - [ ] Check `files` field in package.json matches intended contents
7
+ - [ ] Verify version number is correct and not already published
8
+ - [ ] Run full test suite: `npm test`
9
+ - [ ] Check for accidentally included secrets or credentials
10
+ - [ ] Confirm README is up to date with current API/usage
@@ -0,0 +1,15 @@
1
+ # Pin runtime versions before upgrading dependencies
2
+
3
+ > Delete this example file and create real patterns using the playbook-case skill.
4
+
5
+ ## When to Use
6
+
7
+ Apply this pattern whenever you upgrade a major runtime (Node.js, Python, Ruby) or a core dependency that has runtime version requirements.
8
+
9
+ ## Pattern
10
+
11
+ 1. **Pin the current version first** — add `engines` field (Node), `python_requires` (Python), or equivalent before making any changes
12
+ 2. **Upgrade in CI first** — update the CI base image/config before changing local dev setup
13
+ 3. **Run full test suite against new version** — don't rely on local smoke tests
14
+ 4. **Update lockfile** — regenerate `package-lock.json` / `yarn.lock` under the new runtime
15
+ 5. **Communicate** — update README and onboarding docs with new version requirement
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: playbook-case
3
+ description: Draft and inspect playbook case entries with explicit scope and tags.
4
+ license: MIT
5
+ ---
6
+
7
+ Use this skill when the user asks to create, review, or inspect case entries.
8
+
9
+ ## Behavior
10
+
11
+ - Keep scope explicit (`project` or `global`).
12
+ - Keep draft generation non-destructive by default.
13
+ - For new cases, enforce this guided flow:
14
+ 1. If the user has not provided a concrete problem statement, ask first:
15
+ "你想把之前遇到的哪个问题沉淀为 case?请一句话描述。"
16
+ 2. After problem statement is available, suggest tags from registry/index context first:
17
+ - Prefer project index: `docs/playbook/INDEX.json`
18
+ - Use global index only when relevant: `~/.playbook/repo/INDEX.json`
19
+ - Ask user to choose existing tags or explicitly confirm a new tag
20
+ 3. If scope is not confirmed, ask for scope confirmation:
21
+ - default is `project`
22
+ - use `global` only when user explicitly confirms
23
+ 4. Only after required inputs are complete, produce the draft package.
24
+ - Required inputs before any draft output:
25
+ - problem statement
26
+ - tag decision (existing tags or confirmed new tag)
27
+ - scope confirmation
28
+ - Hard gate: while any required input is missing, do not output case ID, suggested path, markdown draft, or INDEX update suggestion.
29
+ - For new cases, once inputs are complete, produce a complete draft package:
30
+ - case ID (format: `{type}-{NNN}-{slug}`, e.g. `case-001-oauth-token-race`)
31
+ - suggested path (e.g. `cases/case-001-oauth-token-race.md`)
32
+ - markdown draft
33
+ - INDEX.json update: the exact JSON entry to add
34
+
35
+ ## INDEX.json Schema
36
+
37
+ INDEX.json is a flat object keyed by entry ID. Each entry has this structure:
38
+
39
+ ```json
40
+ {
41
+ "case-001-oauth-token-race": {
42
+ "type": "case",
43
+ "title": "OAuth token refresh race condition under concurrency",
44
+ "tags": ["auth", "api"],
45
+ "path": "cases/case-001-oauth-token-race.md",
46
+ "created": "2026-02-10"
47
+ }
48
+ }
49
+ ```
50
+
51
+ ### Fields
52
+
53
+ | Field | Type | Description |
54
+ |-------|------|-------------|
55
+ | `type` | `"case"` \| `"pattern"` \| `"checklist"` | Entry classification |
56
+ | `title` | string | Human-readable one-line summary |
57
+ | `tags` | string[] | Tag IDs from registry (`tags.yml` or INDEX context) |
58
+ | `path` | string | Relative path to the content file |
59
+ | `created` | string | Creation date in YYYY-MM-DD format |
60
+
61
+ ### Entry ID Convention
62
+
63
+ Format: `{type}-{NNN}-{slug}`
64
+ - `type`: one of `case`, `pattern`, `checklist`
65
+ - `NNN`: zero-padded 3-digit sequence number (check existing entries to determine next number)
66
+ - `slug`: lowercase hyphenated descriptor derived from the title
67
+
68
+ ### Draft Package Output
69
+
70
+ When producing the draft package, output the INDEX entry as a ready-to-merge JSON snippet:
71
+
72
+ ```
73
+ **Entry ID:** case-001-oauth-token-race
74
+ **Path:** cases/case-001-oauth-token-race.md
75
+ **INDEX update:**
76
+ Add this entry to INDEX.json:
77
+ {
78
+ "case-001-oauth-token-race": {
79
+ "type": "case",
80
+ "title": "...",
81
+ "tags": ["..."],
82
+ "path": "cases/case-001-oauth-token-race.md",
83
+ "created": "YYYY-MM-DD"
84
+ }
85
+ }
86
+ ```
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: playbook-query
3
+ description: Query project and global playbook knowledge in a concise, index-first way.
4
+ license: MIT
5
+ ---
6
+
7
+ Use this skill when the user asks to search, filter, or inspect playbook knowledge.
8
+
9
+ ## Behavior
10
+
11
+ ### Lookup Sequence
12
+
13
+ **IMPORTANT: Only read the specific files listed below by their exact path. Do NOT use broad file search, glob, or grep across the filesystem — this can trigger macOS privacy prompts.**
14
+
15
+ 1. **Project first**: Read `docs/playbook/INDEX.json` (relative to project root)
16
+ 2. **Global fallback**: If no matches found, or user asks for broader results, read `~/.playbook/repo/INDEX.json` (this exact path only)
17
+ 3. Merge results if both scopes have relevant entries. Label scope in output.
18
+
19
+ ### Matching
20
+
21
+ - Parse INDEX.json: each key is an entry ID, each value has `type`, `title`, `tags`, `path`, `created`
22
+ - Match user query against:
23
+ - `tags` array (primary match — compare with tag IDs and aliases from `tags.yml`)
24
+ - `title` text (secondary match — keyword overlap)
25
+ - `type` filter (if user specifies "show me patterns" or "any checklists for...")
26
+ - If no matches found in either scope, say so clearly
27
+
28
+ ### Progressive Disclosure
29
+
30
+ Return results in order of increasing detail and token cost:
31
+
32
+ 1. **Checklists first** — actionable, cheapest to show. Display inline if short.
33
+ 2. **Patterns next** — show title + tags summary. Offer to expand.
34
+ 3. **Cases last** — show title + tags summary only. Expand full content only when user explicitly asks.
35
+
36
+ For each match, show:
37
+ ```
38
+ [{type}] {title} (tags: {tags}) [{scope}]
39
+ ```
40
+
41
+ ### Expanding Entries
42
+
43
+ - When user asks to see details, read the content file at the entry's `path` (relative to `docs/playbook/` for project, `~/.playbook/repo/` for global)
44
+ - Show the full markdown content of the referenced file
45
+
46
+ ### Edge Cases
47
+
48
+ - If INDEX.json is empty (`{}`), tell the user: "No entries yet. Use playbook-case to create your first entry."
49
+ - If a referenced file at `path` doesn't exist, report it as a broken reference
50
+ - If user query is vague, show all entries grouped by type
package/index.js DELETED
@@ -1 +0,0 @@
1
- module.exports = {};