@punk6529/playbook 0.0.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/README.md CHANGED
@@ -1,5 +1,137 @@
1
1
  # @punk6529/playbook
2
2
 
3
- Placeholder release to reserve the package name.
3
+ 结构化工程知识库 CLI —— 初始化、更新和查询项目 Playbook 资产。
4
4
 
5
- The real implementation will be published in a later version.
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install -g @punk6529/playbook
9
+ ```
10
+
11
+ 或免安装直接运行:
12
+
13
+ ```bash
14
+ npx @punk6529/playbook --help
15
+ ```
16
+
17
+ ## 命令
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
+ playbook query [tags...] [--limit N] [--scope project|global|both] [--json] [--tags-only]
24
+ ```
25
+
26
+ ## 作用范围
27
+
28
+ - `playbook init` 项目级:
29
+ - 创建 `docs/playbook/{cases,patterns,checklists}` 和 `docs/playbook/INDEX.json`
30
+ - 根据 `--tools` 参数写入 AI skill 模板:
31
+ - Codex: `.codex/skills/{playbook-query,playbook-case,playbook-advisor}/SKILL.md`
32
+ - Claude: `.claude/skills/{playbook-query,playbook-case,playbook-advisor}/SKILL.md`
33
+ - `playbook global init` 全局级:
34
+ - 创建 `~/.playbook/repo/{cases,patterns,checklists}`
35
+ - 创建 `~/.playbook/repo/INDEX.json` 和 `~/.playbook/repo/tags.yml`
36
+ - 不支持 `--tools`
37
+ - `playbook update` 项目级:
38
+ - 仅更新托管模板文件
39
+ - 不修改用户业务内容(如 case markdown 文件)
40
+ - V1 暂不支持 `playbook global update`
41
+
42
+ ## Skill 交付
43
+
44
+ CLI 为支持的 AI 工具(`codex`、`claude`)安装以下 skill:
45
+
46
+ | Skill | 用途 |
47
+ |-------|------|
48
+ | `playbook-query` | 手动查询知识库 |
49
+ | `playbook-case` | 引导式 case 创建 |
50
+ | `playbook-advisor` | 主动知识应用(自动感知 tag,AI 自主查询) |
51
+
52
+ CLI 只负责文件交付(`init`/`update`),skill 的运行时执行由宿主 AI 工具处理。
53
+
54
+ ## 冲突策略
55
+
56
+ - `playbook init` 和 `playbook global init` 默认非破坏性:
57
+ - 有冲突的托管文件会跳过并报告为 `conflict`
58
+ - `playbook update` 采用分级策略:
59
+ - skill 模板:内容不同时自动刷新
60
+ - `docs/playbook/INDEX.json`:保护不覆盖,除非使用 `--force`
61
+ - 使用 `--force` 可强制覆盖受保护的托管文件
62
+
63
+ ## 查询命令
64
+
65
+ `playbook query` 按 tag 搜索 INDEX.json 条目(OR 逻辑),同时搜索项目和全局范围。
66
+
67
+ ```bash
68
+ playbook query docker env # 匹配 "docker" 或 "env" 的条目
69
+ playbook query # 所有条目
70
+ playbook query --tags-only # 仅输出 tag 概览:docker(3) env(5) ci(2)
71
+ playbook query --scope project # 仅项目范围
72
+ playbook query --limit 0 # 不限数量
73
+ playbook query docker --json # JSON 格式输出
74
+ ```
75
+
76
+ ## 在 AI 中使用
77
+
78
+ `playbook init` 之后,AI 工具中有三个 skill 可用:
79
+
80
+ ### 1. `/playbook-advisor` —— 主动知识顾问
81
+
82
+ 在会话开始时调用一次,AI 会自动加载 tag 感知,后续遇到相关问题时自主查询。
83
+
84
+ **典型使用流程:**
85
+
86
+ ```
87
+ 你:/playbook-advisor
88
+ AI:(自动执行 playbook query --tags-only)
89
+ AI:已加载知识库标签:docker(3) env(5) react(2) ci(1)
90
+
91
+ ...(正常工作中)...
92
+
93
+ AI:(遇到 Docker 端口绑定问题,发现与 "docker" tag 相关)
94
+ AI:(自动执行 playbook query docker)
95
+ AI:(发现 "case-001-docker-bind" 标题相关,读取该 case)
96
+ AI:根据之前的经验,这个问题是因为 Docker 默认绑定 0.0.0.0,需要改为 127.0.0.1...
97
+ ```
98
+
99
+ 特点:用户触发一次,之后 AI 自主判断何时查询,无需人工干预。
100
+
101
+ ### 2. `/playbook-query` —— 手动知识查询
102
+
103
+ 用户主动搜索知识库时使用。
104
+
105
+ ```
106
+ 你:/playbook-query
107
+ AI:请提供要搜索的 tag
108
+ 你:docker
109
+ AI:找到以下条目:
110
+ [project] case-001-docker-bind Docker bind address issue docker,env
111
+ (1 match: 1 project)
112
+ ```
113
+
114
+ ### 3. `/playbook-case` —— 记录新 case
115
+
116
+ 解决问题后,用来记录经验到知识库。
117
+
118
+ ```
119
+ 你:/playbook-case
120
+ AI:请描述遇到的问题...
121
+ 你:Next.js 部署到 Vercel 后 API 路由 404
122
+ AI:(引导你填写 问题/原因/解决/教训 四个部分)
123
+ AI:(生成 case 文件,更新 INDEX.json)
124
+ ```
125
+
126
+ ## 错误处理
127
+
128
+ - 未知命令或无效选项会给出可操作的提示信息
129
+ - 托管文件读写失败会包含源路径和原因
130
+
131
+ ## 打包说明
132
+
133
+ 发布前确认包内容:
134
+
135
+ ```bash
136
+ npm pack --dry-run
137
+ ```
@@ -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.2.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,171 @@
1
+ const { CliError } = require("./errors");
2
+ const { parseProjectCommandArgs, parseGlobalInitArgs, parseQueryArgs } = 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 { queryIndex, aggregateTags, formatPlainText, formatJson, formatTagsOnly } = require("./commands/query");
7
+ const { printSummary } = require("./reporting");
8
+
9
+ function usageText() {
10
+ return [
11
+ "Usage:",
12
+ " playbook init [path] [--tools all|none|codex,claude] [--force]",
13
+ " playbook global init [--force]",
14
+ " playbook update [path] [--tools all|none|codex,claude] [--force]",
15
+ " playbook query [tags...] [--limit N] [--scope project|global|both] [--json] [--tags-only]",
16
+ "",
17
+ "Notes:",
18
+ " - `init` and `update` are project-scoped commands.",
19
+ " - `global init` initializes ~/.playbook/repo and does not support --tools.",
20
+ " - `query` searches INDEX.json by tags and returns matching entries.",
21
+ " - V1 does not support `playbook global update`.",
22
+ ].join("\n");
23
+ }
24
+
25
+ function writeStdout(io, line) {
26
+ io.stdout.write(`${line}\n`);
27
+ }
28
+
29
+ function writeStderr(io, line) {
30
+ io.stderr.write(`${line}\n`);
31
+ }
32
+
33
+ function ensureNoHelpRequest(options, io) {
34
+ if (options.help) {
35
+ writeStdout(io, usageText());
36
+ return true;
37
+ }
38
+
39
+ return false;
40
+ }
41
+
42
+ function runInit(args, io) {
43
+ const options = parseProjectCommandArgs(args, "playbook init");
44
+ if (ensureNoHelpRequest(options, io)) {
45
+ return 0;
46
+ }
47
+
48
+ const result = initProject(options);
49
+ printSummary("playbook init completed", result.projectRoot, result.results, (line) =>
50
+ writeStdout(io, line)
51
+ );
52
+ return 0;
53
+ }
54
+
55
+ function runUpdate(args, io) {
56
+ const options = parseProjectCommandArgs(args, "playbook update");
57
+ if (ensureNoHelpRequest(options, io)) {
58
+ return 0;
59
+ }
60
+
61
+ const result = updateProject(options);
62
+ printSummary("playbook update completed", result.projectRoot, result.results, (line) =>
63
+ writeStdout(io, line)
64
+ );
65
+ return 0;
66
+ }
67
+
68
+ function queryHelpText() {
69
+ return [
70
+ "Usage: playbook query [tags...] [options]",
71
+ "",
72
+ "Search INDEX.json entries by tags (OR logic).",
73
+ "Without tags, returns all entries.",
74
+ "",
75
+ "Options:",
76
+ " --limit N Max results to return (default: 7, 0 = unlimited)",
77
+ " --scope X Search scope: project, global, or both (default: both)",
78
+ " --json Output results as JSON array",
79
+ " --tags-only Output only tag names with case counts",
80
+ " -h, --help Show this help",
81
+ ].join("\n");
82
+ }
83
+
84
+ function runQuery(args, io) {
85
+ const options = parseQueryArgs(args);
86
+ if (options.help) {
87
+ writeStdout(io, queryHelpText());
88
+ return 0;
89
+ }
90
+
91
+ if (options.tagsOnly) {
92
+ const result = aggregateTags(options);
93
+ writeStdout(io, formatTagsOnly(result));
94
+ return 0;
95
+ }
96
+
97
+ const result = queryIndex(options);
98
+ const output = options.json ? formatJson(result) : formatPlainText(result);
99
+ writeStdout(io, output);
100
+ return 0;
101
+ }
102
+
103
+ function runGlobal(args, io) {
104
+ const subcommand = args[0];
105
+ if (!subcommand) {
106
+ throw new CliError("Missing subcommand for `playbook global`. Supported: init");
107
+ }
108
+
109
+ if (subcommand === "update") {
110
+ throw new CliError(
111
+ "Unsupported command: `playbook global update` (V1). Use `playbook global init` or `playbook update`."
112
+ );
113
+ }
114
+
115
+ if (subcommand !== "init") {
116
+ throw new CliError(`Unknown command: playbook global ${subcommand}`);
117
+ }
118
+
119
+ const options = parseGlobalInitArgs(args.slice(1));
120
+ if (ensureNoHelpRequest(options, io)) {
121
+ return 0;
122
+ }
123
+
124
+ const result = initGlobal(options);
125
+ printSummary("playbook global init completed", result.globalRoot, result.results, (line) =>
126
+ writeStdout(io, line)
127
+ );
128
+ writeStdout(io, "Next: run `playbook init` inside a project to scaffold project assets.");
129
+ return 0;
130
+ }
131
+
132
+ async function runCli(argv, io = process) {
133
+ try {
134
+ if (argv.length === 0 || argv[0] === "-h" || argv[0] === "--help") {
135
+ writeStdout(io, usageText());
136
+ return 0;
137
+ }
138
+
139
+ const command = argv[0];
140
+ if (command === "init") {
141
+ return runInit(argv.slice(1), io);
142
+ }
143
+
144
+ if (command === "update") {
145
+ return runUpdate(argv.slice(1), io);
146
+ }
147
+
148
+ if (command === "query") {
149
+ return runQuery(argv.slice(1), io);
150
+ }
151
+
152
+ if (command === "global") {
153
+ return runGlobal(argv.slice(1), io);
154
+ }
155
+
156
+ throw new CliError(`Unknown command: ${command}. Supported: init, global, update, query`);
157
+ } catch (error) {
158
+ if (error instanceof CliError) {
159
+ writeStderr(io, `Error: ${error.message}`);
160
+ return error.exitCode;
161
+ }
162
+
163
+ writeStderr(io, `Unexpected error: ${error.message}`);
164
+ return 1;
165
+ }
166
+ }
167
+
168
+ module.exports = {
169
+ runCli,
170
+ usageText,
171
+ };
@@ -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,172 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+
5
+ const DEFAULT_LIMIT = 7;
6
+
7
+ function readIndex(indexPath) {
8
+ try {
9
+ const content = fs.readFileSync(indexPath, "utf8");
10
+ return JSON.parse(content);
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+
16
+ function filterByTags(index, tags) {
17
+ const entries = [];
18
+ for (const [id, entry] of Object.entries(index)) {
19
+ if (tags.length === 0 || entry.tags.some((t) => tags.includes(t))) {
20
+ entries.push({ id, ...entry });
21
+ }
22
+ }
23
+ return entries;
24
+ }
25
+
26
+ function queryIndex(options) {
27
+ const { tags = [], limit = DEFAULT_LIMIT, scope = "both", targetPath = "." } = options;
28
+
29
+ const projectIndexPath = path.resolve(targetPath, "docs/playbook/INDEX.json");
30
+ const globalIndexPath = path.join(os.homedir(), ".playbook/repo/INDEX.json");
31
+
32
+ let projectEntries = [];
33
+ let globalEntries = [];
34
+ let anyIndexFound = false;
35
+
36
+ if (scope === "both" || scope === "project") {
37
+ const projectIndex = readIndex(projectIndexPath);
38
+ if (projectIndex) {
39
+ anyIndexFound = true;
40
+ projectEntries = filterByTags(projectIndex, tags).map((e) => ({ ...e, scope: "project" }));
41
+ }
42
+ }
43
+
44
+ if (scope === "both" || scope === "global") {
45
+ const globalIndex = readIndex(globalIndexPath);
46
+ if (globalIndex) {
47
+ anyIndexFound = true;
48
+ globalEntries = filterByTags(globalIndex, tags).map((e) => ({ ...e, scope: "global" }));
49
+ }
50
+ }
51
+
52
+ const all = [...projectEntries, ...globalEntries];
53
+
54
+ if (!anyIndexFound) {
55
+ return { entries: [], found: false };
56
+ }
57
+
58
+ const limited = limit > 0 ? all.slice(0, limit) : all;
59
+
60
+ return { entries: limited, total: all.length, found: true };
61
+ }
62
+
63
+ function formatPlainText(result) {
64
+ if (!result.found) {
65
+ return "No playbook INDEX found.";
66
+ }
67
+
68
+ if (result.entries.length === 0) {
69
+ return "No matching entries.";
70
+ }
71
+
72
+ const lines = result.entries.map(
73
+ (e) => `[${e.scope}]\t${e.id}\t${e.title}\t${e.tags.join(",")}`
74
+ );
75
+
76
+ const displayed = result.entries.length;
77
+ const total = result.total;
78
+ const projectCount = result.entries.filter((e) => e.scope === "project").length;
79
+ const globalCount = result.entries.filter((e) => e.scope === "global").length;
80
+
81
+ const parts = [];
82
+ if (projectCount > 0) parts.push(`${projectCount} project`);
83
+ if (globalCount > 0) parts.push(`${globalCount} global`);
84
+
85
+ let summary = `(${total} match${total === 1 ? "" : "es"}`;
86
+ if (displayed < total) {
87
+ summary += `, showing ${displayed}`;
88
+ }
89
+ summary += `: ${parts.join(", ")})`;
90
+
91
+ lines.push(summary);
92
+ return lines.join("\n");
93
+ }
94
+
95
+ function aggregateTags(options) {
96
+ const { scope = "both", targetPath = "." } = options;
97
+
98
+ const projectIndexPath = path.resolve(targetPath, "docs/playbook/INDEX.json");
99
+ const globalIndexPath = path.join(os.homedir(), ".playbook/repo/INDEX.json");
100
+
101
+ const tagCounts = {};
102
+ let anyIndexFound = false;
103
+
104
+ function countTags(index) {
105
+ for (const entry of Object.values(index)) {
106
+ for (const tag of entry.tags) {
107
+ tagCounts[tag] = (tagCounts[tag] || 0) + 1;
108
+ }
109
+ }
110
+ }
111
+
112
+ if (scope === "both" || scope === "project") {
113
+ const projectIndex = readIndex(projectIndexPath);
114
+ if (projectIndex) {
115
+ anyIndexFound = true;
116
+ countTags(projectIndex);
117
+ }
118
+ }
119
+
120
+ if (scope === "both" || scope === "global") {
121
+ const globalIndex = readIndex(globalIndexPath);
122
+ if (globalIndex) {
123
+ anyIndexFound = true;
124
+ countTags(globalIndex);
125
+ }
126
+ }
127
+
128
+ if (!anyIndexFound) {
129
+ return { tags: {}, found: false };
130
+ }
131
+
132
+ return { tags: tagCounts, found: true };
133
+ }
134
+
135
+ function formatTagsOnly(result) {
136
+ if (!result.found) {
137
+ return "No playbook INDEX found.";
138
+ }
139
+
140
+ const entries = Object.entries(result.tags);
141
+ if (entries.length === 0) {
142
+ return "No tags found.";
143
+ }
144
+
145
+ return entries.map(([tag, count]) => `${tag}(${count})`).join(" ");
146
+ }
147
+
148
+ function formatJson(result) {
149
+ if (!result.found) {
150
+ return JSON.stringify([]);
151
+ }
152
+
153
+ const output = result.entries.map((e) => ({
154
+ id: e.id,
155
+ scope: e.scope,
156
+ type: e.type,
157
+ title: e.title,
158
+ tags: e.tags,
159
+ path: e.path,
160
+ }));
161
+
162
+ return JSON.stringify(output, null, 2);
163
+ }
164
+
165
+ module.exports = {
166
+ queryIndex,
167
+ aggregateTags,
168
+ formatPlainText,
169
+ formatJson,
170
+ formatTagsOnly,
171
+ DEFAULT_LIMIT,
172
+ };
@@ -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
+ };