@kage-core/kage-graph-mcp 1.1.15 → 1.1.17
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 +39 -17
- package/dist/index.js +1 -1
- package/dist/kernel.js +75 -13
- package/package.json +3 -4
- package/dist/release.js +0 -121
package/README.md
CHANGED
|
@@ -9,15 +9,29 @@ This package exposes two surfaces:
|
|
|
9
9
|
|
|
10
10
|
## Latest Release
|
|
11
11
|
|
|
12
|
-
`1.1.
|
|
13
|
-
|
|
14
|
-
- `
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
`1.1.17` publishes content-based graph freshness:
|
|
13
|
+
|
|
14
|
+
- `kage pr check` now uses graph input hashes, so push-only operations and
|
|
15
|
+
empty/same-tree commits do not force another refresh while real source,
|
|
16
|
+
approved-memory, or code-index changes still stale generated graph artifacts.
|
|
17
|
+
|
|
18
|
+
`1.1.16` fixes the guarded release helper's npm verification step:
|
|
19
|
+
|
|
20
|
+
- exact-version `npm view` checks now retry with backoff after publish so npm
|
|
21
|
+
registry propagation does not make a successful publish look failed.
|
|
22
|
+
- the release helper is maintainer-only repo tooling: public package metadata no
|
|
23
|
+
longer exposes npm release scripts, and `dist/release.js` is excluded from the
|
|
24
|
+
published tarball.
|
|
25
|
+
|
|
26
|
+
`1.1.15` hardened the npm release path and memory-only review flow:
|
|
27
|
+
|
|
28
|
+
- the source-repo maintainer helper can run the guarded release checks without
|
|
29
|
+
publishing, or push/publish/smoke-test when explicitly invoked from a built
|
|
30
|
+
checkout.
|
|
31
|
+
- it requires a clean worktree, fetches the remote branch, verifies local `HEAD`
|
|
32
|
+
contains `origin/<branch>`, runs tests and `npm pack --dry-run`, pushes the
|
|
33
|
+
branch before publishing, publishes with `--access public`, verifies npm
|
|
34
|
+
registry metadata, and performs a smoke install.
|
|
21
35
|
- all git steps run with `GIT_EDITOR=true` so agent sessions cannot get stuck in
|
|
22
36
|
an interactive commit or rebase editor.
|
|
23
37
|
- `kage propose --from-diff` now includes repo memory packet-only changes from
|
|
@@ -54,20 +68,27 @@ This package exposes two surfaces:
|
|
|
54
68
|
```bash
|
|
55
69
|
npm install
|
|
56
70
|
npm run build
|
|
57
|
-
npm run release:npm:dry-run
|
|
58
71
|
```
|
|
59
72
|
|
|
60
|
-
Publishing from the repo should use the guarded
|
|
61
|
-
commit is ready
|
|
73
|
+
Publishing from the source repo should use the guarded maintainer helper after
|
|
74
|
+
the release commit is ready. It is intentionally not exposed as a public npm
|
|
75
|
+
script or included in the published tarball:
|
|
62
76
|
|
|
63
77
|
```bash
|
|
64
|
-
npm run
|
|
78
|
+
npm run build --prefix mcp
|
|
79
|
+
cd mcp
|
|
80
|
+
node dist/release.js --dry-run
|
|
81
|
+
node dist/release.js --publish --push --smoke
|
|
65
82
|
```
|
|
66
83
|
|
|
67
84
|
The script fetches the current branch and blocks if the remote branch is not an
|
|
68
85
|
ancestor of local `HEAD`, which prevents publishing an npm version from a branch
|
|
69
86
|
that cannot be pushed cleanly.
|
|
70
87
|
|
|
88
|
+
Do not refresh again just because the branch was pushed. Graph freshness is
|
|
89
|
+
based on source, approved memory, and code-index inputs; empty/same-tree commits
|
|
90
|
+
are accepted by `kage pr check`.
|
|
91
|
+
|
|
71
92
|
## CLI
|
|
72
93
|
|
|
73
94
|
```bash
|
|
@@ -221,9 +242,10 @@ hashes, git state, audit trust, inbox counts, and metrics readiness. CI, PR, and
|
|
|
221
242
|
sync workflows build it after refresh.
|
|
222
243
|
|
|
223
244
|
Use `kage refresh --project <repo>` or the `kage_refresh` MCP tool after
|
|
224
|
-
meaningful file changes. Refresh rebuilds indexes, code graph, memory
|
|
225
|
-
metrics, and stale-memory metadata. Memory is marked stale when status or
|
|
226
|
-
feedback says it is stale, its TTL expires, or grounded paths disappear.
|
|
245
|
+
meaningful file/content changes. Refresh rebuilds indexes, code graph, memory
|
|
246
|
+
graph, metrics, and stale-memory metadata. Memory is marked stale when status or
|
|
247
|
+
feedback says it is stale, its TTL expires, or grounded paths disappear. Pushes
|
|
248
|
+
and empty/same-tree commits do not need another refresh.
|
|
227
249
|
|
|
228
250
|
Use `kage gc --project <repo> --dry-run` to preview stale packet cleanup.
|
|
229
251
|
`kage gc --project <repo>` marks stale repo packets deprecated, rebuilds
|
|
@@ -396,7 +418,7 @@ Minimum policy:
|
|
|
396
418
|
Before code changes or repo-specific answers:
|
|
397
419
|
1. Call `kage_context` with `project_dir` and the user task as `query`.
|
|
398
420
|
2. Capture reusable learnings with `kage_learn` or `kage_capture`.
|
|
399
|
-
3. After meaningful file changes, call `kage_refresh
|
|
421
|
+
3. After meaningful file/content changes, call `kage_refresh`; skip it for push-only or same-tree commits.
|
|
400
422
|
4. Before finishing changed-file tasks, call `kage_propose_from_diff` or `kage_pr_summarize`.
|
|
401
423
|
5. Before merge, call `kage_pr_check`.
|
|
402
424
|
6. Never publish or promote org/global memory automatically.
|
package/dist/index.js
CHANGED
|
@@ -217,7 +217,7 @@ function listTools() {
|
|
|
217
217
|
},
|
|
218
218
|
{
|
|
219
219
|
name: "kage_refresh",
|
|
220
|
-
description: "Rebuild repo indexes, code graph, memory graph, metrics, and stale-memory metadata. Agents should run this after meaningful file changes
|
|
220
|
+
description: "Rebuild repo indexes, code graph, memory graph, metrics, and stale-memory metadata. Agents should run this after meaningful file/content changes before PR checks; push-only or same-tree commits do not need another refresh.",
|
|
221
221
|
inputSchema: {
|
|
222
222
|
type: "object",
|
|
223
223
|
properties: {
|
package/dist/kernel.js
CHANGED
|
@@ -193,8 +193,10 @@ decisions, debugging, explanation, or action. Do not store raw transcripts.
|
|
|
193
193
|
|
|
194
194
|
## End-Of-Task Proposal
|
|
195
195
|
|
|
196
|
-
After meaningful file changes, call \`kage_refresh\` so indexes, code
|
|
197
|
-
memory graph, metrics, and stale-memory checks are current.
|
|
196
|
+
After meaningful file/content changes, call \`kage_refresh\` so indexes, code
|
|
197
|
+
graph, memory graph, metrics, and stale-memory checks are current. Do not
|
|
198
|
+
refresh solely because a branch was pushed, an empty commit was created, or the
|
|
199
|
+
git commit changed without graph inputs changing.
|
|
198
200
|
|
|
199
201
|
Before finishing a task that changed files, call \`kage_pr_summarize\` or
|
|
200
202
|
\`kage_propose_from_diff\`, then call \`kage_pr_check\`.
|
|
@@ -232,7 +234,7 @@ For normal coding tasks:
|
|
|
232
234
|
1. \`kage_context\` — validate + recall + code graph + knowledge graph in one call
|
|
233
235
|
2. Work on the task
|
|
234
236
|
3. \`kage_learn\` for concrete learnings
|
|
235
|
-
4. \`kage_refresh\` after meaningful file changes
|
|
237
|
+
4. \`kage_refresh\` after meaningful file/content changes, not after push-only or same-tree commits
|
|
236
238
|
5. \`kage_propose_from_diff\` before the final response to create repo-local change memory
|
|
237
239
|
|
|
238
240
|
For quick factual questions, \`kage_context\` alone is enough. For status or demo requests, call \`kage_metrics\`.
|
|
@@ -882,6 +884,9 @@ function gitBranch(projectDir) {
|
|
|
882
884
|
function gitHead(projectDir) {
|
|
883
885
|
return readGit(projectDir, ["rev-parse", "HEAD"]);
|
|
884
886
|
}
|
|
887
|
+
function gitTree(projectDir) {
|
|
888
|
+
return readGit(projectDir, ["rev-parse", "HEAD^{tree}"]);
|
|
889
|
+
}
|
|
885
890
|
function gitMergeBase(projectDir) {
|
|
886
891
|
return readGit(projectDir, ["merge-base", "HEAD", "origin/main"])
|
|
887
892
|
|| readGit(projectDir, ["merge-base", "HEAD", "origin/master"]);
|
|
@@ -1863,6 +1868,49 @@ function externalIndexFiles(projectDir) {
|
|
|
1863
1868
|
{ path: (0, node_path_1.join)(projectDir, "dump.lsif"), parser: "lsif", format: "lsif" },
|
|
1864
1869
|
];
|
|
1865
1870
|
}
|
|
1871
|
+
function sha256Hex(content) {
|
|
1872
|
+
return (0, node_crypto_1.createHash)("sha256").update(content).digest("hex");
|
|
1873
|
+
}
|
|
1874
|
+
function projectRelative(projectDir, path) {
|
|
1875
|
+
return (0, node_path_1.relative)(projectDir, path).replace(/\\/g, "/");
|
|
1876
|
+
}
|
|
1877
|
+
function graphInputHash(entries) {
|
|
1878
|
+
const hash = (0, node_crypto_1.createHash)("sha256");
|
|
1879
|
+
const sorted = entries.slice().sort((a, b) => a.kind.localeCompare(b.kind) || a.path.localeCompare(b.path));
|
|
1880
|
+
for (const entry of sorted) {
|
|
1881
|
+
hash.update(entry.kind);
|
|
1882
|
+
hash.update("\0");
|
|
1883
|
+
hash.update(entry.path);
|
|
1884
|
+
hash.update("\0");
|
|
1885
|
+
hash.update(entry.sha256);
|
|
1886
|
+
hash.update("\0");
|
|
1887
|
+
}
|
|
1888
|
+
return hash.digest("hex");
|
|
1889
|
+
}
|
|
1890
|
+
function fileInputEntries(projectDir, paths, kind) {
|
|
1891
|
+
return paths
|
|
1892
|
+
.filter((path) => (0, node_fs_1.existsSync)(path))
|
|
1893
|
+
.map((path) => ({
|
|
1894
|
+
kind,
|
|
1895
|
+
path: projectRelative(projectDir, path),
|
|
1896
|
+
sha256: sha256Hex((0, node_fs_1.readFileSync)(path)),
|
|
1897
|
+
}));
|
|
1898
|
+
}
|
|
1899
|
+
function codeGraphInputHash(projectDir) {
|
|
1900
|
+
return graphInputHash([
|
|
1901
|
+
...fileInputEntries(projectDir, listCodeFiles(projectDir), "code_file"),
|
|
1902
|
+
...fileInputEntries(projectDir, externalIndexFiles(projectDir).map((index) => index.path), "external_code_index"),
|
|
1903
|
+
]);
|
|
1904
|
+
}
|
|
1905
|
+
function knowledgeGraphInputHash(projectDir, codeInputHash = codeGraphInputHash(projectDir)) {
|
|
1906
|
+
const packetEntries = loadPacketEntriesFromDir(packetsDir(projectDir))
|
|
1907
|
+
.filter((entry) => entry.packet.status === "approved")
|
|
1908
|
+
.map((entry) => entry.path);
|
|
1909
|
+
return graphInputHash([
|
|
1910
|
+
{ kind: "code_graph_input", path: ".agent_memory/code_graph/input", sha256: codeInputHash },
|
|
1911
|
+
...fileInputEntries(projectDir, packetEntries, "approved_packet"),
|
|
1912
|
+
]);
|
|
1913
|
+
}
|
|
1866
1914
|
function normalizeExternalKind(value) {
|
|
1867
1915
|
const kind = String(value ?? "").toLowerCase();
|
|
1868
1916
|
if (["function", "method", "class", "constant", "route", "test"].includes(kind))
|
|
@@ -2103,7 +2151,9 @@ function buildCodeGraph(projectDir) {
|
|
|
2103
2151
|
ensureMemoryDirs(projectDir);
|
|
2104
2152
|
const branch = gitBranch(projectDir);
|
|
2105
2153
|
const head = gitHead(projectDir);
|
|
2154
|
+
const tree = gitTree(projectDir);
|
|
2106
2155
|
const mergeBase = gitMergeBase(projectDir);
|
|
2156
|
+
const inputHash = codeGraphInputHash(projectDir);
|
|
2107
2157
|
const absoluteFiles = listCodeFiles(projectDir);
|
|
2108
2158
|
const knownFiles = new Set(absoluteFiles.map((path) => (0, node_path_1.relative)(projectDir, path).replace(/\\/g, "/")));
|
|
2109
2159
|
const files = [];
|
|
@@ -2188,7 +2238,7 @@ function buildCodeGraph(projectDir) {
|
|
|
2188
2238
|
project_dir: projectDir,
|
|
2189
2239
|
repo_key: repoKey(projectDir),
|
|
2190
2240
|
generated_at: nowIso(),
|
|
2191
|
-
repo_state: { branch, head, merge_base: mergeBase },
|
|
2241
|
+
repo_state: { branch, head, merge_base: mergeBase, tree, input_hash: inputHash },
|
|
2192
2242
|
files: files.sort((a, b) => a.path.localeCompare(b.path)),
|
|
2193
2243
|
symbols: symbols.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line || a.name.localeCompare(b.name)),
|
|
2194
2244
|
imports: imports.sort((a, b) => a.from_path.localeCompare(b.from_path) || a.line - b.line || a.specifier.localeCompare(b.specifier)),
|
|
@@ -2212,6 +2262,7 @@ function buildKnowledgeGraph(projectDir) {
|
|
|
2212
2262
|
const packets = loadApprovedPackets(projectDir).sort((a, b) => a.id.localeCompare(b.id));
|
|
2213
2263
|
const branch = gitBranch(projectDir);
|
|
2214
2264
|
const head = gitHead(projectDir);
|
|
2265
|
+
const tree = gitTree(projectDir);
|
|
2215
2266
|
const mergeBase = gitMergeBase(projectDir);
|
|
2216
2267
|
const entities = new Map();
|
|
2217
2268
|
const edges = new Map();
|
|
@@ -2219,6 +2270,7 @@ function buildKnowledgeGraph(projectDir) {
|
|
|
2219
2270
|
const repoEntityId = graphEntityId("repo", repoKey(projectDir));
|
|
2220
2271
|
const generatedFrom = packets.map((packet) => packet.updated_at).sort().at(-1) ?? null;
|
|
2221
2272
|
const codeGraph = buildCodeGraph(projectDir);
|
|
2273
|
+
const inputHash = knowledgeGraphInputHash(projectDir, codeGraph.repo_state.input_hash ?? codeGraphInputHash(projectDir));
|
|
2222
2274
|
addEntity(entities, {
|
|
2223
2275
|
id: repoEntityId,
|
|
2224
2276
|
type: "repo",
|
|
@@ -2540,7 +2592,7 @@ function buildKnowledgeGraph(projectDir) {
|
|
|
2540
2592
|
project_dir: projectDir,
|
|
2541
2593
|
repo_key: repoKey(projectDir),
|
|
2542
2594
|
generated_from_updated_at: generatedFrom,
|
|
2543
|
-
repo_state: { branch, head, merge_base: mergeBase },
|
|
2595
|
+
repo_state: { branch, head, merge_base: mergeBase, tree, input_hash: inputHash },
|
|
2544
2596
|
episodes: episodes.sort((a, b) => a.id.localeCompare(b.id)),
|
|
2545
2597
|
entities: [...entities.values()].sort((a, b) => a.id.localeCompare(b.id)),
|
|
2546
2598
|
edges: [...edges.values()].sort((a, b) => a.id.localeCompare(b.id)),
|
|
@@ -4239,7 +4291,7 @@ Before making code changes or answering implementation questions:
|
|
|
4239
4291
|
1. Call kage_context with project_dir and the user task as query.
|
|
4240
4292
|
2. Use returned memory only when it is relevant, source-backed, and not stale.
|
|
4241
4293
|
When you learn something reusable: kage_learn.
|
|
4242
|
-
After meaningful file changes: kage_refresh.
|
|
4294
|
+
After meaningful file/content changes: kage_refresh. Push-only or same-tree commits do not need another refresh.
|
|
4243
4295
|
Before finishing a task that changed files: kage_pr_summarize or kage_propose_from_diff, then kage_pr_check.
|
|
4244
4296
|
If recalled memory helped: kage_feedback helpful. If wrong or stale: kage_feedback wrong or stale."
|
|
4245
4297
|
fi
|
|
@@ -4963,15 +5015,22 @@ function createReviewArtifact(projectDir) {
|
|
|
4963
5015
|
(0, node_fs_1.writeFileSync)(path, `${lines.join("\n").trim()}\n`, "utf8");
|
|
4964
5016
|
return { path, pending: pending.length };
|
|
4965
5017
|
}
|
|
4966
|
-
function graphIsCurrent(projectDir, relativePath,
|
|
5018
|
+
function graphIsCurrent(projectDir, relativePath, expected) {
|
|
4967
5019
|
const path = (0, node_path_1.join)(projectDir, relativePath);
|
|
4968
5020
|
if (!(0, node_fs_1.existsSync)(path))
|
|
4969
5021
|
return false;
|
|
4970
|
-
if (!head)
|
|
4971
|
-
return true;
|
|
4972
5022
|
try {
|
|
4973
5023
|
const graph = readJson(path);
|
|
4974
|
-
|
|
5024
|
+
const repoState = graph.repo_state;
|
|
5025
|
+
if (!repoState)
|
|
5026
|
+
return false;
|
|
5027
|
+
if (expected.inputHash && repoState.input_hash)
|
|
5028
|
+
return repoState.input_hash === expected.inputHash;
|
|
5029
|
+
if (expected.tree && repoState.tree)
|
|
5030
|
+
return repoState.tree === expected.tree;
|
|
5031
|
+
if (!expected.head)
|
|
5032
|
+
return true;
|
|
5033
|
+
return repoState.head === expected.head;
|
|
4975
5034
|
}
|
|
4976
5035
|
catch {
|
|
4977
5036
|
return false;
|
|
@@ -5005,6 +5064,9 @@ function prCheck(projectDir) {
|
|
|
5005
5064
|
const overlay = buildBranchOverlay(projectDir);
|
|
5006
5065
|
const rawStatus = readGit(projectDir, ["status", "--porcelain", "-uall"]) ?? "";
|
|
5007
5066
|
const validation = validateProject(projectDir);
|
|
5067
|
+
const tree = gitTree(projectDir);
|
|
5068
|
+
const codeInputHash = codeGraphInputHash(projectDir);
|
|
5069
|
+
const memoryInputHash = knowledgeGraphInputHash(projectDir, codeInputHash);
|
|
5008
5070
|
const stalePackets = loadPacketsFromDir(packetsDir(projectDir))
|
|
5009
5071
|
.map((packet) => ({ packet, reasons: staleMemoryReasons(projectDir, packet) }))
|
|
5010
5072
|
.filter((entry) => entry.reasons.length)
|
|
@@ -5014,8 +5076,8 @@ function prCheck(projectDir) {
|
|
|
5014
5076
|
.map(parsePorcelainPath)
|
|
5015
5077
|
.map((path) => path.replace(/^.* -> /, ""))
|
|
5016
5078
|
.filter((path) => path.startsWith(".agent_memory/packets/") && path.endsWith(".json"))).sort();
|
|
5017
|
-
const codeGraphCurrent = graphIsCurrent(projectDir, ".agent_memory/code_graph/graph.json", overlay.head);
|
|
5018
|
-
const memoryGraphCurrent = graphIsCurrent(projectDir, ".agent_memory/graph/graph.json", overlay.head);
|
|
5079
|
+
const codeGraphCurrent = graphIsCurrent(projectDir, ".agent_memory/code_graph/graph.json", { head: overlay.head, tree, inputHash: codeInputHash });
|
|
5080
|
+
const memoryGraphCurrent = graphIsCurrent(projectDir, ".agent_memory/graph/graph.json", { head: overlay.head, tree, inputHash: memoryInputHash });
|
|
5019
5081
|
const errors = [...validation.errors];
|
|
5020
5082
|
const warnings = [...validation.warnings];
|
|
5021
5083
|
const requiredActions = [];
|
|
@@ -5024,7 +5086,7 @@ function prCheck(projectDir) {
|
|
|
5024
5086
|
requiredActions.push("Run kage refresh, then update or supersede stale packets.");
|
|
5025
5087
|
}
|
|
5026
5088
|
if (!codeGraphCurrent || !memoryGraphCurrent) {
|
|
5027
|
-
errors.push("Generated graph artifacts are missing or not current for this
|
|
5089
|
+
errors.push("Generated graph artifacts are missing or not current for this working tree content.");
|
|
5028
5090
|
requiredActions.push("Run kage refresh --project <dir> before merge.");
|
|
5029
5091
|
}
|
|
5030
5092
|
if (!memoryPacketChanges.length && overlay.changed_files.some((path) => !path.startsWith(".agent_memory/"))) {
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kage-core/kage-graph-mcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.17",
|
|
4
4
|
"description": "Local-first repo memory, code graph, and recall MCP server for coding agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist/**/*.js",
|
|
8
8
|
"!dist/**/*.test.js",
|
|
9
|
+
"!dist/release.js",
|
|
9
10
|
"viewer/**/*",
|
|
10
11
|
"!viewer/graphs/*.json",
|
|
11
12
|
"README.md"
|
|
@@ -21,9 +22,7 @@
|
|
|
21
22
|
"build": "tsc",
|
|
22
23
|
"start": "node dist/index.js",
|
|
23
24
|
"dev": "ts-node index.ts",
|
|
24
|
-
"test": "npm run build && node --test dist/**/*.test.js"
|
|
25
|
-
"release:npm:dry-run": "npm run build && node dist/release.js --dry-run",
|
|
26
|
-
"release:npm": "npm run build && node dist/release.js --publish --push --smoke"
|
|
25
|
+
"test": "npm run build && node --test dist/**/*.test.js"
|
|
27
26
|
},
|
|
28
27
|
"keywords": [
|
|
29
28
|
"mcp",
|
package/dist/release.js
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseReleaseArgs = parseReleaseArgs;
|
|
4
|
-
exports.buildNpmReleasePlan = buildNpmReleasePlan;
|
|
5
|
-
exports.runNpmRelease = runNpmRelease;
|
|
6
|
-
const node_child_process_1 = require("node:child_process");
|
|
7
|
-
const node_fs_1 = require("node:fs");
|
|
8
|
-
const node_os_1 = require("node:os");
|
|
9
|
-
const node_path_1 = require("node:path");
|
|
10
|
-
const DEFAULT_CACHE = "/private/tmp/kage-npm-cache";
|
|
11
|
-
function parseReleaseArgs(argv) {
|
|
12
|
-
const options = {
|
|
13
|
-
publish: false,
|
|
14
|
-
push: false,
|
|
15
|
-
smoke: false,
|
|
16
|
-
cache: DEFAULT_CACHE,
|
|
17
|
-
};
|
|
18
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
19
|
-
const arg = argv[index];
|
|
20
|
-
if (arg === "--publish")
|
|
21
|
-
options.publish = true;
|
|
22
|
-
else if (arg === "--dry-run")
|
|
23
|
-
options.publish = false;
|
|
24
|
-
else if (arg === "--push")
|
|
25
|
-
options.push = true;
|
|
26
|
-
else if (arg === "--smoke")
|
|
27
|
-
options.smoke = true;
|
|
28
|
-
else if (arg === "--cache") {
|
|
29
|
-
const value = argv[index + 1];
|
|
30
|
-
if (!value)
|
|
31
|
-
throw new Error("--cache requires a path");
|
|
32
|
-
options.cache = value;
|
|
33
|
-
index += 1;
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
throw new Error(`Unknown release option: ${arg}`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return options;
|
|
40
|
-
}
|
|
41
|
-
function buildNpmReleasePlan(context) {
|
|
42
|
-
const gitEnv = { GIT_EDITOR: "true" };
|
|
43
|
-
const steps = [
|
|
44
|
-
{ name: "ensure clean worktree", command: "git", args: ["status", "--porcelain", "-uall"], env: gitEnv, expectEmptyStdout: true },
|
|
45
|
-
{ name: "fetch remote branch", command: "git", args: ["fetch", "origin", context.branch], env: gitEnv },
|
|
46
|
-
{ name: "ensure branch contains remote", command: "git", args: ["merge-base", "--is-ancestor", `origin/${context.branch}`, "HEAD"], env: gitEnv },
|
|
47
|
-
{ name: "run package tests", command: "npm", args: ["test"] },
|
|
48
|
-
{ name: "pack dry run", command: "npm", args: ["--cache", context.cache, "pack", "--dry-run"] },
|
|
49
|
-
];
|
|
50
|
-
if (context.push) {
|
|
51
|
-
steps.push({ name: "push branch", command: "git", args: ["push", "origin", context.branch], env: gitEnv });
|
|
52
|
-
}
|
|
53
|
-
if (context.publish) {
|
|
54
|
-
steps.push({ name: "publish package", command: "npm", args: ["--cache", context.cache, "publish", "--access", "public"] }, { name: "verify npm version", command: "npm", args: ["view", `${context.packageName}@${context.version}`, "version"] });
|
|
55
|
-
if (context.smoke) {
|
|
56
|
-
steps.push({
|
|
57
|
-
name: "smoke install published package",
|
|
58
|
-
command: "npm",
|
|
59
|
-
args: ["--cache", context.cache, "install", "--prefix", smokeInstallDir(context.version), `${context.packageName}@${context.version}`],
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return steps;
|
|
64
|
-
}
|
|
65
|
-
function smokeInstallDir(version) {
|
|
66
|
-
return (0, node_path_1.join)((0, node_os_1.tmpdir)(), `kage-npm-smoke-${version}`);
|
|
67
|
-
}
|
|
68
|
-
function stdout(command, args, cwd) {
|
|
69
|
-
return (0, node_child_process_1.execFileSync)(command, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
70
|
-
}
|
|
71
|
-
function packageMetadata(packageDir) {
|
|
72
|
-
const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(packageDir, "package.json"), "utf8"));
|
|
73
|
-
if (typeof pkg.name !== "string" || typeof pkg.version !== "string") {
|
|
74
|
-
throw new Error("package.json must contain string name and version");
|
|
75
|
-
}
|
|
76
|
-
return { name: pkg.name, version: pkg.version };
|
|
77
|
-
}
|
|
78
|
-
function runStep(step, cwd) {
|
|
79
|
-
console.log(`release:npm: ${step.name}`);
|
|
80
|
-
if (step.expectEmptyStdout) {
|
|
81
|
-
const output = (0, node_child_process_1.execFileSync)(step.command, step.args, {
|
|
82
|
-
cwd,
|
|
83
|
-
encoding: "utf8",
|
|
84
|
-
stdio: ["ignore", "pipe", "inherit"],
|
|
85
|
-
env: { ...process.env, ...step.env },
|
|
86
|
-
}).trim();
|
|
87
|
-
if (output)
|
|
88
|
-
throw new Error(`release:npm: ${step.name} failed because output was not empty:\n${output}`);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
(0, node_child_process_1.execFileSync)(step.command, step.args, {
|
|
92
|
-
cwd,
|
|
93
|
-
stdio: "inherit",
|
|
94
|
-
env: { ...process.env, ...step.env },
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
function runNpmRelease(argv = process.argv.slice(2), packageDir = process.cwd()) {
|
|
98
|
-
const options = parseReleaseArgs(argv);
|
|
99
|
-
const repoRoot = stdout("git", ["rev-parse", "--show-toplevel"], packageDir);
|
|
100
|
-
const branch = stdout("git", ["branch", "--show-current"], repoRoot);
|
|
101
|
-
if (!branch)
|
|
102
|
-
throw new Error("npm release requires a named git branch");
|
|
103
|
-
(0, node_fs_1.mkdirSync)(options.cache, { recursive: true });
|
|
104
|
-
if (options.smoke)
|
|
105
|
-
(0, node_fs_1.mkdirSync)(smokeInstallDir(packageMetadata(packageDir).version), { recursive: true });
|
|
106
|
-
const metadata = packageMetadata(packageDir);
|
|
107
|
-
const plan = buildNpmReleasePlan({ ...options, branch, packageName: metadata.name, version: metadata.version });
|
|
108
|
-
console.log(`release:npm: package ${metadata.name}@${metadata.version}`);
|
|
109
|
-
console.log(`release:npm: mode ${options.publish ? "publish" : "dry-run"}`);
|
|
110
|
-
for (const step of plan)
|
|
111
|
-
runStep(step, repoRoot === (0, node_path_1.resolve)(packageDir, "..") ? packageDir : repoRoot);
|
|
112
|
-
}
|
|
113
|
-
if (process.argv[1] && process.argv[1].endsWith("release.js")) {
|
|
114
|
-
try {
|
|
115
|
-
runNpmRelease();
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
119
|
-
process.exit(2);
|
|
120
|
-
}
|
|
121
|
-
}
|