projectstore-codex 0.0.1 → 0.28.2
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 +176 -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 +261 -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 +35 -14
- 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,422 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// projectstore — reconcile.mjs
|
|
3
|
+
// Re-derives every derived view from the vault's source of truth:
|
|
4
|
+
// kanban.md, folder-index README tables, code-map.md, graph.md. Hand-edits
|
|
5
|
+
// therefore can never *permanently* desync the views (PS-IMPROVE story-002,
|
|
6
|
+
// ADR-004/005: vault-side repairs belong to reconcile, not doctor --fix).
|
|
7
|
+
//
|
|
8
|
+
// Two modes (spec: atomic-regeneration-of-derived-views):
|
|
9
|
+
// compute (default) — read-only. Output JSON lists each target with
|
|
10
|
+
// {path, changed, content?, stats?} plus summary.{changed, failed}.
|
|
11
|
+
// Idempotent: a clean vault yields zero changes. Exit stays 0 (a
|
|
12
|
+
// reporting tool) — per-target errors surface via summary.failed and
|
|
13
|
+
// the command prose, not the exit code.
|
|
14
|
+
// --write — applies the regeneration itself: every selected target is
|
|
15
|
+
// recomputed from the vault state at write time (the approval covers
|
|
16
|
+
// the regeneration action, not a byte snapshot) and replaced atomically
|
|
17
|
+
// via lib.mjs writeFileAtomic. Report per target {path, changed,
|
|
18
|
+
// written, error?} — no content on stdout. Continue-on-error; exits 1
|
|
19
|
+
// iff a selected target failed, so a cron caller notices. Headless use
|
|
20
|
+
// is sanctioned by ADR-009 ("reconcile as a scheduled repair job").
|
|
21
|
+
//
|
|
22
|
+
// --only <sel>[,…] limits either mode to: kanban | codemap | graph |
|
|
23
|
+
// indexes | indexes=<folder-path>. Unknown selectors, and explicitly
|
|
24
|
+
// *named* targets the layout or vault cannot provide, die loudly — a
|
|
25
|
+
// typo'd cron job must not silently reconcile nothing. Bare invocation
|
|
26
|
+
// (and the class-wide `indexes`) keeps the silent-skip behaviour for
|
|
27
|
+
// absent targets.
|
|
28
|
+
|
|
29
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
30
|
+
import { join, basename } from "node:path";
|
|
31
|
+
import { spawnSync } from "node:child_process";
|
|
32
|
+
import { resolve } from "node:path";
|
|
33
|
+
import {
|
|
34
|
+
readConfig,
|
|
35
|
+
readConfigAt,
|
|
36
|
+
loadLayout,
|
|
37
|
+
projectRoot,
|
|
38
|
+
pluginRoot,
|
|
39
|
+
indexHeaderRe,
|
|
40
|
+
slugIdentity,
|
|
41
|
+
displayNumberOf,
|
|
42
|
+
compareArtifactOrder,
|
|
43
|
+
writeFileAtomic, isMain
|
|
44
|
+
} from "./lib.mjs";
|
|
45
|
+
import { childEnv } from "./harness.mjs";
|
|
46
|
+
|
|
47
|
+
// Set by runReconcile when a caller names the project; the generators it
|
|
48
|
+
// spawns receive it. Null means "this process's own project".
|
|
49
|
+
let _projectDir = null;
|
|
50
|
+
let _env = null;
|
|
51
|
+
import { scanArtifacts } from "./doctor.mjs";
|
|
52
|
+
|
|
53
|
+
function die(msg) {
|
|
54
|
+
process.stderr.write(`projectstore/reconcile: ${msg}\n`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function runGenerator(script) {
|
|
59
|
+
const r = spawnSync(process.execPath, [join(pluginRoot(), "scripts", script)], {
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
timeout: 10000,
|
|
62
|
+
env: _env || childEnv(process.env, { projectRoot: _projectDir || projectRoot() }),
|
|
63
|
+
});
|
|
64
|
+
if (r.status !== 0) return { error: (r.stderr || "generator failed").trim() };
|
|
65
|
+
try { return JSON.parse(r.stdout); } catch { return { error: "unparseable generator output" }; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const normalize = (s) =>
|
|
69
|
+
s.split("\n").filter((l) => !l.startsWith("generated_at:")).join("\n").trimEnd();
|
|
70
|
+
|
|
71
|
+
const errMsg = (e) => String((e && e.message) || e);
|
|
72
|
+
|
|
73
|
+
// Which targets does this invocation cover? Pure over the layout so the
|
|
74
|
+
// kanban-less-layout branch is testable without a fake plugin root.
|
|
75
|
+
// Returns {error} on a selector the layout cannot satisfy. `named` marks an
|
|
76
|
+
// individually named index (indexes=adr) — those fail loudly when the vault
|
|
77
|
+
// cannot provide them; the class-wide selectors skip, like bare invocation.
|
|
78
|
+
export function resolveSelection(layout, onlyRaw) {
|
|
79
|
+
const validSelectors = () =>
|
|
80
|
+
[layout.kanban ? "kanban" : null, "codemap", "graph", "indexes",
|
|
81
|
+
...layout.folders.map((f) => `indexes=${f.path}`)]
|
|
82
|
+
.filter(Boolean).join(", ");
|
|
83
|
+
if (onlyRaw == null) {
|
|
84
|
+
return {
|
|
85
|
+
explicit: false,
|
|
86
|
+
kanban: true,
|
|
87
|
+
codemap: true,
|
|
88
|
+
graph: true,
|
|
89
|
+
indexes: layout.folders.map((f) => ({ path: f.path, named: false })),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const sel = { explicit: true, kanban: false, codemap: false, graph: false, indexes: [] };
|
|
93
|
+
const parts = String(onlyRaw).split(",").map((s) => s.trim()).filter(Boolean);
|
|
94
|
+
if (!parts.length) return { error: `empty --only selection — valid: ${validSelectors()}` };
|
|
95
|
+
for (const raw of parts) {
|
|
96
|
+
if (raw === "kanban") {
|
|
97
|
+
if (!layout.kanban) return { error: `layout declares no kanban — valid: ${validSelectors()}` };
|
|
98
|
+
sel.kanban = true;
|
|
99
|
+
} else if (raw === "codemap") {
|
|
100
|
+
if (!layout.folders.some((f) => f.kind === "epic")) {
|
|
101
|
+
return { error: `layout has no epic folder, so there is no code map — valid: ${validSelectors()}` };
|
|
102
|
+
}
|
|
103
|
+
sel.codemap = true;
|
|
104
|
+
} else if (raw === "graph") {
|
|
105
|
+
// Valid on every layout: the graph's node universe is whatever
|
|
106
|
+
// artifact kinds the layout declares — there is no layout without one.
|
|
107
|
+
sel.graph = true;
|
|
108
|
+
} else if (raw === "indexes") {
|
|
109
|
+
// Merge, never assign: `indexes=adr,indexes` must keep adr's named
|
|
110
|
+
// loudness rather than silently demoting it to a skippable member.
|
|
111
|
+
for (const f of layout.folders) {
|
|
112
|
+
if (!sel.indexes.some((x) => x.path === f.path)) sel.indexes.push({ path: f.path, named: false });
|
|
113
|
+
}
|
|
114
|
+
} else if (raw.startsWith("indexes=")) {
|
|
115
|
+
const p = raw.slice("indexes=".length);
|
|
116
|
+
if (!layout.folders.some((f) => f.path === p)) {
|
|
117
|
+
return { error: `no folder "${p}" in this layout — valid: ${validSelectors()}` };
|
|
118
|
+
}
|
|
119
|
+
const existing = sel.indexes.find((x) => x.path === p);
|
|
120
|
+
if (existing) existing.named = true; // upgrade, whatever the order
|
|
121
|
+
else sel.indexes.push({ path: p, named: true });
|
|
122
|
+
} else {
|
|
123
|
+
return { error: `unknown selector "${raw}" — valid: ${validSelectors()}` };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return sel;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Per-target politeness skips for bare invocation. `explicit` (the human
|
|
130
|
+
// named the target with --only) suppresses them wholesale — an explicitly
|
|
131
|
+
// requested target is always produced. Each returns a reason string or null.
|
|
132
|
+
//
|
|
133
|
+
// codemap: skip only a fresh vault (file absent AND zero code_refs) — a
|
|
134
|
+
// deleted code-map.md on a vault with refs IS re-minted by bare --write.
|
|
135
|
+
// graph: STRICTER — skip whenever graph.md is absent (spec contract 1): a
|
|
136
|
+
// routine `reconcile --write` after a plugin upgrade must not silently mint
|
|
137
|
+
// a new root file, and near-simultaneous first creation on two machines
|
|
138
|
+
// would mint an iCloud conflicted copy. Deletion therefore reads as first
|
|
139
|
+
// creation; the standing signal is doctor's missing-graph info.
|
|
140
|
+
const SKIP = {
|
|
141
|
+
"kanban.mjs": null,
|
|
142
|
+
"codemap.mjs": (g, onDisk) =>
|
|
143
|
+
onDisk === null && g.stats && g.stats.epics_with_refs === 0 && g.stats.story_rows === 0
|
|
144
|
+
? "no code_refs anywhere and no existing file"
|
|
145
|
+
: null,
|
|
146
|
+
"graph.mjs": (g, onDisk) =>
|
|
147
|
+
onDisk === null
|
|
148
|
+
? "graph.md does not exist yet — create it explicitly (--only graph or /projectstore:graph)"
|
|
149
|
+
: null,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Compute one generator-backed target (kanban, code-map, graph).
|
|
153
|
+
// `fallbackPath` keeps the report shape ({path, …}) even when the generator
|
|
154
|
+
// itself failed and never told us where its target lives.
|
|
155
|
+
function derivedTarget(script, explicit = false, fallbackPath = null) {
|
|
156
|
+
const g = runGenerator(script);
|
|
157
|
+
if (g.error) return { ...(fallbackPath ? { path: fallbackPath } : {}), error: g.error };
|
|
158
|
+
let onDisk = null;
|
|
159
|
+
try {
|
|
160
|
+
onDisk = existsSync(g.path) ? readFileSync(g.path, "utf8") : null;
|
|
161
|
+
} catch (e) {
|
|
162
|
+
return { path: g.path, error: errMsg(e) };
|
|
163
|
+
}
|
|
164
|
+
const skipWhy = !explicit && SKIP[script] ? SKIP[script](g, onDisk) : null;
|
|
165
|
+
if (skipWhy) {
|
|
166
|
+
return { path: g.path, changed: false, skipped: skipWhy, stats: g.stats };
|
|
167
|
+
}
|
|
168
|
+
const changed = onDisk === null || normalize(onDisk) !== normalize(g.content);
|
|
169
|
+
const base = { path: g.path, changed, stats: g.stats };
|
|
170
|
+
return changed ? { ...base, content: g.content } : base;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Rebuild a folder README's Index table rows from artifact frontmatter,
|
|
174
|
+
// preserving every byte outside the managed table. Pure over the bytes so
|
|
175
|
+
// the check-and-retry writer can recompute from a fresh read.
|
|
176
|
+
// Returns {content} (possibly === original) or {unusable: reason} when the
|
|
177
|
+
// README carries no managed table this script may touch.
|
|
178
|
+
export function rebuildIndexRows(original, folder, artifacts) {
|
|
179
|
+
const lines = original.split("\n");
|
|
180
|
+
// Header matched via the heading registry (PS-SPEC story-002) — ru vaults'
|
|
181
|
+
// localized index headers were unreconcilable while this was an English
|
|
182
|
+
// literal. Unrecognized headers surface as a doctor index-header finding.
|
|
183
|
+
const headerRe = indexHeaderRe();
|
|
184
|
+
const headIdx = lines.findIndex((l) => headerRe.test(l));
|
|
185
|
+
if (headIdx === -1) return { unusable: "no recognised index-table header" };
|
|
186
|
+
if (!/^\|[-\s|]+\|$/.test(lines[headIdx + 1] || "")) {
|
|
187
|
+
return { unusable: "malformed separator row under the index header" };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let end = headIdx + 2;
|
|
191
|
+
while (end < lines.length && /^\|/.test(lines[end])) end++;
|
|
192
|
+
|
|
193
|
+
const rows = [];
|
|
194
|
+
const inFolder = artifacts.filter((a) =>
|
|
195
|
+
folder.kind === "epic"
|
|
196
|
+
? a.kind === "epic" && a.rel.startsWith(`${folder.path}/`)
|
|
197
|
+
: a.kind === folder.kind && a.rel === `${folder.path}/${basename(a.rel)}`);
|
|
198
|
+
// Ordering per SPEC-002 contract 8: date ascending (date:, else created:),
|
|
199
|
+
// display number then slug as tiebreak — the grandfathered ADR-001…N order
|
|
200
|
+
// survives because same-date groups tiebreak by number.
|
|
201
|
+
const decorated = inFolder.map((a) => {
|
|
202
|
+
const file = basename(a.rel);
|
|
203
|
+
const idOpts = { prefix: folder.prefix || null };
|
|
204
|
+
return {
|
|
205
|
+
a,
|
|
206
|
+
file,
|
|
207
|
+
date: String(a.fm.date || a.fm.created || ""),
|
|
208
|
+
number: folder.kind === "epic" ? null : displayNumberOf(a.fm, file, idOpts),
|
|
209
|
+
slug: folder.kind === "epic" ? a.rel.split("/")[1].toLowerCase() : slugIdentity(file, idOpts).primary,
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
for (const d of decorated.sort(compareArtifactOrder)) {
|
|
213
|
+
const { a, file, date, number } = d;
|
|
214
|
+
if (folder.kind === "epic") {
|
|
215
|
+
const id = a.rel.split("/")[1];
|
|
216
|
+
rows.push(`| [${id}](./${id}/epic.md) | ${a.fm.title || id} | ${a.fm.status || "planned"} | ${date} |`);
|
|
217
|
+
} else {
|
|
218
|
+
// The display number renders only when present — grandfathered
|
|
219
|
+
// SPEC-NNN rows keep their labels, slug-only rows are labelled by slug.
|
|
220
|
+
const label = number && folder.prefix ? `${folder.prefix}${number}` : file.replace(/\.md$/, "");
|
|
221
|
+
const status = a.fm.status || (folder.numbered ? "proposed" : "draft");
|
|
222
|
+
rows.push(`| [${label}](./${file}) | ${a.fm.title || label} | ${status} | ${date} |`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { content: [...lines.slice(0, headIdx + 2), ...rows, ...lines.slice(end)].join("\n") };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Compute-mode wrapper: read + rebuild, today's {path, folder, changed,
|
|
230
|
+
// content?} shape. null = nothing to reconcile here (absent README or no
|
|
231
|
+
// managed table) — bare invocation filters those out silently.
|
|
232
|
+
export function rebuildIndex(cfg, folder, artifacts) {
|
|
233
|
+
const readmePath = join(cfg.vault_path, folder.path, "README.md");
|
|
234
|
+
let original;
|
|
235
|
+
try {
|
|
236
|
+
if (!existsSync(readmePath)) return null;
|
|
237
|
+
original = readFileSync(readmePath, "utf8");
|
|
238
|
+
} catch (e) {
|
|
239
|
+
return { path: readmePath, folder: folder.path, error: errMsg(e) };
|
|
240
|
+
}
|
|
241
|
+
const r = rebuildIndexRows(original, folder, artifacts);
|
|
242
|
+
if (r.unusable) return null;
|
|
243
|
+
return r.content === original
|
|
244
|
+
? { path: readmePath, folder: folder.path, changed: false }
|
|
245
|
+
: { path: readmePath, folder: folder.path, changed: true, content: r.content };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Apply an index regeneration with check-and-retry (spec contract 3): the
|
|
249
|
+
// README is a source+derived hybrid — its prose is content a human owns and
|
|
250
|
+
// nothing can recompute. Read, rebuild the managed rows, re-read immediately
|
|
251
|
+
// before the atomic replace; on any drift recompute from the fresh bytes.
|
|
252
|
+
// Never writes from stale bytes; after `attempts` collisions it reports an
|
|
253
|
+
// error instead. The microseconds between the final re-read and the rename,
|
|
254
|
+
// and a crash mid-sequence, are the residual risk the spec accepts (no
|
|
255
|
+
// locks, per ADR-010). `rebuild` is (bytes) => {content}|{unusable} —
|
|
256
|
+
// production passes rebuildIndexRows, tests inject a mutating hook.
|
|
257
|
+
export function writeIndexWithRetry(readmePath, rebuild, { attempts = 3 } = {}) {
|
|
258
|
+
let base;
|
|
259
|
+
try {
|
|
260
|
+
if (!existsSync(readmePath)) return { unusable: "README.md does not exist" };
|
|
261
|
+
base = readFileSync(readmePath, "utf8");
|
|
262
|
+
} catch (e) {
|
|
263
|
+
return { error: errMsg(e) };
|
|
264
|
+
}
|
|
265
|
+
for (let i = 0; i < attempts; i++) {
|
|
266
|
+
const r = rebuild(base);
|
|
267
|
+
if (r.unusable) return { unusable: r.unusable };
|
|
268
|
+
let now;
|
|
269
|
+
try {
|
|
270
|
+
now = readFileSync(readmePath, "utf8");
|
|
271
|
+
} catch (e) {
|
|
272
|
+
return { error: errMsg(e) };
|
|
273
|
+
}
|
|
274
|
+
if (now !== base) {
|
|
275
|
+
base = now; // drift during rebuild — recompute from what is there now
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (r.content === base) return { changed: false, written: false };
|
|
279
|
+
try {
|
|
280
|
+
writeFileAtomic(readmePath, r.content);
|
|
281
|
+
} catch (e) {
|
|
282
|
+
return { changed: true, written: false, error: errMsg(e) };
|
|
283
|
+
}
|
|
284
|
+
return { changed: true, written: true };
|
|
285
|
+
}
|
|
286
|
+
return { error: `concurrent edits persisted through ${attempts} attempts — re-run reconcile` };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Apply a generator-backed target. The compute happens here, immediately
|
|
290
|
+
// before its own write — never compute-all-then-write-all — which is what
|
|
291
|
+
// shrinks the staleness window to microseconds (spec contract 1).
|
|
292
|
+
function applyDerived(script, explicit, fallbackPath) {
|
|
293
|
+
const t = derivedTarget(script, explicit, fallbackPath);
|
|
294
|
+
if (t.error) return { ...(t.path ? { path: t.path } : {}), changed: t.changed === true, written: false, error: t.error };
|
|
295
|
+
if (t.skipped) return { path: t.path, changed: false, written: false, skipped: t.skipped, stats: t.stats };
|
|
296
|
+
if (!t.changed) return { path: t.path, changed: false, written: false, stats: t.stats };
|
|
297
|
+
try {
|
|
298
|
+
writeFileAtomic(t.path, t.content);
|
|
299
|
+
return { path: t.path, changed: true, written: true, stats: t.stats };
|
|
300
|
+
} catch (e) {
|
|
301
|
+
return { path: t.path, changed: true, written: false, error: errMsg(e), stats: t.stats };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function runReconcile({ write = false, only = null, projectDir = null, env = null } = {}) {
|
|
306
|
+
_projectDir = projectDir;
|
|
307
|
+
_env = env;
|
|
308
|
+
const cfg = projectDir ? readConfigAt(projectDir) : readConfig();
|
|
309
|
+
if (!cfg) { const e = new Error("No projectstore config. Run /projectstore:bind first."); e.code = "UNBOUND"; throw e; }
|
|
310
|
+
const layout = loadLayout(cfg.layout);
|
|
311
|
+
const sel = resolveSelection(layout, only);
|
|
312
|
+
if (sel.error) throw new Error(sel.error);
|
|
313
|
+
const artifacts = scanArtifacts(cfg, layout);
|
|
314
|
+
|
|
315
|
+
// Pre-flight every individually NAMED index before any side effect: a
|
|
316
|
+
// named target the vault cannot provide is a config error, and a config
|
|
317
|
+
// error must abort with nothing written. Throwing later, mid-loop, would
|
|
318
|
+
// discard the report while keeping mutations already applied — the one
|
|
319
|
+
// shape contract 1 forbids outright. A target that becomes unusable
|
|
320
|
+
// between this check and its write (a race) degrades to a per-target
|
|
321
|
+
// error below instead.
|
|
322
|
+
for (const { path: folderPath, named } of sel.indexes) {
|
|
323
|
+
if (!named) continue;
|
|
324
|
+
const folder = layout.folders.find((f) => f.path === folderPath);
|
|
325
|
+
const readmePath = join(cfg.vault_path, folder.path, "README.md");
|
|
326
|
+
if (!existsSync(readmePath)) throw new Error(`${folder.path}/README.md does not exist`);
|
|
327
|
+
const r = rebuildIndexRows(readFileSync(readmePath, "utf8"), folder, artifacts);
|
|
328
|
+
if (r.unusable) throw new Error(`${folder.path}/README.md: ${r.unusable}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const out = {};
|
|
332
|
+
|
|
333
|
+
const kanbanPath = layout.kanban ? join(cfg.vault_path, layout.kanban.file || "kanban.md") : null;
|
|
334
|
+
if (!sel.kanban) out.kanban = { skipped: "not selected" };
|
|
335
|
+
else if (!layout.kanban) out.kanban = { skipped: "layout has no kanban" };
|
|
336
|
+
else out.kanban = write ? applyDerived("kanban.mjs", sel.explicit, kanbanPath) : derivedTarget("kanban.mjs", sel.explicit, kanbanPath);
|
|
337
|
+
|
|
338
|
+
const codemapPath = join(cfg.vault_path, "code-map.md");
|
|
339
|
+
if (!sel.codemap) out.codemap = { skipped: "not selected" };
|
|
340
|
+
else out.codemap = write ? applyDerived("codemap.mjs", sel.explicit, codemapPath) : derivedTarget("codemap.mjs", sel.explicit, codemapPath);
|
|
341
|
+
|
|
342
|
+
const graphPath = join(cfg.vault_path, "graph.md");
|
|
343
|
+
if (!sel.graph) out.graph = { skipped: "not selected" };
|
|
344
|
+
else out.graph = write ? applyDerived("graph.mjs", sel.explicit, graphPath) : derivedTarget("graph.mjs", sel.explicit, graphPath);
|
|
345
|
+
|
|
346
|
+
out.indexes = [];
|
|
347
|
+
for (const { path: folderPath, named } of sel.indexes) {
|
|
348
|
+
const folder = layout.folders.find((f) => f.path === folderPath);
|
|
349
|
+
const readmePath = join(cfg.vault_path, folder.path, "README.md");
|
|
350
|
+
if (write) {
|
|
351
|
+
// Index rows are rebuilt from the artifact scan taken at the top of
|
|
352
|
+
// this invocation (already inside the apply call, i.e. after the human
|
|
353
|
+
// approval gap), while the README *bytes* are re-read per attempt.
|
|
354
|
+
// Kanban/code-map recompute fully inline — the asymmetry is deliberate:
|
|
355
|
+
// rescanning every artifact per index would be quadratic for a
|
|
356
|
+
// freshness gain of milliseconds that contract 4 convergence covers.
|
|
357
|
+
const r = writeIndexWithRetry(readmePath, (b) => rebuildIndexRows(b, folder, artifacts));
|
|
358
|
+
if (r.unusable) {
|
|
359
|
+
// Pre-flighted named targets can only reach this via a race.
|
|
360
|
+
if (named) {
|
|
361
|
+
out.indexes.push({ path: readmePath, folder: folder.path, changed: false, written: false, error: r.unusable });
|
|
362
|
+
}
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
out.indexes.push({
|
|
366
|
+
path: readmePath,
|
|
367
|
+
folder: folder.path,
|
|
368
|
+
changed: !!r.changed,
|
|
369
|
+
written: !!r.written,
|
|
370
|
+
...(r.error ? { error: r.error } : {}),
|
|
371
|
+
});
|
|
372
|
+
} else {
|
|
373
|
+
const t = rebuildIndex(cfg, folder, artifacts);
|
|
374
|
+
if (t === null) {
|
|
375
|
+
if (named) {
|
|
376
|
+
out.indexes.push({ path: readmePath, folder: folder.path, error: "absent or no managed index table (changed since pre-flight)" });
|
|
377
|
+
}
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
out.indexes.push(t);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const all = [out.kanban, out.codemap, out.graph, ...out.indexes];
|
|
385
|
+
// `failed` is reported in BOTH modes — a compute-mode generator error must
|
|
386
|
+
// be visible in the summary, not buried in a target the prose's "changed
|
|
387
|
+
// targets" preview never lists. Exit-code policy stays write-scoped
|
|
388
|
+
// (compute remains a reporting tool; see main()).
|
|
389
|
+
out.summary = {
|
|
390
|
+
changed: all.filter((t) => t && t.changed).length,
|
|
391
|
+
failed: all.filter((t) => t && t.error).length,
|
|
392
|
+
};
|
|
393
|
+
if (write) out.summary.written = all.filter((t) => t && t.written).length;
|
|
394
|
+
return out;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function main() {
|
|
398
|
+
const argv = process.argv.slice(2);
|
|
399
|
+
let write = false;
|
|
400
|
+
let only = null;
|
|
401
|
+
for (let i = 0; i < argv.length; i++) {
|
|
402
|
+
const a = argv[i];
|
|
403
|
+
if (a === "--write") write = true;
|
|
404
|
+
else if (a === "--only") only = argv[++i];
|
|
405
|
+
else if (a.startsWith("--only=")) only = a.slice("--only=".length);
|
|
406
|
+
else die(`unknown argument: ${a}`);
|
|
407
|
+
}
|
|
408
|
+
if (only === undefined) die("--only requires a value");
|
|
409
|
+
|
|
410
|
+
let out;
|
|
411
|
+
try {
|
|
412
|
+
out = runReconcile({ write, only });
|
|
413
|
+
} catch (e) {
|
|
414
|
+
die(errMsg(e));
|
|
415
|
+
}
|
|
416
|
+
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
417
|
+
if (write && out.summary.failed > 0) process.exitCode = 1;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (isMain(import.meta.url)) {
|
|
421
|
+
main();
|
|
422
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// projectstore — status line launcher. GENERATED FILE, edits are lost.
|
|
3
|
+
//
|
|
4
|
+
// Why this file exists: the Claude Code `statusLine` slot holds ONE absolute
|
|
5
|
+
// command, and the session reads it at startup. Plugin code lives under a
|
|
6
|
+
// VERSIONED cache path (…/plugins/cache/<marketplace>/projectstore/<version>/),
|
|
7
|
+
// so wiring that path directly meant the rendered line was always the version
|
|
8
|
+
// installed at the PREVIOUS session start — an update showed up one restart
|
|
9
|
+
// late, badge and behaviour both.
|
|
10
|
+
//
|
|
11
|
+
// This launcher is written once and stays valid: it resolves the CURRENTLY
|
|
12
|
+
// installed plugin from Claude Code's own registry on every render, so
|
|
13
|
+
// `/plugin update` + `/reload-plugins` (or any restart) is reflected
|
|
14
|
+
// immediately. The generating root is kept only as a fallback.
|
|
15
|
+
//
|
|
16
|
+
// Never crashes: a status line that throws blanks the user's HUD, so every
|
|
17
|
+
// failure path degrades to an empty line.
|
|
18
|
+
|
|
19
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
20
|
+
import { join, dirname } from "node:path";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { spawnSync } from "node:child_process";
|
|
23
|
+
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
24
|
+
|
|
25
|
+
// Substituted by renderStatusLineLauncher() in scripts/lib.mjs — the fallback
|
|
26
|
+
// root and the harness's own variable names (this file runs before it knows
|
|
27
|
+
// which plugin root to load, so it cannot import harness.mjs to ask).
|
|
28
|
+
const FALLBACK_ROOT = "__PROJECTSTORE_ROOT__";
|
|
29
|
+
const HOME_ENV = "__PROJECTSTORE_HOME_ENV__";
|
|
30
|
+
const PLUGIN_ROOT_ENV = "__PROJECTSTORE_PLUGIN_ROOT_ENV__";
|
|
31
|
+
// The project this launcher was written for (2026-09-06): named at render
|
|
32
|
+
// time, not derived from this file's depth — the file lives under
|
|
33
|
+
// <project>/.projectstore/state/<harness>/ now, and a depth walk would name
|
|
34
|
+
// the state directory.
|
|
35
|
+
const PROJECT_DIR = "__PROJECTSTORE_PROJECT__";
|
|
36
|
+
|
|
37
|
+
function versionKey(v) {
|
|
38
|
+
const p = String(v || "0").split(".").map((n) => parseInt(n, 10) || 0);
|
|
39
|
+
return [p[0] || 0, p[1] || 0, p[2] || 0]; // pad: a missing component must compare as 0, not NaN
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Newest installed projectstore that is actually on disk. Deliberately a
|
|
43
|
+
// standalone copy of installedPluginRoot() in scripts/lib.mjs: importing that
|
|
44
|
+
// helper would mean naming a versioned path, which is the exact bug this file
|
|
45
|
+
// exists to remove.
|
|
46
|
+
function installedRoot() {
|
|
47
|
+
try {
|
|
48
|
+
const claudeHome = (HOME_ENV && process.env[HOME_ENV]) || join(homedir(), ".claude");
|
|
49
|
+
const reg = JSON.parse(
|
|
50
|
+
readFileSync(join(claudeHome, "plugins", "installed_plugins.json"), "utf8"),
|
|
51
|
+
);
|
|
52
|
+
const family = dirname(FALLBACK_ROOT); // …/<marketplace>/projectstore
|
|
53
|
+
// This launcher lives at <project>/<config dir>/<runtime dir>/…: a row the
|
|
54
|
+
// host recorded for THIS checkout (local scope, one per checkout) comes
|
|
55
|
+
// first; another checkout's newer install is the fallback, not the pick.
|
|
56
|
+
let project = null;
|
|
57
|
+
try { project = realpathSync(PROJECT_DIR); } catch { project = PROJECT_DIR || null; }
|
|
58
|
+
const sameProject = (p) => { try { return Boolean(project && p && realpathSync(p) === project); } catch { return false; } };
|
|
59
|
+
const found = [];
|
|
60
|
+
for (const [key, list] of Object.entries((reg && reg.plugins) || {})) {
|
|
61
|
+
if (key !== "projectstore" && !key.startsWith("projectstore@")) continue;
|
|
62
|
+
for (const e of Array.isArray(list) ? list : [list]) {
|
|
63
|
+
const p = e && e.installPath;
|
|
64
|
+
if (typeof p !== "string") continue;
|
|
65
|
+
if (!existsSync(join(p, "scripts", "statusline.mjs"))) continue;
|
|
66
|
+
found.push({
|
|
67
|
+
p,
|
|
68
|
+
same: dirname(p) === family ? 1 : 0, // stay on the marketplace we came from
|
|
69
|
+
mine: sameProject(e && e.projectPath) ? 1 : 0,
|
|
70
|
+
at: Date.parse((e && e.lastUpdated) || "") || 0,
|
|
71
|
+
v: versionKey(e && e.version),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Family is a FILTER, not a tiebreak — an install from another marketplace
|
|
76
|
+
// is someone else's fork of projectstore, not a newer copy of ours, and
|
|
77
|
+
// this file imports whatever it picks. Same rule as installedPluginRoot().
|
|
78
|
+
const pool = found.filter((f) => f.same);
|
|
79
|
+
pool.sort((a, b) => b.mine - a.mine || b.at - a.at || b.v[0] - a.v[0] || b.v[1] - a.v[1] || b.v[2] - a.v[2]);
|
|
80
|
+
return pool.length ? pool[0].p : null;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Try the installed plugin, then the root that generated this file. The second
|
|
87
|
+
// attempt matters: existsSync only proves the script is there, and a truncated
|
|
88
|
+
// or mid-update file throws on import — without a retry a working fallback
|
|
89
|
+
// would sit unused while the user's whole HUD (ours AND the base one we
|
|
90
|
+
// compose over) goes blank. Safe to retry because statusline.mjs prints only
|
|
91
|
+
// at the end of its own main(), so a failed import has written nothing.
|
|
92
|
+
let rendered = false;
|
|
93
|
+
for (const root of [installedRoot(), FALLBACK_ROOT]) {
|
|
94
|
+
if (rendered || !root) continue;
|
|
95
|
+
const target = join(root, "scripts", "statusline.mjs");
|
|
96
|
+
try {
|
|
97
|
+
if (!existsSync(target)) continue;
|
|
98
|
+
// The renderer resolves its own root from the harness's plugin-root
|
|
99
|
+
// variable when set — point it at the install we actually loaded, or the
|
|
100
|
+
// badge, the strings and the breadcrumb would describe a different one.
|
|
101
|
+
if (PLUGIN_ROOT_ENV) process.env[PLUGIN_ROOT_ENV] = root;
|
|
102
|
+
// Renders on import (statusline.mjs runs main() at module load and reads
|
|
103
|
+
// its own stdin) — one process, no extra spawn.
|
|
104
|
+
await import(pathToFileURL(target).href);
|
|
105
|
+
rendered = true;
|
|
106
|
+
} catch {
|
|
107
|
+
// fall through to the next candidate
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Last resort: projectstore is gone (uninstalled, or its cache swept) but this
|
|
112
|
+
// launcher is still wired. Our entry outranks the user's own statusLine, so
|
|
113
|
+
// giving up here would blank the HUD they had before us — in every bound
|
|
114
|
+
// project, with no projectstore left to unwire it. Run their base command
|
|
115
|
+
// instead, exactly as the renderer would have composed over it.
|
|
116
|
+
if (!rendered) {
|
|
117
|
+
let out = null;
|
|
118
|
+
try {
|
|
119
|
+
const stdin = (() => { try { return readFileSync(0, "utf8"); } catch { return ""; } })();
|
|
120
|
+
const projectDir = (() => {
|
|
121
|
+
try {
|
|
122
|
+
const i = JSON.parse(stdin) || {};
|
|
123
|
+
return (i.workspace && i.workspace.project_dir) || i.cwd || null;
|
|
124
|
+
} catch { return null; }
|
|
125
|
+
})();
|
|
126
|
+
const claudeHome = (HOME_ENV && process.env[HOME_ENV]) || join(homedir(), ".claude");
|
|
127
|
+
const candidates = [
|
|
128
|
+
projectDir ? join(projectDir, ".claude", "settings.json") : null,
|
|
129
|
+
join(claudeHome, "settings.json"),
|
|
130
|
+
].filter(Boolean);
|
|
131
|
+
for (const p of candidates) {
|
|
132
|
+
let cmd = null;
|
|
133
|
+
try { cmd = JSON.parse(readFileSync(p, "utf8"))?.statusLine?.command; } catch { continue; }
|
|
134
|
+
if (typeof cmd !== "string" || !cmd.trim()) continue;
|
|
135
|
+
if (cmd.includes("statusline.mjs")) continue; // ours — would recurse
|
|
136
|
+
const r = spawnSync(cmd, { shell: true, input: stdin, encoding: "utf8", timeout: 2000, maxBuffer: 1 << 20 });
|
|
137
|
+
if (r.status === 0 && r.stdout && r.stdout.trim()) { out = r.stdout.replace(/\s+$/, ""); break; }
|
|
138
|
+
}
|
|
139
|
+
} catch {}
|
|
140
|
+
process.stdout.write((out || "") + "\n");
|
|
141
|
+
}
|