@zosmaai/pi-llm-wiki 0.1.6 → 0.2.1
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/.github/workflows/ci.yml +7 -6
- package/.github/workflows/release.yml +3 -3
- package/AGENTS.md +57 -0
- package/CHANGELOG.md +30 -0
- package/CONTRIBUTING.md +43 -0
- package/LICENSE +21 -0
- package/README.md +42 -342
- 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 +339 -0
- package/extensions/llm-wiki/lib/tools.ts +932 -0
- package/extensions/llm-wiki/lib/utils.ts +222 -0
- package/package.json +7 -2
- package/prompts/wiki-digest.md +5 -1
- package/prompts/wiki-discover.md +5 -1
- package/prompts/wiki-ingest.md +5 -1
- package/prompts/wiki-init.md +5 -1
- package/prompts/wiki-lint.md +5 -1
- package/prompts/wiki-query.md +5 -1
- package/prompts/wiki-run.md +5 -1
- 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/pages/analysis.md +35 -0
- package/test/llm-wiki.test.ts +51 -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.1
|
|
3
|
+
"version": "0.2.1",
|
|
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,10 +27,14 @@
|
|
|
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
40
|
"extensions": ["./extensions"],
|
|
@@ -49,6 +53,7 @@
|
|
|
49
53
|
"@biomejs/biome": "^1.9.4",
|
|
50
54
|
"@mariozechner/pi-coding-agent": "^0.70.2",
|
|
51
55
|
"@mermaid-js/mermaid-cli": "^11.12.0",
|
|
56
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
52
57
|
"typebox": "^1.1.34",
|
|
53
58
|
"typescript": "^5.7.0",
|
|
54
59
|
"vitest": "^3.0.0"
|
package/prompts/wiki-digest.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Generate a daily or weekly digest of wiki changes — new sources, pages, insights, and gaps.
|
|
3
|
-
|
|
3
|
+
argument-hint: "[--period daily|weekly]"
|
|
4
4
|
section: LLM Wiki
|
|
5
5
|
topLevelCli: true
|
|
6
6
|
---
|
|
@@ -9,6 +9,10 @@ topLevelCli: true
|
|
|
9
9
|
|
|
10
10
|
Generate a digest of recent wiki activity.
|
|
11
11
|
|
|
12
|
+
## User Arguments
|
|
13
|
+
|
|
14
|
+
$ARGUMENTS
|
|
15
|
+
|
|
12
16
|
## Steps
|
|
13
17
|
|
|
14
18
|
1. Read `wiki/LOG.md` — filter entries since last digest
|
package/prompts/wiki-discover.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Auto-discover new sources from the web. Searches based on config topics and known knowledge gaps.
|
|
3
|
-
|
|
3
|
+
argument-hint: "[--topic <topic>]"
|
|
4
4
|
section: LLM Wiki
|
|
5
5
|
topLevelCli: true
|
|
6
6
|
---
|
|
@@ -9,6 +9,10 @@ topLevelCli: true
|
|
|
9
9
|
|
|
10
10
|
Find new source material for the wiki by searching the web.
|
|
11
11
|
|
|
12
|
+
## User Arguments
|
|
13
|
+
|
|
14
|
+
$ARGUMENTS
|
|
15
|
+
|
|
12
16
|
Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first. Also read `config.yaml` for topics and feeds.
|
|
13
17
|
|
|
14
18
|
## Steps
|
package/prompts/wiki-ingest.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Process new source files in raw/ and update the wiki. Creates summaries, entities, concepts, and cross-references.
|
|
3
|
-
|
|
3
|
+
argument-hint: "[path]"
|
|
4
4
|
section: LLM Wiki
|
|
5
5
|
topLevelCli: true
|
|
6
6
|
---
|
|
@@ -9,6 +9,10 @@ topLevelCli: true
|
|
|
9
9
|
|
|
10
10
|
Process new files in `raw/` and integrate them into the wiki.
|
|
11
11
|
|
|
12
|
+
## User Arguments
|
|
13
|
+
|
|
14
|
+
$ARGUMENTS
|
|
15
|
+
|
|
12
16
|
Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first to understand the full schema, page formats, and conventions.
|
|
13
17
|
|
|
14
18
|
## Steps
|
package/prompts/wiki-init.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Initialize a new LLM Wiki in the current directory. Creates the full directory structure, config, and template files.
|
|
3
|
-
|
|
3
|
+
argument-hint: "<topic> [--mode personal|company]"
|
|
4
4
|
section: LLM Wiki
|
|
5
5
|
topLevelCli: true
|
|
6
6
|
---
|
|
@@ -9,6 +9,10 @@ topLevelCli: true
|
|
|
9
9
|
|
|
10
10
|
Initialize a new LLM Wiki in the current directory.
|
|
11
11
|
|
|
12
|
+
## User Arguments
|
|
13
|
+
|
|
14
|
+
$ARGUMENTS
|
|
15
|
+
|
|
12
16
|
Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` (or wherever the skill is installed) first to understand the full schema and conventions.
|
|
13
17
|
|
|
14
18
|
## Steps
|
package/prompts/wiki-lint.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Health check the wiki. Detects contradictions, orphans, missing pages, stale claims, and knowledge gaps.
|
|
3
|
-
|
|
3
|
+
argument-hint: "[--fix]"
|
|
4
4
|
section: LLM Wiki
|
|
5
5
|
topLevelCli: true
|
|
6
6
|
---
|
|
@@ -9,6 +9,10 @@ topLevelCli: true
|
|
|
9
9
|
|
|
10
10
|
Run a comprehensive health check on the wiki.
|
|
11
11
|
|
|
12
|
+
## User Arguments
|
|
13
|
+
|
|
14
|
+
$ARGUMENTS
|
|
15
|
+
|
|
12
16
|
Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first to understand the full schema and conventions.
|
|
13
17
|
|
|
14
18
|
## Steps
|
package/prompts/wiki-query.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Ask questions against the wiki. Synthesizes answers from wiki pages with cross-reference citations.
|
|
3
|
-
|
|
3
|
+
argument-hint: "<question>"
|
|
4
4
|
section: LLM Wiki
|
|
5
5
|
topLevelCli: true
|
|
6
6
|
---
|
|
@@ -9,6 +9,10 @@ topLevelCli: true
|
|
|
9
9
|
|
|
10
10
|
Ask a question and get an answer synthesized from wiki content.
|
|
11
11
|
|
|
12
|
+
## User Question
|
|
13
|
+
|
|
14
|
+
$ARGUMENTS
|
|
15
|
+
|
|
12
16
|
Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first to understand the full schema and conventions.
|
|
13
17
|
|
|
14
18
|
## Steps
|
package/prompts/wiki-run.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Run the full wiki cycle: discover → ingest → lint. Optionally schedule for auto-updates.
|
|
3
|
-
|
|
3
|
+
argument-hint: "[--schedule daily|weekly]"
|
|
4
4
|
section: LLM Wiki
|
|
5
5
|
topLevelCli: true
|
|
6
6
|
---
|
|
@@ -9,6 +9,10 @@ topLevelCli: true
|
|
|
9
9
|
|
|
10
10
|
Run the complete wiki maintenance cycle: discover new sources, ingest them, and lint for health.
|
|
11
11
|
|
|
12
|
+
## User Arguments
|
|
13
|
+
|
|
14
|
+
$ARGUMENTS
|
|
15
|
+
|
|
12
16
|
Read the LLM Wiki skill at `.pi/skills/llm-wiki/SKILL.md` first.
|
|
13
17
|
|
|
14
18
|
## Steps
|
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.`);
|