@zosmaai/pi-llm-wiki 0.1.2 → 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/.coderabbit.yaml +43 -0
- package/.github/workflows/ci.yml +9 -8
- package/.github/workflows/codeql.yml +39 -0
- package/.github/workflows/release.yml +6 -4
- package/AGENTS.md +57 -0
- package/CHANGELOG.md +30 -0
- package/CONTRIBUTING.md +43 -0
- package/LICENSE +21 -0
- package/README.md +44 -344
- package/assets/README.md +3 -3
- package/assets/architecture.md +1 -1
- package/biome.json +3 -0
- package/docs/api.md +105 -0
- package/docs/architecture.md +65 -0
- package/docs/commands.md +51 -0
- package/docs/configuration.md +38 -0
- package/docs/obsidian.md +21 -0
- package/extensions/llm-wiki/index.ts +53 -0
- package/extensions/llm-wiki/lib/guardrails.ts +69 -0
- package/extensions/llm-wiki/lib/metadata.ts +218 -0
- package/extensions/llm-wiki/lib/source-packet.ts +292 -0
- package/extensions/llm-wiki/lib/tools.ts +932 -0
- package/extensions/llm-wiki/lib/utils.ts +222 -0
- package/package.json +11 -12
- package/prompts/wiki-digest.md +1 -1
- package/prompts/wiki-discover.md +2 -2
- package/prompts/wiki-ingest.md +2 -2
- package/prompts/wiki-init.md +2 -2
- package/prompts/wiki-lint.md +1 -1
- package/prompts/wiki-query.md +2 -2
- package/prompts/wiki-run.md +6 -6
- package/prompts/wiki-status.md +1 -1
- package/scripts/release.js +72 -0
- package/skills/llm-wiki/SKILL.md +95 -369
- package/skills/llm-wiki/templates/DASHBOARD.md +2 -2
- package/skills/llm-wiki/templates/pages/analysis.md +35 -0
- package/test/llm-wiki.test.ts +18 -9
- package/extensions/llm-wiki-tools.ts +0 -705
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Vault utility functions for the LLM Wiki extension.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface VaultPaths {
|
|
10
|
+
root: string;
|
|
11
|
+
raw: string;
|
|
12
|
+
rawSources: string;
|
|
13
|
+
wiki: string;
|
|
14
|
+
meta: string;
|
|
15
|
+
dotWiki: string;
|
|
16
|
+
outputs: string;
|
|
17
|
+
discoveries: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Resolve vault root from cwd or find nearest wiki root. */
|
|
21
|
+
export function resolveVaultRoot(cwd: string): string {
|
|
22
|
+
// If cwd has .wiki/config.json, it's the root
|
|
23
|
+
if (existsSync(join(cwd, ".wiki", "config.json"))) return cwd;
|
|
24
|
+
|
|
25
|
+
// Walk up looking for .wiki/config.json
|
|
26
|
+
let dir = cwd;
|
|
27
|
+
while (dir !== dirname(dir)) {
|
|
28
|
+
if (existsSync(join(dir, ".wiki", "config.json"))) return dir;
|
|
29
|
+
dir = dirname(dir);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Fallback: cwd itself
|
|
33
|
+
return cwd;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Get all vault paths. */
|
|
37
|
+
export function getVaultPaths(root: string): VaultPaths {
|
|
38
|
+
return {
|
|
39
|
+
root,
|
|
40
|
+
raw: join(root, "raw"),
|
|
41
|
+
rawSources: join(root, "raw", "sources"),
|
|
42
|
+
wiki: join(root, "wiki"),
|
|
43
|
+
meta: join(root, "meta"),
|
|
44
|
+
dotWiki: join(root, ".wiki"),
|
|
45
|
+
outputs: join(root, "outputs"),
|
|
46
|
+
discoveries: join(root, ".discoveries"),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Ensure all vault directories exist. */
|
|
51
|
+
export function ensureVaultStructure(paths: VaultPaths): void {
|
|
52
|
+
const dirs = [
|
|
53
|
+
paths.rawSources,
|
|
54
|
+
join(paths.raw, "assets"),
|
|
55
|
+
join(paths.wiki, "sources"),
|
|
56
|
+
join(paths.wiki, "entities"),
|
|
57
|
+
join(paths.wiki, "concepts"),
|
|
58
|
+
join(paths.wiki, "syntheses"),
|
|
59
|
+
join(paths.wiki, "analyses"),
|
|
60
|
+
paths.meta,
|
|
61
|
+
paths.dotWiki,
|
|
62
|
+
paths.outputs,
|
|
63
|
+
paths.discoveries,
|
|
64
|
+
join(paths.dotWiki, "templates"),
|
|
65
|
+
join(paths.dotWiki, "templates", "pages"),
|
|
66
|
+
];
|
|
67
|
+
for (const d of dirs) mkdirSync(d, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Read JSON file or return default. */
|
|
71
|
+
export function readJson<T>(path: string, defaultValue: T): T {
|
|
72
|
+
try {
|
|
73
|
+
if (!existsSync(path)) return defaultValue;
|
|
74
|
+
return JSON.parse(readFileSync(path, "utf-8")) as T;
|
|
75
|
+
} catch {
|
|
76
|
+
return defaultValue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Write JSON file atomically. */
|
|
81
|
+
export function writeJson(path: string, data: unknown): void {
|
|
82
|
+
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Read text file or return empty string. */
|
|
86
|
+
export function readText(path: string): string {
|
|
87
|
+
try {
|
|
88
|
+
if (!existsSync(path)) return "";
|
|
89
|
+
return readFileSync(path, "utf-8");
|
|
90
|
+
} catch {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Generate the next source ID. */
|
|
96
|
+
export function nextSourceId(paths: VaultPaths): string {
|
|
97
|
+
const today = new Date().toISOString().split("T")[0];
|
|
98
|
+
const prefix = `SRC-${today}`;
|
|
99
|
+
|
|
100
|
+
if (!existsSync(paths.rawSources)) return `${prefix}-001`;
|
|
101
|
+
|
|
102
|
+
const dirs = readdirSync(paths.rawSources)
|
|
103
|
+
.filter((d) => d.startsWith(prefix))
|
|
104
|
+
.sort();
|
|
105
|
+
|
|
106
|
+
if (dirs.length === 0) return `${prefix}-001`;
|
|
107
|
+
|
|
108
|
+
const last = dirs[dirs.length - 1];
|
|
109
|
+
const num = Number.parseInt(last.slice(-3), 10);
|
|
110
|
+
return `${prefix}-${String(num + 1).padStart(3, "0")}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Extract frontmatter from markdown. */
|
|
114
|
+
export function parseFrontmatter(content: string): {
|
|
115
|
+
frontmatter: Record<string, unknown>;
|
|
116
|
+
body: string;
|
|
117
|
+
} {
|
|
118
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
119
|
+
if (!match) return { frontmatter: {}, body: content };
|
|
120
|
+
|
|
121
|
+
const frontmatter: Record<string, unknown> = {};
|
|
122
|
+
const lines = match[1].split("\n");
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
const idx = line.indexOf(":");
|
|
125
|
+
if (idx > 0) {
|
|
126
|
+
const key = line.slice(0, idx).trim();
|
|
127
|
+
const val = line.slice(idx + 1).trim();
|
|
128
|
+
frontmatter[key] = val;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { frontmatter, body: match[2] };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Find all wiki pages recursively. */
|
|
135
|
+
export function findWikiPages(
|
|
136
|
+
wikiDir: string,
|
|
137
|
+
): Array<{ path: string; relative: string; content: string }> {
|
|
138
|
+
const results: Array<{ path: string; relative: string; content: string }> = [];
|
|
139
|
+
|
|
140
|
+
function walk(dir: string, rel: string) {
|
|
141
|
+
if (!existsSync(dir)) return;
|
|
142
|
+
for (const entry of readdirSync(dir)) {
|
|
143
|
+
const full = join(dir, entry);
|
|
144
|
+
const stat = statSync(full);
|
|
145
|
+
if (stat.isDirectory()) {
|
|
146
|
+
walk(full, rel ? `${rel}/${entry}` : entry);
|
|
147
|
+
} else if (entry.endsWith(".md")) {
|
|
148
|
+
results.push({
|
|
149
|
+
path: full,
|
|
150
|
+
relative: rel ? `${rel}/${entry.slice(0, -3)}` : entry.slice(0, -3),
|
|
151
|
+
content: readFileSync(full, "utf-8"),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
walk(wikiDir, "");
|
|
158
|
+
return results;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Extract all [[wikilinks]] from content. */
|
|
162
|
+
export function extractWikilinks(content: string): string[] {
|
|
163
|
+
const links: string[] = [];
|
|
164
|
+
const regex = /\[\[([^\]|]+)(?:\|[^\]]*)?\]\]/g;
|
|
165
|
+
let m: RegExpExecArray | null = regex.exec(content);
|
|
166
|
+
while (m !== null) {
|
|
167
|
+
links.push(m[1]);
|
|
168
|
+
m = regex.exec(content);
|
|
169
|
+
}
|
|
170
|
+
return links;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Slugify a title. */
|
|
174
|
+
export function slugify(title: string): string {
|
|
175
|
+
return title
|
|
176
|
+
.toLowerCase()
|
|
177
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
178
|
+
.trim()
|
|
179
|
+
.replace(/\s+/g, "-")
|
|
180
|
+
.slice(0, 80);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Format date as YYYY-MM-DD. */
|
|
184
|
+
export function fmtDate(d = new Date()): string {
|
|
185
|
+
return d.toISOString().split("T")[0];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Run a shell command via pi.exec. */
|
|
189
|
+
export async function exec(
|
|
190
|
+
pi: ExtensionAPI,
|
|
191
|
+
command: string,
|
|
192
|
+
args: string[],
|
|
193
|
+
options?: { signal?: AbortSignal; timeout?: number; cwd?: string },
|
|
194
|
+
): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
195
|
+
const result = await pi.exec(command, args, options ?? {});
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Check if a path is inside a protected directory. */
|
|
200
|
+
export function isProtectedPath(
|
|
201
|
+
absPath: string,
|
|
202
|
+
root: string,
|
|
203
|
+
): { protected: boolean; reason?: string } {
|
|
204
|
+
const rawPath = resolve(root, "raw");
|
|
205
|
+
const metaPath = resolve(root, "meta");
|
|
206
|
+
const norm = resolve(absPath);
|
|
207
|
+
|
|
208
|
+
if (norm.startsWith(`${rawPath}/`) || norm === rawPath) {
|
|
209
|
+
return {
|
|
210
|
+
protected: true,
|
|
211
|
+
reason: "Raw sources are immutable. Use wiki_capture_source to add sources.",
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (norm.startsWith(`${metaPath}/`) || norm === metaPath) {
|
|
215
|
+
return {
|
|
216
|
+
protected: true,
|
|
217
|
+
reason: "Metadata is auto-generated. Use wiki_rebuild_meta or wiki_log_event instead.",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { protected: false };
|
|
222
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zosmaai/pi-llm-wiki",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "LLM Wiki for Pi — self-maintaining knowledge base following Karpathy's pattern. Obsidian-friendly, auto-updating, personal & company wiki.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -27,22 +27,20 @@
|
|
|
27
27
|
"scripts": {
|
|
28
28
|
"test": "vitest run",
|
|
29
29
|
"test:watch": "vitest",
|
|
30
|
+
"test:coverage": "vitest run --coverage",
|
|
30
31
|
"typecheck": "tsc --noEmit",
|
|
31
32
|
"lint": "biome check .",
|
|
32
33
|
"lint:fix": "biome check --apply .",
|
|
33
|
-
"
|
|
34
|
+
"release:patch": "node scripts/release.js patch",
|
|
35
|
+
"release:minor": "node scripts/release.js minor",
|
|
36
|
+
"release:major": "node scripts/release.js major",
|
|
37
|
+
"release:push": "git push origin main --tags"
|
|
34
38
|
},
|
|
35
39
|
"pi": {
|
|
36
|
-
"extensions": [
|
|
37
|
-
|
|
38
|
-
],
|
|
39
|
-
"
|
|
40
|
-
"./skills"
|
|
41
|
-
],
|
|
42
|
-
"prompts": [
|
|
43
|
-
"./prompts"
|
|
44
|
-
],
|
|
45
|
-
"image": "https://raw.githubusercontent.com/zosmaai/pi-llm-wiki/master/assets/screenshot.png"
|
|
40
|
+
"extensions": ["./extensions"],
|
|
41
|
+
"skills": ["./skills"],
|
|
42
|
+
"prompts": ["./prompts"],
|
|
43
|
+
"image": "https://raw.githubusercontent.com/zosmaai/pi-llm-wiki/main/assets/screenshot.png"
|
|
46
44
|
},
|
|
47
45
|
"peerDependencies": {
|
|
48
46
|
"@mariozechner/pi-coding-agent": "*",
|
|
@@ -55,6 +53,7 @@
|
|
|
55
53
|
"@biomejs/biome": "^1.9.4",
|
|
56
54
|
"@mariozechner/pi-coding-agent": "^0.70.2",
|
|
57
55
|
"@mermaid-js/mermaid-cli": "^11.12.0",
|
|
56
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
58
57
|
"typebox": "^1.1.34",
|
|
59
58
|
"typescript": "^5.7.0",
|
|
60
59
|
"vitest": "^3.0.0"
|
package/prompts/wiki-digest.md
CHANGED
package/prompts/wiki-discover.md
CHANGED
|
@@ -5,7 +5,7 @@ section: LLM Wiki
|
|
|
5
5
|
topLevelCli: true
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# /wiki
|
|
8
|
+
# /wiki-discover
|
|
9
9
|
|
|
10
10
|
Find new source material for the wiki by searching the web.
|
|
11
11
|
|
|
@@ -24,6 +24,6 @@ Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first. Also read `conf
|
|
|
24
24
|
a. Fetch full content
|
|
25
25
|
b. Save to `raw/articles/YYYY-MM-DD-slug.md` with frontmatter (title, url, discovered, topic)
|
|
26
26
|
6. Update `.discoveries/history.json`
|
|
27
|
-
7. Report: "Discovered [N] new sources. Run `/wiki
|
|
27
|
+
7. Report: "Discovered [N] new sources. Run `/wiki-ingest` to process them."
|
|
28
28
|
|
|
29
29
|
**Rules:** Max 5-10 sources. Skip ads, listicles, duplicates. Prefer in-depth analysis.
|
package/prompts/wiki-ingest.md
CHANGED
|
@@ -5,7 +5,7 @@ section: LLM Wiki
|
|
|
5
5
|
topLevelCli: true
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# /wiki
|
|
8
|
+
# /wiki-ingest
|
|
9
9
|
|
|
10
10
|
Process new files in `raw/` and integrate them into the wiki.
|
|
11
11
|
|
|
@@ -14,7 +14,7 @@ Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first to understand th
|
|
|
14
14
|
## Steps
|
|
15
15
|
|
|
16
16
|
1. Read `config.yaml` and `.discoveries/history.json`
|
|
17
|
-
2. If a specific path is given (e.g., `/wiki
|
|
17
|
+
2. If a specific path is given (e.g., `/wiki-ingest raw/articles/my-file.md`), process just that file
|
|
18
18
|
3. If no path given, scan all files in `raw/` and find ones not in history
|
|
19
19
|
4. For each new source:
|
|
20
20
|
a. Read the full content
|
package/prompts/wiki-init.md
CHANGED
|
@@ -5,7 +5,7 @@ section: LLM Wiki
|
|
|
5
5
|
topLevelCli: true
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# /wiki
|
|
8
|
+
# /wiki-init
|
|
9
9
|
|
|
10
10
|
Initialize a new LLM Wiki in the current directory.
|
|
11
11
|
|
|
@@ -25,6 +25,6 @@ Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` (or wherever the skill
|
|
|
25
25
|
6. Create `wiki/DASHBOARD.md` with Dataview queries for Obsidian
|
|
26
26
|
7. Create `.gitignore` to exclude `outputs/` from version control if desired
|
|
27
27
|
8. Initialize git repo if not already present
|
|
28
|
-
9. Report the structure and suggest first steps: "Drop sources into `raw/` and run `/wiki
|
|
28
|
+
9. Report the structure and suggest first steps: "Drop sources into `raw/` and run `/wiki-ingest`"
|
|
29
29
|
|
|
30
30
|
If `--mode company`, add the `change_detection: true` flag to config.yaml and add a `wiki/decisions/` folder.
|
package/prompts/wiki-lint.md
CHANGED
package/prompts/wiki-query.md
CHANGED
|
@@ -5,7 +5,7 @@ section: LLM Wiki
|
|
|
5
5
|
topLevelCli: true
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# /wiki
|
|
8
|
+
# /wiki-query
|
|
9
9
|
|
|
10
10
|
Ask a question and get an answer synthesized from wiki content.
|
|
11
11
|
|
|
@@ -24,7 +24,7 @@ Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first to understand th
|
|
|
24
24
|
**Example:**
|
|
25
25
|
|
|
26
26
|
```
|
|
27
|
-
/wiki
|
|
27
|
+
/wiki-query What are the key differences between RAG and LLM Wiki?
|
|
28
28
|
→ Reads INDEX.md, finds pages on RAG and LLM Wiki patterns
|
|
29
29
|
→ Reads both pages
|
|
30
30
|
→ Synthesizes a comparison table with [[wikilink]] citations
|
package/prompts/wiki-run.md
CHANGED
|
@@ -5,7 +5,7 @@ section: LLM Wiki
|
|
|
5
5
|
topLevelCli: true
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# /wiki
|
|
8
|
+
# /wiki-run
|
|
9
9
|
|
|
10
10
|
Run the complete wiki maintenance cycle: discover new sources, ingest them, and lint for health.
|
|
11
11
|
|
|
@@ -13,9 +13,9 @@ Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first.
|
|
|
13
13
|
|
|
14
14
|
## Steps
|
|
15
15
|
|
|
16
|
-
1. Run `/wiki
|
|
17
|
-
2. Run `/wiki
|
|
18
|
-
3. Run `/wiki
|
|
16
|
+
1. Run `/wiki-discover` → find new sources
|
|
17
|
+
2. Run `/wiki-ingest` → process all new files
|
|
18
|
+
3. Run `/wiki-lint` → health check
|
|
19
19
|
4. If critical gaps found → optionally one more discover+ingest cycle
|
|
20
20
|
5. Save summary → `outputs/run-YYYY-MM-DD.md`
|
|
21
21
|
6. Report final summary
|
|
@@ -25,11 +25,11 @@ Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first.
|
|
|
25
25
|
If `--schedule daily` is used, use `schedule_prompt` to set up daily runs:
|
|
26
26
|
|
|
27
27
|
```
|
|
28
|
-
schedule_prompt action=add schedule="0 0 8 * * *" prompt="Run /wiki
|
|
28
|
+
schedule_prompt action=add schedule="0 0 8 * * *" prompt="Run /wiki-run for the LLM Wiki"
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
If `--schedule weekly` is used:
|
|
32
32
|
|
|
33
33
|
```
|
|
34
|
-
schedule_prompt action=add schedule="0 0 9 * * 1" prompt="Run /wiki
|
|
34
|
+
schedule_prompt action=add schedule="0 0 9 * * 1" prompt="Run /wiki-run for the LLM Wiki"
|
|
35
35
|
```
|
package/prompts/wiki-status.md
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Semantic version release script.
|
|
4
|
+
* Usage: node scripts/release.js [patch|minor|major]
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { execSync } = require("node:child_process");
|
|
8
|
+
const fs = require("node:fs");
|
|
9
|
+
const path = require("node:path");
|
|
10
|
+
|
|
11
|
+
const bump = process.argv[2];
|
|
12
|
+
if (!["patch", "minor", "major"].includes(bump)) {
|
|
13
|
+
console.error("Usage: node scripts/release.js [patch|minor|major]");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Verify clean tree
|
|
18
|
+
const status = execSync("git status --porcelain", { encoding: "utf-8" }).trim();
|
|
19
|
+
if (status) {
|
|
20
|
+
console.error("Error: working tree is not clean");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Verify main branch
|
|
25
|
+
const branch = execSync("git branch --show-current", { encoding: "utf-8" }).trim();
|
|
26
|
+
if (branch !== "main") {
|
|
27
|
+
console.error("Error: not on main branch");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Run checks
|
|
32
|
+
execSync("npm run typecheck", { stdio: "inherit" });
|
|
33
|
+
execSync("npm run lint", { stdio: "inherit" });
|
|
34
|
+
execSync("npm test", { stdio: "inherit" });
|
|
35
|
+
|
|
36
|
+
// Read current version
|
|
37
|
+
const pkgPath = path.join(__dirname, "..", "package.json");
|
|
38
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
39
|
+
const current = pkg.version;
|
|
40
|
+
const [major, minor, patch] = current.split(".").map(Number);
|
|
41
|
+
|
|
42
|
+
let next;
|
|
43
|
+
if (bump === "major") next = `${major + 1}.0.0`;
|
|
44
|
+
else if (bump === "minor") next = `${major}.${minor + 1}.0`;
|
|
45
|
+
else next = `${major}.${minor}.${patch + 1}`;
|
|
46
|
+
|
|
47
|
+
// Update package.json
|
|
48
|
+
pkg.version = next;
|
|
49
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
|
|
50
|
+
|
|
51
|
+
// Update CHANGELOG
|
|
52
|
+
const changelogPath = path.join(__dirname, "..", "CHANGELOG.md");
|
|
53
|
+
let changelog = "";
|
|
54
|
+
if (fs.existsSync(changelogPath)) {
|
|
55
|
+
changelog = fs.readFileSync(changelogPath, "utf-8");
|
|
56
|
+
}
|
|
57
|
+
const today = new Date().toISOString().split("T")[0];
|
|
58
|
+
const newSection = `## [${next}] - ${today}\n\n### Added\n- Release ${next}\n`;
|
|
59
|
+
if (changelog.includes("## [Unreleased]")) {
|
|
60
|
+
changelog = changelog.replace("## [Unreleased]", `## [Unreleased]\n\n${newSection}`);
|
|
61
|
+
} else {
|
|
62
|
+
changelog = `# Changelog\n\n## [Unreleased]\n\n${newSection}\n${changelog.replace("# Changelog\n\n", "")}`;
|
|
63
|
+
}
|
|
64
|
+
fs.writeFileSync(changelogPath, changelog, "utf-8");
|
|
65
|
+
|
|
66
|
+
// Commit and tag
|
|
67
|
+
execSync("git add package.json CHANGELOG.md", { stdio: "inherit" });
|
|
68
|
+
execSync(`git commit -m "chore(release): v${next}"`, { stdio: "inherit" });
|
|
69
|
+
execSync(`git tag v${next}`, { stdio: "inherit" });
|
|
70
|
+
|
|
71
|
+
console.log(`\n✅ Released v${next}`);
|
|
72
|
+
console.log(`Run "npm run release:push" to publish.`);
|