projectstore-codex 0.0.1 → 0.28.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/.codex-plugin/plugin.json +48 -0
- package/README.md +15 -7
- package/bin/projectstore-codex.mjs +88 -0
- package/hooks/hooks.json +59 -0
- package/node_modules/projectstore/.claude-plugin/marketplace.json +40 -0
- package/node_modules/projectstore/.claude-plugin/plugin.json +23 -0
- package/node_modules/projectstore/.mcp.json +14 -0
- package/node_modules/projectstore/AGENTS.md +26 -0
- package/node_modules/projectstore/LICENSE +21 -0
- package/node_modules/projectstore/README.md +284 -0
- package/node_modules/projectstore/agents/archaeologist.md +76 -0
- package/node_modules/projectstore/agents/clerk.md +93 -0
- package/node_modules/projectstore/agents/critic.md +94 -0
- package/node_modules/projectstore/agents/librarian.md +81 -0
- package/node_modules/projectstore/agents/planner.md +80 -0
- package/node_modules/projectstore/agents/reviewer.md +98 -0
- package/node_modules/projectstore/bin/projectstore.mjs +7 -0
- package/node_modules/projectstore/commands/adr.md +57 -0
- package/node_modules/projectstore/commands/agents.md +180 -0
- package/node_modules/projectstore/commands/bind.md +128 -0
- package/node_modules/projectstore/commands/codemap.md +50 -0
- package/node_modules/projectstore/commands/concept.md +17 -0
- package/node_modules/projectstore/commands/doctor.md +166 -0
- package/node_modules/projectstore/commands/epic.md +40 -0
- package/node_modules/projectstore/commands/graph.md +56 -0
- package/node_modules/projectstore/commands/kanban.md +40 -0
- package/node_modules/projectstore/commands/meeting.md +17 -0
- package/node_modules/projectstore/commands/reconcile.md +73 -0
- package/node_modules/projectstore/commands/research.md +17 -0
- package/node_modules/projectstore/commands/review.md +89 -0
- package/node_modules/projectstore/commands/runbook.md +17 -0
- package/node_modules/projectstore/commands/scaffold.md +23 -0
- package/node_modules/projectstore/commands/search.md +22 -0
- package/node_modules/projectstore/commands/spec.md +91 -0
- package/node_modules/projectstore/commands/status.md +27 -0
- package/node_modules/projectstore/commands/statusline.md +46 -0
- package/node_modules/projectstore/commands/story.md +113 -0
- package/node_modules/projectstore/docs/extending.md +172 -0
- package/node_modules/projectstore/docs/getting-started.md +133 -0
- package/node_modules/projectstore/docs/harnesses.md +163 -0
- package/node_modules/projectstore/docs/how-it-works.md +263 -0
- package/node_modules/projectstore/docs/images/loop-light.svg +94 -0
- package/node_modules/projectstore/docs/images/loop.svg +93 -0
- package/node_modules/projectstore/docs/images/statusline-hud.png +0 -0
- package/node_modules/projectstore/docs/images/team-light.svg +79 -0
- package/node_modules/projectstore/docs/images/team.svg +79 -0
- package/node_modules/projectstore/harnesses/claude-code.json +483 -0
- package/node_modules/projectstore/harnesses/codex.json +332 -0
- package/node_modules/projectstore/hooks/hooks.json +59 -0
- package/node_modules/projectstore/hooks/pre-compact.mjs +121 -0
- package/node_modules/projectstore/hooks/session-rules.mjs +63 -0
- package/node_modules/projectstore/hooks/session-start.mjs +301 -0
- package/node_modules/projectstore/hooks/session-stop.mjs +84 -0
- package/node_modules/projectstore/package.json +70 -0
- package/node_modules/projectstore/scaffold/checklists.json +88 -0
- package/node_modules/projectstore/scaffold/headings.json +171 -0
- package/node_modules/projectstore/scaffold/layouts/engineering.json +85 -0
- package/node_modules/projectstore/scripts/binding.mjs +165 -0
- package/node_modules/projectstore/scripts/build-adapters.mjs +264 -0
- package/node_modules/projectstore/scripts/cli.mjs +595 -0
- package/node_modules/projectstore/scripts/codemap.mjs +99 -0
- package/node_modules/projectstore/scripts/diff-refs.mjs +127 -0
- package/node_modules/projectstore/scripts/doctor.mjs +2127 -0
- package/node_modules/projectstore/scripts/draft.mjs +261 -0
- package/node_modules/projectstore/scripts/graph.mjs +219 -0
- package/node_modules/projectstore/scripts/harness.mjs +608 -0
- package/node_modules/projectstore/scripts/install-harness.mjs +1387 -0
- package/node_modules/projectstore/scripts/kanban.mjs +174 -0
- package/node_modules/projectstore/scripts/lib.mjs +3085 -0
- package/node_modules/projectstore/scripts/mcp.mjs +391 -0
- package/node_modules/projectstore/scripts/portable-registration.mjs +198 -0
- package/node_modules/projectstore/scripts/provenance.mjs +375 -0
- package/node_modules/projectstore/scripts/query.mjs +490 -0
- package/node_modules/projectstore/scripts/reconcile.mjs +422 -0
- package/node_modules/projectstore/scripts/statusline-launcher.mjs +141 -0
- package/node_modules/projectstore/scripts/statusline.mjs +253 -0
- package/node_modules/projectstore/scripts/story-section.mjs +209 -0
- package/node_modules/projectstore/scripts/surfaces.mjs +421 -0
- package/node_modules/projectstore/scripts/tokens.mjs +449 -0
- package/node_modules/projectstore/scripts/touch-session.mjs +336 -0
- package/node_modules/projectstore/scripts/version-guard.mjs +255 -0
- package/node_modules/projectstore/scripts/worktree.mjs +109 -0
- package/node_modules/projectstore/skills/projectstore-decision-detector/SKILL.md +40 -0
- package/node_modules/projectstore/skills/projectstore-peer-reviewer/SKILL.md +38 -0
- package/node_modules/projectstore/skills/projectstore-story-completion/SKILL.md +50 -0
- package/node_modules/projectstore/skills/projectstore-vault-communication/SKILL.md +96 -0
- package/node_modules/projectstore/templates/claude-md-block.md.tmpl +26 -0
- package/node_modules/projectstore/templates/de/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/de/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/de/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/de/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/de/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/de/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/de/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/de/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/de/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/de/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/de/strings.json +6 -0
- package/node_modules/projectstore/templates/en/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/en/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/en/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/en/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/en/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/en/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/en/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/en/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/en/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/en/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/en/strings.json +6 -0
- package/node_modules/projectstore/templates/es/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/es/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/es/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/es/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/es/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/es/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/es/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/es/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/es/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/es/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/es/strings.json +6 -0
- package/node_modules/projectstore/templates/fr/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/fr/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/fr/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/fr/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/fr/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/fr/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/fr/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/fr/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/fr/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/fr/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/fr/strings.json +6 -0
- package/node_modules/projectstore/templates/ru/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/ru/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/ru/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/ru/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/ru/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/ru/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/ru/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/ru/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/ru/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/ru/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/ru/strings.json +6 -0
- package/node_modules/projectstore/templates/zh/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/zh/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/zh/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/zh/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/zh/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/zh/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/zh/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/zh/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/zh/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/zh/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/zh/strings.json +6 -0
- package/package.json +36 -14
- package/plugin.json +53 -0
- package/skills/projectstore-adr/SKILL.md +76 -0
- package/skills/projectstore-agents/SKILL.md +50 -0
- package/skills/projectstore-archaeologist/SKILL.md +109 -0
- package/skills/projectstore-bind/SKILL.md +44 -0
- package/skills/projectstore-clerk/SKILL.md +126 -0
- package/skills/projectstore-codemap/SKILL.md +69 -0
- package/skills/projectstore-concept/SKILL.md +36 -0
- package/skills/projectstore-critic/SKILL.md +127 -0
- package/skills/projectstore-decision-detector/SKILL.md +59 -0
- package/skills/projectstore-doctor/SKILL.md +33 -0
- package/skills/projectstore-epic/SKILL.md +59 -0
- package/skills/projectstore-graph/SKILL.md +75 -0
- package/skills/projectstore-kanban/SKILL.md +60 -0
- package/skills/projectstore-librarian/SKILL.md +114 -0
- package/skills/projectstore-meeting/SKILL.md +36 -0
- package/skills/projectstore-peer-reviewer/SKILL.md +57 -0
- package/skills/projectstore-planner/SKILL.md +113 -0
- package/skills/projectstore-reconcile/SKILL.md +92 -0
- package/skills/projectstore-research/SKILL.md +36 -0
- package/skills/projectstore-review/SKILL.md +108 -0
- package/skills/projectstore-reviewer/SKILL.md +131 -0
- package/skills/projectstore-runbook/SKILL.md +36 -0
- package/skills/projectstore-scaffold/SKILL.md +42 -0
- package/skills/projectstore-search/SKILL.md +41 -0
- package/skills/projectstore-spec/SKILL.md +110 -0
- package/skills/projectstore-status/SKILL.md +47 -0
- package/skills/projectstore-statusline/SKILL.md +29 -0
- package/skills/projectstore-story/SKILL.md +132 -0
- package/skills/projectstore-story-completion/SKILL.md +69 -0
- package/skills/projectstore-vault-communication/SKILL.md +115 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// projectstore — touch-session.mjs
|
|
3
|
+
//
|
|
4
|
+
// Called by BOTH the PreToolUse and PostToolUse hooks, and gated on which:
|
|
5
|
+
// a write inside the vault fires both events, so without the gate the pointer
|
|
6
|
+
// patch below would run twice per call — and that patch is the unguarded
|
|
7
|
+
// read-modify-write the entry-rule state deliberately avoids.
|
|
8
|
+
//
|
|
9
|
+
// Responsibilities:
|
|
10
|
+
// 1. Liveness — touches this session's registration file (mtime) so other
|
|
11
|
+
// sessions can see we are active. Bootstraps the record on first call
|
|
12
|
+
// when SessionStart did not run (plugin installed mid-session via
|
|
13
|
+
// /reload-plugins, or rare path where SessionStart was skipped).
|
|
14
|
+
// 2. Activity log — extracts the target file path from the tool input
|
|
15
|
+
// and, if it lives inside the vault, appends an entry to
|
|
16
|
+
// `recent_activity` in the session file (capped at 50, deduped).
|
|
17
|
+
// The PreCompact hook reads this list to build a survival packet.
|
|
18
|
+
// 3. Entry-rule detection (PostToolUse only) — counts distinct SOURCE paths
|
|
19
|
+
// written this session and, once the threshold is reached with no story
|
|
20
|
+
// open, emits the one advisory reminder. PostToolUse rather than
|
|
21
|
+
// PreToolUse because it fires only after a call succeeds, so declined
|
|
22
|
+
// edits are not counted as work that happened. Spec: "Entry-rule
|
|
23
|
+
// detection: the score, the open-story predicate, and the delivery seams".
|
|
24
|
+
//
|
|
25
|
+
// Session identity comes from Claude's own `session_id` field in the
|
|
26
|
+
// hook input JSON (stdin), so two Claude Code instances open on the
|
|
27
|
+
// same project get distinct session files.
|
|
28
|
+
//
|
|
29
|
+
// Silent no-op when there is no projectstore config, when stdin lacks
|
|
30
|
+
// session_id, or on any error — a PreToolUse hook must never crash the
|
|
31
|
+
// user's tool call.
|
|
32
|
+
|
|
33
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
import {
|
|
36
|
+
readConfig,
|
|
37
|
+
readStdinJson,
|
|
38
|
+
adoptHookInput,
|
|
39
|
+
touchSession,
|
|
40
|
+
writeSession,
|
|
41
|
+
cleanupStaleSessions,
|
|
42
|
+
sessionFilePath,
|
|
43
|
+
projectRoot,
|
|
44
|
+
appendActivity,
|
|
45
|
+
isInsideVault,
|
|
46
|
+
parseFrontmatter,
|
|
47
|
+
readSessionState,
|
|
48
|
+
writeSessionState,
|
|
49
|
+
isSourcePath,
|
|
50
|
+
registerSourcePath,
|
|
51
|
+
entryScore,
|
|
52
|
+
resolveOpenStory,
|
|
53
|
+
readOpenStoryCache,
|
|
54
|
+
writeOpenStoryCache,
|
|
55
|
+
mayRemind,
|
|
56
|
+
electEmitter,
|
|
57
|
+
appendEntryLog,
|
|
58
|
+
entryReminderText,
|
|
59
|
+
ENTRY_THRESHOLD,
|
|
60
|
+
isWriteTool,
|
|
61
|
+
loadLayout,
|
|
62
|
+
anchorKeyOf,
|
|
63
|
+
foldAnchor,
|
|
64
|
+
composeAnchorName,
|
|
65
|
+
bumpAnchor,
|
|
66
|
+
readAnchorState,
|
|
67
|
+
readAnchorOffer,
|
|
68
|
+
writeAnchorOffer,
|
|
69
|
+
liveSessionNames,
|
|
70
|
+
ownSessionName,
|
|
71
|
+
sessionNameOffer,
|
|
72
|
+
sessionNameOfferText,
|
|
73
|
+
toolPaths,
|
|
74
|
+
} from "./lib.mjs";
|
|
75
|
+
|
|
76
|
+
const NUDGE_INTERVAL_MS = 10 * 60 * 1000;
|
|
77
|
+
|
|
78
|
+
// Per-session active pointer (ADR-006) — denormalized titles/status captured
|
|
79
|
+
// at write time so the statusline renders with zero vault reads — plus the
|
|
80
|
+
// raw-edit nudge (PS-IMPROVE story-003): throttled, never blocking.
|
|
81
|
+
// The session-name offer (ADR: the settled-anchor offer). Returns a line or
|
|
82
|
+
// null. Separate from the pointer patch because the two answer different
|
|
83
|
+
// questions — the pointer is "what is current", this is "has the session
|
|
84
|
+
// settled" — and because this one must be gated where the pointer is not.
|
|
85
|
+
let LAYOUT = null;
|
|
86
|
+
function layoutOnce(cfg) {
|
|
87
|
+
if (LAYOUT === null) LAYOUT = loadLayout(cfg.layout);
|
|
88
|
+
return LAYOUT;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function anchorOffer(cfg, proj, sid, filePath, toolName, isSubagent, sessionsDir = null) {
|
|
92
|
+
// The two gates. The pointer patch above deliberately has neither: it answers
|
|
93
|
+
// "what am I looking at", for which a Read is a fine answer. A NAME is a
|
|
94
|
+
// claim about what the session is DOING, so a review session that greps
|
|
95
|
+
// thirty files must not be named after them, and a subagent — which shares
|
|
96
|
+
// this session id and cannot accept a name — must not vote on one.
|
|
97
|
+
if (!isWriteTool(toolName || "")) return null;
|
|
98
|
+
if (isSubagent) return null;
|
|
99
|
+
|
|
100
|
+
const rel = filePath.slice(cfg.vault_path.length + 1);
|
|
101
|
+
let hit = null;
|
|
102
|
+
try { hit = anchorKeyOf(rel, layoutOnce(cfg)); } catch { return null; }
|
|
103
|
+
if (!hit) return null;
|
|
104
|
+
|
|
105
|
+
// Both slots: the key tally is what settles an anchor, the leaf tally only
|
|
106
|
+
// picks which story names it. Bumping the leaf alone leaves the anchor
|
|
107
|
+
// permanently unsettled — silently, since a rule that never fires looks
|
|
108
|
+
// exactly like a quiet one.
|
|
109
|
+
bumpAnchor(proj, sid, hit.key, null);
|
|
110
|
+
if (hit.leaf) bumpAnchor(proj, sid, hit.key, hit.leaf);
|
|
111
|
+
|
|
112
|
+
const prev = readAnchorState(proj, sid);
|
|
113
|
+
// The tally on disk already includes this event, so replay it against the
|
|
114
|
+
// state MINUS this event: fold is what decides an offer, and it must see the
|
|
115
|
+
// increment happen rather than find it already there.
|
|
116
|
+
const counts = { ...prev.counts };
|
|
117
|
+
if (counts[hit.key]) counts[hit.key] -= 1;
|
|
118
|
+
const leaves = { ...prev.leaves };
|
|
119
|
+
if (hit.leaf && leaves[hit.key] && leaves[hit.key][hit.leaf]) {
|
|
120
|
+
leaves[hit.key] = { ...leaves[hit.key], [hit.leaf]: leaves[hit.key][hit.leaf] - 1 };
|
|
121
|
+
}
|
|
122
|
+
const { state, offer } = foldAnchor(
|
|
123
|
+
{ counts, leaves, incumbent: prev.incumbent, offered: prev.offered }, hit);
|
|
124
|
+
if (!offer) return null;
|
|
125
|
+
|
|
126
|
+
const rec = readAnchorOffer(proj, sid);
|
|
127
|
+
const current = ownSessionName(sid, sessionsDir);
|
|
128
|
+
// A session almost always arrives already wearing a name the harness assigned
|
|
129
|
+
// — so "current is not our last offer" does NOT mean the person chose it.
|
|
130
|
+
// Reading it that way silenced the feature permanently for every real
|
|
131
|
+
// session: on the first offer `offered` is null, so any pre-existing name was
|
|
132
|
+
// classified as a deliberate choice and nothing was ever spoken again.
|
|
133
|
+
// A name is the person's only if we have spoken at least once and it is none
|
|
134
|
+
// of the names we composed.
|
|
135
|
+
const declined = current && rec.offers.length > 0 && !rec.offers.includes(current)
|
|
136
|
+
&& !rec.declined.includes(current)
|
|
137
|
+
? [...rec.declined, current]
|
|
138
|
+
: rec.declined;
|
|
139
|
+
|
|
140
|
+
const chosen = sessionNameOffer(offer.name, {
|
|
141
|
+
peers: liveSessionNames(sid, sessionsDir),
|
|
142
|
+
current,
|
|
143
|
+
declined,
|
|
144
|
+
});
|
|
145
|
+
// The incumbent moved whether or not we speak, or the same anchor would
|
|
146
|
+
// re-arm and offer again on the next write. But `offered` records what was
|
|
147
|
+
// actually SAID: a name suppressed by the collision ladder was never heard,
|
|
148
|
+
// and burning it would keep this session silent after the peer exits.
|
|
149
|
+
writeAnchorOffer(proj, sid, {
|
|
150
|
+
incumbent: state.incumbent,
|
|
151
|
+
offered: chosen ? chosen.name : rec.offered,
|
|
152
|
+
offers: chosen ? [...new Set([...rec.offers, chosen.name])] : rec.offers,
|
|
153
|
+
declined,
|
|
154
|
+
});
|
|
155
|
+
if (!chosen) return null;
|
|
156
|
+
try {
|
|
157
|
+
appendEntryLog(proj, {
|
|
158
|
+
at: new Date().toISOString(), session_id: sid, kind: "name-offer", name: chosen.name,
|
|
159
|
+
});
|
|
160
|
+
} catch {}
|
|
161
|
+
return sessionNameOfferText(chosen);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function updatePointerAndNudge(cfg, proj, sid, filePath, toolName) {
|
|
165
|
+
const rel = filePath.slice(cfg.vault_path.length + 1);
|
|
166
|
+
let patch = null;
|
|
167
|
+
|
|
168
|
+
const m = rel.match(/^epics\/([^/]+)\//);
|
|
169
|
+
if (m) {
|
|
170
|
+
const epicId = m[1];
|
|
171
|
+
patch = { active_epic: epicId };
|
|
172
|
+
try {
|
|
173
|
+
const { data } = parseFrontmatter(
|
|
174
|
+
readFileSync(join(cfg.vault_path, "epics", epicId, "epic.md"), "utf8"));
|
|
175
|
+
if (data.title) patch.epic_title = String(data.title);
|
|
176
|
+
} catch {}
|
|
177
|
+
const sm = rel.match(/^epics\/[^/]+\/stories\/([^/]+\.md)$/);
|
|
178
|
+
if (sm) {
|
|
179
|
+
patch.active_story = sm[1].replace(/\.md$/, "");
|
|
180
|
+
try {
|
|
181
|
+
const { data } = parseFrontmatter(readFileSync(filePath, "utf8"));
|
|
182
|
+
if (data.title) patch.story_title = String(data.title);
|
|
183
|
+
if (data.status) patch.story_status = String(data.status);
|
|
184
|
+
} catch {}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let nudge = null;
|
|
189
|
+
if (isWriteTool(toolName || "") && cfg.guard !== "off") {
|
|
190
|
+
const st = readSessionState(proj, sid);
|
|
191
|
+
const last = st && st.nudged_at ? Date.parse(st.nudged_at) : 0;
|
|
192
|
+
if (Date.now() - last > NUDGE_INTERVAL_MS) {
|
|
193
|
+
nudge =
|
|
194
|
+
"projectstore: vault file edited directly — if this bypassed a /projectstore:* command, run /projectstore:reconcile (or doctor) afterwards so the board/indexes stay in sync.";
|
|
195
|
+
patch = { ...(patch || {}), nudged_at: new Date().toISOString() };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (patch) writeSessionState(proj, sid, patch);
|
|
200
|
+
return nudge;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// The source-side branch (PostToolUse). Never throws: every failure here must
|
|
204
|
+
// leave the user's tool call untouched.
|
|
205
|
+
async function entryBranch(cfg, proj, sid, filePath, input) {
|
|
206
|
+
// Writes only. extractToolPath happily yields a path for Read, Grep, Glob and
|
|
207
|
+
// LS — all of which carry file_path or path — so without this gate three
|
|
208
|
+
// read-only calls would trip the threshold and the reminder would announce
|
|
209
|
+
// work that never happened. (Grep's `path` is often a directory, which would
|
|
210
|
+
// then be counted as a "source file".) The event tells us the call succeeded;
|
|
211
|
+
// only the tool name tells us it wrote.
|
|
212
|
+
if (!isWriteTool(input.tool_name || "")) return;
|
|
213
|
+
if (!isSourcePath(filePath, proj, cfg.vault_path)) return;
|
|
214
|
+
|
|
215
|
+
// Belt and braces. PostToolUse only fires after success — failures raise
|
|
216
|
+
// PostToolUseFailure, which this script is not registered on — so the event
|
|
217
|
+
// itself is the discrimination. Nothing may DEPEND on this field's shape, and
|
|
218
|
+
// the shape varies by BOTH harness and tool: Claude Code sends an object;
|
|
219
|
+
// Codex sent a string for 368 of 371 PostToolUse firings and a content-block
|
|
220
|
+
// array for the other 3, all of them `webrun` (measured 2026-09-07). So the
|
|
221
|
+
// guard is written for "an object with success === false", and every other
|
|
222
|
+
// shape is a pass-through. Narrowed explicitly rather than relying on
|
|
223
|
+
// `"…".success` being undefined — that is an accident of JavaScript, not a
|
|
224
|
+
// decision, and it does not hold for every shape a tool may invent.
|
|
225
|
+
const response = input.tool_response;
|
|
226
|
+
if (response && typeof response === "object" && response.success === false) return;
|
|
227
|
+
|
|
228
|
+
registerSourcePath(proj, sid, filePath);
|
|
229
|
+
|
|
230
|
+
// Subagents count toward the score but are never the audience: a reminder
|
|
231
|
+
// delivered there reaches an actor mid-implementation under explicit
|
|
232
|
+
// instructions, who cannot open a story.
|
|
233
|
+
if (input.agent_id || input.agent_type) return;
|
|
234
|
+
if (cfg.guard === "off") return;
|
|
235
|
+
|
|
236
|
+
const score = entryScore(proj, sid);
|
|
237
|
+
if (score < ENTRY_THRESHOLD) return;
|
|
238
|
+
if (!mayRemind(proj, sid)) return;
|
|
239
|
+
|
|
240
|
+
let verdict = readOpenStoryCache(proj, sid);
|
|
241
|
+
if (verdict === null) {
|
|
242
|
+
verdict = await resolveOpenStory(cfg.vault_path);
|
|
243
|
+
writeOpenStoryCache(proj, sid, verdict);
|
|
244
|
+
}
|
|
245
|
+
if (verdict === "unknown") {
|
|
246
|
+
// Reached either from a fresh sweep that hit its budget with reads still
|
|
247
|
+
// outstanding (the event loop would keep this hook alive until they settle)
|
|
248
|
+
// or from a cached unknown, where nothing is outstanding and the exit is
|
|
249
|
+
// merely redundant. Exiting is safe here and only here: an unknown verdict suppresses the reminder, so stdout is
|
|
250
|
+
// untouched — process.exit does not flush pending pipe writes, and exiting
|
|
251
|
+
// after emitting would truncate the reminder.
|
|
252
|
+
process.exit(0);
|
|
253
|
+
}
|
|
254
|
+
if (verdict !== false) return;
|
|
255
|
+
|
|
256
|
+
if (!electEmitter(proj, sid)) return;
|
|
257
|
+
appendEntryLog(proj, { at: new Date().toISOString(), session_id: sid, score });
|
|
258
|
+
process.stdout.write(JSON.stringify({
|
|
259
|
+
hookSpecificOutput: {
|
|
260
|
+
hookEventName: "PostToolUse",
|
|
261
|
+
additionalContext: entryReminderText(score),
|
|
262
|
+
},
|
|
263
|
+
}) + "\n");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function main() {
|
|
267
|
+
// The payload before the project. On a harness that exports no project-dir
|
|
268
|
+
// variable the payload's cwd is the only answer better than "whatever
|
|
269
|
+
// directory this process started in", and readConfig() resolves through
|
|
270
|
+
// projectRoot() — so anything read before this line answers for the wrong
|
|
271
|
+
// project, silently. Read once: fd 0 is empty on a second read.
|
|
272
|
+
const input = adoptHookInput(readStdinJson());
|
|
273
|
+
if (!input) return;
|
|
274
|
+
|
|
275
|
+
const cfg = readConfig();
|
|
276
|
+
if (!cfg) return;
|
|
277
|
+
|
|
278
|
+
const sid = input.session_id;
|
|
279
|
+
if (!sid) return;
|
|
280
|
+
|
|
281
|
+
const proj = projectRoot();
|
|
282
|
+
const event = input.hook_event_name;
|
|
283
|
+
|
|
284
|
+
// Liveness and the vault-side branch stay on PreToolUse, exactly as before.
|
|
285
|
+
// PreToolUse always precedes PostToolUse for the same call, so pinning them
|
|
286
|
+
// here changes nothing about when they run — it only stops them running twice.
|
|
287
|
+
if (event !== "PostToolUse") {
|
|
288
|
+
if (existsSync(sessionFilePath(cfg.vault_path, sid))) {
|
|
289
|
+
touchSession(cfg.vault_path, sid);
|
|
290
|
+
} else {
|
|
291
|
+
// The exemption (contract 23) is passed for agreement with the other
|
|
292
|
+
// caller, not for effect: this branch runs only when our own session file
|
|
293
|
+
// does NOT exist, so there is nothing here to exempt. Two callers of one
|
|
294
|
+
// function disagreeing about its signature is how the next reader
|
|
295
|
+
// concludes the argument is optional.
|
|
296
|
+
cleanupStaleSessions(cfg.vault_path, 24, sid);
|
|
297
|
+
writeSession(cfg.vault_path, sid, proj);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!input.tool_name) return;
|
|
302
|
+
const filePaths = toolPaths(input);
|
|
303
|
+
if (!filePaths.length) return;
|
|
304
|
+
|
|
305
|
+
if (event === "PostToolUse") {
|
|
306
|
+
for (const filePath of filePaths) await entryBranch(cfg, proj, sid, filePath, input);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const messages = [];
|
|
311
|
+
for (const filePath of filePaths) if (isInsideVault(filePath, cfg.vault_path)) {
|
|
312
|
+
try { appendActivity(cfg.vault_path, sid, filePath, input.tool_name); } catch {}
|
|
313
|
+
let nudge = null, offer = null;
|
|
314
|
+
try { nudge = updatePointerAndNudge(cfg, proj, sid, filePath, input.tool_name); } catch {}
|
|
315
|
+
if (cfg.guard !== "off") {
|
|
316
|
+
try {
|
|
317
|
+
offer = anchorOffer(cfg, proj, sid, filePath, input.tool_name,
|
|
318
|
+
Boolean(input.agent_id || input.agent_type),
|
|
319
|
+
process.env.PROJECTSTORE_SESSIONS_DIR || null);
|
|
320
|
+
} catch {}
|
|
321
|
+
}
|
|
322
|
+
// One invocation carries one systemMessage, so the two compose rather than
|
|
323
|
+
// race: emitting twice would silently drop whichever went second, and the
|
|
324
|
+
// offer fires once or twice a session against a nudge that fires every ten
|
|
325
|
+
// minutes — the rare one would be the one lost.
|
|
326
|
+
messages.push(...[nudge, offer].filter(Boolean));
|
|
327
|
+
}
|
|
328
|
+
if (messages.length) {
|
|
329
|
+
process.stdout.write(JSON.stringify({ systemMessage: [...new Set(messages)].join("\n") }) + "\n");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
main().catch(() => {
|
|
334
|
+
// A hook must never crash the user's tool call — sync throws and rejected
|
|
335
|
+
// promises alike.
|
|
336
|
+
});
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// projectstore — version-guard.mjs (PS-HARNESS: "Claim the npm name and ship
|
|
3
|
+
// the first release")
|
|
4
|
+
//
|
|
5
|
+
// The release-time guard. It did not exist before this story: ADR-009 and the
|
|
6
|
+
// harness-landscape research note both describe "the version-check guard" as
|
|
7
|
+
// something already in place, and neither was true — there was no script, no
|
|
8
|
+
// test and no CI step. Doctor's version checks are a different guard entirely
|
|
9
|
+
// (runtime: an installed plugin against the marketplace), and they stay.
|
|
10
|
+
//
|
|
11
|
+
// node scripts/version-guard.mjs [--tag vX.Y.Z]
|
|
12
|
+
// node scripts/version-guard.mjs --write-packlist
|
|
13
|
+
//
|
|
14
|
+
// Checks that every version-bearing manifest agrees, and — when --tag is
|
|
15
|
+
// given — that the release tag agrees with them. CI passes the tag; a human
|
|
16
|
+
// running it before a manual publish does not, because publish #1 is not
|
|
17
|
+
// tag-triggered and nothing else covers it.
|
|
18
|
+
//
|
|
19
|
+
// Exit 0 when everything agrees, 1 on disagreement or a malformed manifest.
|
|
20
|
+
// Output is JSON on stdout, always, so a workflow step can quote it.
|
|
21
|
+
|
|
22
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
23
|
+
import { spawnSync } from "node:child_process";
|
|
24
|
+
import { resolve, dirname } from "node:path";
|
|
25
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
26
|
+
// Contract 2 of the atomic-regeneration work: nothing under scripts/ writes
|
|
27
|
+
// directly, and tests/scripts.test.mjs enforces it by globbing this directory.
|
|
28
|
+
import { writeFileAtomic } from "./lib.mjs";
|
|
29
|
+
import { LAYOUT } from "./harness.mjs";
|
|
30
|
+
|
|
31
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
32
|
+
export const PACKLIST = "tests/fixtures/packlist.json";
|
|
33
|
+
|
|
34
|
+
// [path, how to read the version, required]. Required sites must exist: with
|
|
35
|
+
// everything optional, a checkout missing package.json reported agreement,
|
|
36
|
+
// because "no site disagrees" is trivially true when there are no sites.
|
|
37
|
+
const VERSION_SITES = [
|
|
38
|
+
["package.json", (j) => j.version, true],
|
|
39
|
+
[".claude-plugin/plugin.json", (j) => j.version, true],
|
|
40
|
+
// Found by name, not by position: `plugins[0]` reads a sibling plugin's
|
|
41
|
+
// version the moment the marketplace lists more than one, and reports the
|
|
42
|
+
// disagreement against the wrong file.
|
|
43
|
+
[
|
|
44
|
+
".claude-plugin/marketplace.json",
|
|
45
|
+
(j) => j.plugins?.find((p) => p.name === "projectstore")?.version,
|
|
46
|
+
true,
|
|
47
|
+
],
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// The distribution shells (the shells ADR; layout spec contract 11): one
|
|
51
|
+
// package.json per shell under packaging/shells/, each at the core's version
|
|
52
|
+
// with an exact pin on it. Read directly — the directory is in the repository
|
|
53
|
+
// but not in the tarball, and the guard runs from the repository.
|
|
54
|
+
export const SHELLS_DIR = "packaging/shells";
|
|
55
|
+
|
|
56
|
+
export function collectShells(root = ROOT) {
|
|
57
|
+
const dir = resolve(root, SHELLS_DIR);
|
|
58
|
+
if (!existsSync(dir)) return { shells: [] };
|
|
59
|
+
const shells = [];
|
|
60
|
+
// Directories only: a .DS_Store Finder drops on a browse is not a shell.
|
|
61
|
+
for (const name of readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort()) {
|
|
62
|
+
const rel = `${SHELLS_DIR}/${name}/package.json`;
|
|
63
|
+
const abs = resolve(root, rel);
|
|
64
|
+
if (!existsSync(abs)) return { error: `${rel}: missing — every directory under ${SHELLS_DIR}/ is a shell` };
|
|
65
|
+
let json;
|
|
66
|
+
try { json = JSON.parse(readFileSync(abs, "utf8")); } catch (e) { return { error: `${rel}: ${e.message}` }; }
|
|
67
|
+
if (json.name !== name) return { error: `${rel}: name "${json.name}" is not its directory's` };
|
|
68
|
+
if (typeof json.version !== "string" || !json.version) return { error: `${rel}: no version found where one is required` };
|
|
69
|
+
const pluginVersions = [];
|
|
70
|
+
if (name === "projectstore-codex") {
|
|
71
|
+
for (const manifest of ["plugin.json", ".codex-plugin/plugin.json"]) {
|
|
72
|
+
const manifestRel = `${SHELLS_DIR}/${name}/${manifest}`;
|
|
73
|
+
const manifestAbs = resolve(root, manifestRel);
|
|
74
|
+
if (!existsSync(manifestAbs)) return { error: `${manifestRel}: missing — the Codex shell carries both canonical and compatibility manifests` };
|
|
75
|
+
let parsed;
|
|
76
|
+
try { parsed = JSON.parse(readFileSync(manifestAbs, "utf8")); } catch (e) { return { error: `${manifestRel}: ${e.message}` }; }
|
|
77
|
+
if (typeof parsed.version !== "string" || !parsed.version) return { error: `${manifestRel}: no version found where one is required` };
|
|
78
|
+
pluginVersions.push({ file: manifestRel, version: parsed.version });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
shells.push({
|
|
82
|
+
name, file: rel, version: json.version,
|
|
83
|
+
pin: json.dependencies?.projectstore ?? null,
|
|
84
|
+
private: json.private === true,
|
|
85
|
+
bin: json.bin?.[name] ?? null,
|
|
86
|
+
binExists: typeof json.bin?.[name] === "string" && existsSync(resolve(root, SHELLS_DIR, name, json.bin[name])),
|
|
87
|
+
bundled: Array.isArray(json.bundleDependencies) && json.bundleDependencies.includes("projectstore"),
|
|
88
|
+
pluginVersions,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return { shells };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function die(msg) {
|
|
95
|
+
process.stdout.write(JSON.stringify({ ok: false, error: msg }) + "\n");
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function collectVersions(root = ROOT) {
|
|
100
|
+
if (existsSync(resolve(root, ".codex-plugin", "plugin.json"))) {
|
|
101
|
+
return { error: ".codex-plugin/plugin.json must live in the projectstore-codex shell, not at the core package root" };
|
|
102
|
+
}
|
|
103
|
+
const found = [];
|
|
104
|
+
for (const [rel, pick, required] of VERSION_SITES) {
|
|
105
|
+
if (!existsSync(resolve(root, rel))) {
|
|
106
|
+
if (required) return { error: `${rel}: missing, and it carries the release version` };
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
let json;
|
|
110
|
+
try {
|
|
111
|
+
json = JSON.parse(readFileSync(resolve(root, rel), "utf8"));
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return { error: `${rel}: ${e.message}` };
|
|
114
|
+
}
|
|
115
|
+
const version = pick(json);
|
|
116
|
+
if (typeof version !== "string" || !version) {
|
|
117
|
+
return { error: `${rel}: no version found where one is required` };
|
|
118
|
+
}
|
|
119
|
+
found.push({ file: rel, version });
|
|
120
|
+
}
|
|
121
|
+
return { found };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// `v0.25.0` and `0.25.0` are the same release; the tag carries the prefix by
|
|
125
|
+
// this repository's convention and the manifests never do.
|
|
126
|
+
const stripV = (t) => (t.startsWith("v") ? t.slice(1) : t);
|
|
127
|
+
|
|
128
|
+
export function checkVersions({ root = ROOT, tag = null } = {}) {
|
|
129
|
+
const { found, error } = collectVersions(root);
|
|
130
|
+
if (error) return { ok: false, error };
|
|
131
|
+
const sh = collectShells(root);
|
|
132
|
+
if (sh.error) return { ok: false, error: sh.error };
|
|
133
|
+
|
|
134
|
+
// A shell's version is one more site under the same rule.
|
|
135
|
+
const sites = [...found, ...sh.shells.flatMap((s) => [{ file: s.file, version: s.version }, ...(s.pluginVersions || [])])];
|
|
136
|
+
if (tag) sites.push({ file: "<tag>", version: stripV(tag) });
|
|
137
|
+
const distinct = [...new Set(sites.map((s) => s.version))];
|
|
138
|
+
if (distinct.length !== 1) return { ok: false, error: "version mismatch", versions: distinct, checked: sites };
|
|
139
|
+
|
|
140
|
+
// Its pin is exact and equal to that version; its core is bundled; its bin
|
|
141
|
+
// is the one file it ships. A range here is the unverified pairing the
|
|
142
|
+
// shells ADR rejects; a missing bundle ships an installer with no core.
|
|
143
|
+
for (const s of sh.shells) {
|
|
144
|
+
if (s.pin !== `=${s.version}`) return { ok: false, error: "shell pin mismatch", shell: s.name, pin: s.pin, expected: `=${s.version}`, checked: sites };
|
|
145
|
+
if (!s.bundled) return { ok: false, error: "shell does not bundle the core", shell: s.name, checked: sites };
|
|
146
|
+
if (s.bin !== `bin/${s.name}.mjs` || !s.binExists) return { ok: false, error: "shell bin missing or misnamed", shell: s.name, bin: s.bin, expected: `bin/${s.name}.mjs`, checked: sites };
|
|
147
|
+
}
|
|
148
|
+
return { ok: true, version: distinct[0], checked: sites, shells: sh.shells.map((s) => ({ name: s.name, private: s.private })) };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Everything at the repository root that is deliberately NOT in the tarball.
|
|
152
|
+
// This list is the whole point of the tree check below: `npm pack` can only
|
|
153
|
+
// report what `files[]` already allows, so comparing pack output against the
|
|
154
|
+
// fixture is blind to a directory missing from `files[]` — it is absent from
|
|
155
|
+
// both sides and they agree. The tree, not the pack output, is the only place
|
|
156
|
+
// a forgotten directory is visible.
|
|
157
|
+
//
|
|
158
|
+
// Adding a name here is a decision that it must never ship. Anything else new
|
|
159
|
+
// at the root fails the check until it is added to `files[]`.
|
|
160
|
+
export const NOT_SHIPPED = new Set([
|
|
161
|
+
".git",
|
|
162
|
+
".github", // release/test workflows — not part of the plugin payload
|
|
163
|
+
".claude", // local worktrees and the harness's own files
|
|
164
|
+
LAYOUT.root, // this project's own binding, overlays and state (the layout ADR)
|
|
165
|
+
".omc",
|
|
166
|
+
".gitignore",
|
|
167
|
+
".DS_Store",
|
|
168
|
+
"node_modules",
|
|
169
|
+
// `packaging/shells.mjs --build --out dist` writes here, and the release
|
|
170
|
+
// workflow does exactly that. Gitignored, so it is invisible to git status —
|
|
171
|
+
// and the tree check is the only place an unlisted root directory shows up.
|
|
172
|
+
"dist",
|
|
173
|
+
"CLAUDE.md", // a pointer to AGENTS.md, which does ship
|
|
174
|
+
"tests", // 240 kB of fixtures nobody installing the plugin needs
|
|
175
|
+
"packaging", // reserved-name stubs; see packaging/README.md
|
|
176
|
+
"adapters", // generated harness inputs copied into their distribution shells at build time
|
|
177
|
+
"package-lock.json",
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
// Top-level entries that are neither shipped nor deliberately excluded. Kept
|
|
181
|
+
// pure — it takes the tree listing rather than reading it — so a test can hand
|
|
182
|
+
// it a directory that does not exist and watch it fail.
|
|
183
|
+
export function unshippedTopLevel(entries, packlist) {
|
|
184
|
+
const shipped = new Set(
|
|
185
|
+
packlist.map((p) => (p.includes("/") ? p.slice(0, p.indexOf("/")) : p)),
|
|
186
|
+
);
|
|
187
|
+
return entries
|
|
188
|
+
.filter((e) => e !== "." && e !== ".." && !shipped.has(e) && !NOT_SHIPPED.has(e))
|
|
189
|
+
.sort();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// The packlist is the second release-time invariant: what npm would actually
|
|
193
|
+
// ship. Stored as sorted paths only — sizes churn on every content edit and
|
|
194
|
+
// would make the fixture unreviewable.
|
|
195
|
+
export function currentPacklist(root = ROOT) {
|
|
196
|
+
const r = spawnSync("npm", ["pack", "--dry-run", "--json"], {
|
|
197
|
+
cwd: root,
|
|
198
|
+
encoding: "utf8",
|
|
199
|
+
timeout: 120000,
|
|
200
|
+
});
|
|
201
|
+
if (r.status !== 0) return { error: `npm pack failed: ${r.stderr?.trim()}` };
|
|
202
|
+
try {
|
|
203
|
+
const [pkg] = JSON.parse(r.stdout);
|
|
204
|
+
return { files: pkg.files.map((f) => f.path).sort() };
|
|
205
|
+
} catch (e) {
|
|
206
|
+
return { error: `npm pack output unparseable: ${e.message}` };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function main(argv) {
|
|
211
|
+
const tagIdx = argv.indexOf("--tag");
|
|
212
|
+
const tag = tagIdx !== -1 ? argv[tagIdx + 1] : null;
|
|
213
|
+
if (tagIdx !== -1 && !tag) die("--tag requires a value");
|
|
214
|
+
|
|
215
|
+
if (argv.includes("--write-packlist")) {
|
|
216
|
+
const { files, error } = currentPacklist();
|
|
217
|
+
if (error) die(error);
|
|
218
|
+
try {
|
|
219
|
+
writeFileAtomic(resolve(ROOT, PACKLIST), JSON.stringify(files, null, 2) + "\n");
|
|
220
|
+
} catch (e) {
|
|
221
|
+
// The header promises JSON on stdout always; an escaping stack trace
|
|
222
|
+
// would break the one caller that parses this.
|
|
223
|
+
die(`${PACKLIST}: ${e.message}`);
|
|
224
|
+
}
|
|
225
|
+
// The shells' fixtures, from the core's CURRENT pack — after the core's
|
|
226
|
+
// fixture, since each lists the core's files under node_modules/. A lazy
|
|
227
|
+
// import: scripts/ ships and packaging/ does not, so a static specifier
|
|
228
|
+
// here would ship a module that cannot load.
|
|
229
|
+
const shellsModule = resolve(ROOT, "packaging", "shells.mjs");
|
|
230
|
+
const shells = [];
|
|
231
|
+
if (existsSync(shellsModule)) {
|
|
232
|
+
const m = await import(pathToFileURL(shellsModule).href);
|
|
233
|
+
const built = m.buildShells({ root: ROOT });
|
|
234
|
+
if (built.error) die(built.error);
|
|
235
|
+
for (const b of built.shells) {
|
|
236
|
+
if (b.error) die(b.error);
|
|
237
|
+
const rel = m.shellPacklistPath(b.name);
|
|
238
|
+
try { writeFileAtomic(resolve(ROOT, rel), JSON.stringify(b.files, null, 2) + "\n"); } catch (e) { die(`${rel}: ${e.message}`); }
|
|
239
|
+
shells.push({ wrote: rel, count: b.files.length });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
process.stdout.write(
|
|
243
|
+
JSON.stringify({ ok: true, wrote: PACKLIST, count: files.length, shells }) + "\n",
|
|
244
|
+
);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const result = checkVersions({ tag });
|
|
249
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
250
|
+
if (!result.ok) process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
254
|
+
await main(process.argv.slice(2));
|
|
255
|
+
}
|