projectstore-claude 0.29.2 → 0.29.3
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 +1 -1
- package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
- package/node_modules/projectstore/.claude-plugin/plugin.json +2 -1
- package/node_modules/projectstore/README.md +27 -7
- package/node_modules/projectstore/commands/bar.md +30 -0
- package/node_modules/projectstore/commands/bind.md +14 -7
- package/node_modules/projectstore/commands/scaffold.md +24 -16
- package/node_modules/projectstore/commands/status.md +1 -1
- package/node_modules/projectstore/commands/statusline.md +1 -1
- package/node_modules/projectstore/docs/alongside.md +167 -0
- package/node_modules/projectstore/docs/extending.md +17 -3
- package/node_modules/projectstore/docs/getting-started.md +18 -9
- package/node_modules/projectstore/docs/harnesses.md +1 -1
- package/node_modules/projectstore/docs/how-it-works.md +7 -0
- package/node_modules/projectstore/harnesses/claude-code.json +18 -3
- package/node_modules/projectstore/harnesses/codex.json +12 -1
- package/node_modules/projectstore/hooks/band-state.d.ts +44 -0
- package/node_modules/projectstore/hooks/band.tsx +594 -0
- package/node_modules/projectstore/hooks/hooks.json +1 -0
- package/node_modules/projectstore/hooks/session-start.mjs +6 -2
- package/node_modules/projectstore/package.json +2 -1
- package/node_modules/projectstore/scaffold/headings.json +8 -0
- package/node_modules/projectstore/scripts/band-shared.mjs +329 -0
- package/node_modules/projectstore/scripts/band.mjs +156 -0
- package/node_modules/projectstore/scripts/binding.mjs +157 -17
- package/node_modules/projectstore/scripts/build-adapters.mjs +26 -7
- package/node_modules/projectstore/scripts/cli.mjs +545 -179
- package/node_modules/projectstore/scripts/convergence.mjs +468 -0
- package/node_modules/projectstore/scripts/doctor-report.mjs +185 -0
- package/node_modules/projectstore/scripts/doctor.mjs +80 -62
- package/node_modules/projectstore/scripts/fetch-shell.mjs +283 -0
- package/node_modules/projectstore/scripts/harness.mjs +178 -7
- package/node_modules/projectstore/scripts/install-harness.mjs +1488 -257
- package/node_modules/projectstore/scripts/kanban.mjs +4 -20
- package/node_modules/projectstore/scripts/lib.mjs +257 -58
- package/node_modules/projectstore/scripts/portable-registration.mjs +62 -15
- package/node_modules/projectstore/scripts/query.mjs +83 -12
- package/node_modules/projectstore/scripts/run-host.mjs +94 -0
- package/node_modules/projectstore/scripts/scaffold.mjs +183 -0
- package/node_modules/projectstore/scripts/stages.mjs +80 -0
- package/node_modules/projectstore/scripts/statusline.mjs +25 -2
- package/node_modules/projectstore/scripts/surfaces.mjs +78 -13
- package/node_modules/projectstore/scripts/term.mjs +729 -39
- package/node_modules/projectstore/scripts/version-guard.mjs +5 -0
- package/node_modules/projectstore/templates/de/strings.json +63 -1
- package/node_modules/projectstore/templates/de/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/en/strings.json +63 -1
- package/node_modules/projectstore/templates/en/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/es/strings.json +63 -1
- package/node_modules/projectstore/templates/es/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/fr/strings.json +63 -1
- package/node_modules/projectstore/templates/fr/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/ru/strings.json +63 -1
- package/node_modules/projectstore/templates/ru/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/zh/strings.json +63 -1
- package/node_modules/projectstore/templates/zh/vault-readme.md.tmpl +13 -0
- package/package.json +2 -2
|
@@ -23,12 +23,26 @@
|
|
|
23
23
|
// (cli.mjs's resolveProject) and supplies the author. Normative: the CLI
|
|
24
24
|
// story's slice-2 plan (PS-CORE); the fresh config's keys mirror
|
|
25
25
|
// commands/bind.md step 3 (a test pins the two together).
|
|
26
|
+
//
|
|
27
|
+
// `init` creates a whole vault (the story "Scaffold is a core verb, and init
|
|
28
|
+
// creates a whole vault…", PS-CORE; the front-door ADR's decision 7): for a
|
|
29
|
+
// directory that is missing, or there and empty, the plan is the binding's
|
|
30
|
+
// AND the scaffold's, refused as one before anything is made; the apply is
|
|
31
|
+
// mkdir, the binding, `git init`, then the scaffold. Naming the vault stays
|
|
32
|
+
// the confirmation, as for `bind` — `init`'s command line names what it
|
|
33
|
+
// writes, which `scaffold --write`'s does not, so `init` asks nothing even at
|
|
34
|
+
// a terminal. A directory that is not empty is bound as before and never
|
|
35
|
+
// touched: no `git init` inside someone's files, no scaffold. For `bind`, and
|
|
36
|
+
// for that `init`, the scaffold plan only decides the "Next" line.
|
|
26
37
|
|
|
27
38
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, realpathSync } from "node:fs";
|
|
39
|
+
import { spawnSync } from "node:child_process";
|
|
28
40
|
import { join, resolve, isAbsolute, dirname } from "node:path";
|
|
29
41
|
import { homedir } from "node:os";
|
|
30
42
|
import { writeFileAtomic, pluginRoot, ensureRuntimeDir, layoutPaths, commandForm } from "./lib.mjs";
|
|
31
|
-
import { configPath as harnessConfigPath } from "./harness.mjs";
|
|
43
|
+
import { configPath as harnessConfigPath, invokedAs } from "./harness.mjs";
|
|
44
|
+
import { planScaffold, applyScaffold } from "./scaffold.mjs";
|
|
45
|
+
import { icon, kv, commandBlock, wrap, PLAIN } from "./term.mjs";
|
|
32
46
|
|
|
33
47
|
export const DEFAULT_LAYOUT = "engineering";
|
|
34
48
|
export const DEFAULT_LANGUAGE = "en";
|
|
@@ -71,13 +85,22 @@ function readExisting(configPath) {
|
|
|
71
85
|
try { return { cfg: JSON.parse(readFileSync(configPath, "utf8")), corrupt: false }; } catch { return { cfg: null, corrupt: true }; }
|
|
72
86
|
}
|
|
73
87
|
|
|
74
|
-
const shellQuote = (s) => (/^[A-Za-z0-9_\/.~-]+$/.test(s) ? s : `'${String(s).replace(/'/g, `'\\''`)}'`);
|
|
88
|
+
export const shellQuote = (s) => (/^[A-Za-z0-9_\/.~-]+$/.test(s) ? s : `'${String(s).replace(/'/g, `'\\''`)}'`);
|
|
89
|
+
|
|
90
|
+
// A directory with no entries at all. A `.DS_Store` makes it not empty: that
|
|
91
|
+
// edge stays visible rather than guessed at. Unreadable is not empty either.
|
|
92
|
+
function isEmptyDir(p) {
|
|
93
|
+
try { return statSync(p).isDirectory() && readdirSync(p).length === 0; } catch { return false; }
|
|
94
|
+
}
|
|
75
95
|
|
|
76
96
|
// What `bind` would write. `state`: "unbound" (fresh bind), "same" (already
|
|
77
97
|
// bound to this vault — nothing to write), "different" (bound elsewhere —
|
|
78
98
|
// needs --rebind). Refusals are the reasons apply() must not run; each has a
|
|
79
99
|
// code: USAGE (exit 2 upstream), MISSING, REBIND, BOUND, UNREADABLE,
|
|
80
|
-
// NO_PROJECT
|
|
100
|
+
// NO_PROJECT, and on `init`'s whole-vault path the scaffold plan's own
|
|
101
|
+
// (exit 1). `buildsVault`: `init` on a missing or empty directory — mkdir
|
|
102
|
+
// (createsVault, missing only), git and the scaffold follow the binding.
|
|
103
|
+
// `scaffold` is the scaffold plan for the vault, on every path that has one.
|
|
81
104
|
export function planBind(projectDir, { vault, layout = null, language = null, rebind = false, init = false, author = "", env = process.env, root = pluginRoot(), home = homedir() } = {}) {
|
|
82
105
|
const refusals = [];
|
|
83
106
|
const configPath = harnessConfigPath(projectDir, env);
|
|
@@ -104,23 +127,81 @@ export function planBind(projectDir, { vault, layout = null, language = null, re
|
|
|
104
127
|
if (vaultPath && vaultExists && !statSync(vaultPath).isDirectory()) refusals.push({ code: "USAGE", message: `${vaultPath} is not a directory` });
|
|
105
128
|
if (vaultPath && !vaultExists && !init) refusals.push({ code: "MISSING", message: `${vaultPath} does not exist — create it first, or run \`projectstore init ${shellQuote(String(vault))}${rebind ? " --rebind" : ""}\` to create and bind in one step` });
|
|
106
129
|
if (state === "different" && !rebind) refusals.push({ code: "REBIND", message: `${projectDir} is bound to ${before.vault_path}; pass --rebind to point it at ${vaultPath} instead (every other setting is kept)` });
|
|
107
|
-
|
|
130
|
+
const scaffold = vaultPath ? planScaffold(vaultPath, { layout: chosenLayout, language: chosenLanguage, root }) : null;
|
|
131
|
+
const missingFolders = Boolean(scaffold && scaffold.ok && scaffold.creates > 0);
|
|
132
|
+
// Only while the vault is there. A binding whose vault directory was deleted
|
|
133
|
+
// made `init` refuse as BOUND, so "run init" — the scaffold verb's own
|
|
134
|
+
// advice for a missing vault — led nowhere; such an `init` rebuilds it
|
|
135
|
+
// under the binding it already has, writing no config.
|
|
136
|
+
if (state === "same" && init && vaultExists) refusals.push({ code: "BOUND", message: `${projectDir} is already bound to ${vaultPath}${missingFolders ? ` — ${commandForm("scaffold", { env })} in a session, or \`projectstore scaffold --write\`, creates the layout's missing folders and READMEs` : ""}` });
|
|
137
|
+
const buildable = Boolean(init && vaultPath && refusals.length === 0 && (!vaultExists || isEmptyDir(vaultPath)));
|
|
138
|
+
// The whole-vault path is refused as one plan: a missing template or folder
|
|
139
|
+
// string stops the mkdir too. Elsewhere the scaffold plan's refusals are not
|
|
140
|
+
// the binding's — a bind into a vault this language cannot scaffold still
|
|
141
|
+
// binds, and its Next line names `status`.
|
|
142
|
+
if (buildable && scaffold) refusals.push(...scaffold.refusals);
|
|
108
143
|
const after = state === "same" ? before : state === "different"
|
|
109
144
|
? { ...before, vault_path: vaultPath, layout: chosenLayout, language: chosenLanguage }
|
|
110
145
|
: { vault_path: vaultPath, layout: chosenLayout, auto_inject: true, language: chosenLanguage, tags: [], default_author: author || "", active_skills: true, approval_mode: "always" };
|
|
111
146
|
const keptKeys = state === "different" ? Object.keys(before).filter((k) => !["vault_path", "layout", "language"].includes(k)).sort() : [];
|
|
147
|
+
const ok = refusals.length === 0;
|
|
112
148
|
return {
|
|
113
149
|
projectDir, configPath, vault: vaultPath, vaultExists, layout: chosenLayout, language: chosenLanguage, ignored,
|
|
114
|
-
state, before, after, keptKeys, writes:
|
|
150
|
+
state, before, after, keptKeys, writes: ok && state !== "same", createsVault: Boolean(init && vaultPath && !vaultExists && ok),
|
|
151
|
+
buildsVault: buildable && ok, scaffold, refusals, ok,
|
|
115
152
|
};
|
|
116
153
|
}
|
|
117
154
|
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
155
|
+
// `git init` in a vault `init` just made: no commit, on the person's own
|
|
156
|
+
// init.defaultBranch (no `-b`), quiet, with its stderr captured so git's
|
|
157
|
+
// default-branch hint never reaches the person — its exit status alone
|
|
158
|
+
// decides done or failed. The vault is the cwd, so a spawn ENOENT can only
|
|
159
|
+
// mean git is not on PATH: the one skip the front-door ADR allows, and
|
|
160
|
+
// doctor's vault-git finding then stays. Every variable that locates a
|
|
161
|
+
// repository is dropped (GIT_LOCAL_ENV, git's own `rev-parse
|
|
162
|
+
// --local-env-vars`): inherited from a hook or a script, GIT_DIR alone would
|
|
163
|
+
// make this init create — or reinitialise — a repository somewhere else.
|
|
164
|
+
// `rev-parse --show-toplevel` from the new directory says whether it lies
|
|
165
|
+
// inside a work tree (realpath'd: macOS answers /private/var for /var); git
|
|
166
|
+
// init still runs, and the result carries that top. A failure carries its
|
|
167
|
+
// remedy, so a caller reading --json can relay it as the text does.
|
|
168
|
+
const GIT_LOCAL_ENV = Object.freeze([
|
|
169
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES", "GIT_CONFIG", "GIT_CONFIG_PARAMETERS", "GIT_CONFIG_COUNT",
|
|
170
|
+
"GIT_OBJECT_DIRECTORY", "GIT_DIR", "GIT_WORK_TREE", "GIT_IMPLICIT_WORK_TREE", "GIT_GRAFT_FILE",
|
|
171
|
+
"GIT_INDEX_FILE", "GIT_NO_REPLACE_OBJECTS", "GIT_REPLACE_REF_BASE", "GIT_PREFIX",
|
|
172
|
+
"GIT_SHALLOW_FILE", "GIT_COMMON_DIR",
|
|
173
|
+
]);
|
|
174
|
+
function runGit(vault, { spawn = spawnSync, env = process.env } = {}) {
|
|
175
|
+
const childEnv = { ...env };
|
|
176
|
+
for (const k of GIT_LOCAL_ENV) delete childEnv[k];
|
|
177
|
+
const opts = { cwd: vault, env: childEnv, encoding: "utf8", timeout: 15000, stdio: ["ignore", "pipe", "pipe"] };
|
|
178
|
+
const absent = (r) => r.error && r.error.code === "ENOENT";
|
|
179
|
+
const failed = (message) => ({ failed: message, remedy: `Run \`git init\` in ${vault}, or ${commandForm("doctor", { args: "--fix", env })}, which offers it.` });
|
|
180
|
+
const top = spawn("git", ["rev-parse", "--show-toplevel"], opts);
|
|
181
|
+
if (absent(top)) return { skipped: "git is not on PATH" };
|
|
182
|
+
const inside = top.status === 0 && top.stdout && top.stdout.trim() ? top.stdout.trim() : null;
|
|
183
|
+
const r = spawn("git", ["init", "--quiet"], opts);
|
|
184
|
+
if (absent(r)) return { skipped: "git is not on PATH" };
|
|
185
|
+
if (r.error) return failed(r.error.message);
|
|
186
|
+
if (r.status !== 0) return failed(String(r.stderr || "").trim() || `git init exited with ${r.status}`);
|
|
187
|
+
return inside && realOr(inside) !== realOr(vault) ? { done: true, inside } : { done: true };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const SCAFFOLD_REMEDY = "The binding is written; `projectstore scaffold --write` finishes the vault.";
|
|
191
|
+
|
|
192
|
+
// Writes what the plan says, in the story's order: the vault directory when
|
|
193
|
+
// init asked for it (not when it is there and empty), the config through
|
|
194
|
+
// writeFileAtomic (its directory made first — the helper never mkdirs), then,
|
|
195
|
+
// on init's whole-vault path, `git init` and the scaffold. Refuses to run a
|
|
196
|
+
// plan with refusals; a "same" plan writes nothing unless it rebuilds a vault
|
|
197
|
+
// that went missing. A git or scaffold step that fails is reported in `done`
|
|
198
|
+
// and the next step still runs — the binding is already written, so a re-run
|
|
199
|
+
// of `init` would only be refused, and the output names the one command that
|
|
200
|
+
// finishes the vault instead. `spawn` is a parameter so tests can stand in
|
|
201
|
+
// for git.
|
|
202
|
+
export function applyBind(plan, { spawn = spawnSync, env = process.env } = {}) {
|
|
122
203
|
if (!plan.ok) throw Object.assign(new Error(plan.refusals.map((r) => r.message).join("; ")), { code: plan.refusals[0].code });
|
|
123
|
-
const done = { created_vault: false, wrote_config: false };
|
|
204
|
+
const done = { created_vault: false, wrote_config: false, git: null, scaffold: null };
|
|
124
205
|
if (plan.createsVault) { mkdirSync(plan.vault, { recursive: true }); done.created_vault = true; }
|
|
125
206
|
if (plan.writes) {
|
|
126
207
|
// Contract 5 names bind among the writers of the line-merged
|
|
@@ -138,28 +219,87 @@ export function applyBind(plan) {
|
|
|
138
219
|
writeFileAtomic(plan.configPath, JSON.stringify(plan.after, null, 2) + "\n");
|
|
139
220
|
done.wrote_config = true;
|
|
140
221
|
}
|
|
222
|
+
if (plan.buildsVault) {
|
|
223
|
+
done.git = runGit(plan.vault, { spawn, env });
|
|
224
|
+
try { done.scaffold = applyScaffold(plan.scaffold); } catch (e) { done.scaffold = { failed: e && e.message ? e.message : String(e), remedy: SCAFFOLD_REMEDY }; }
|
|
225
|
+
}
|
|
141
226
|
return done;
|
|
142
227
|
}
|
|
143
228
|
|
|
144
229
|
// The result a front-end reports: the decided values, never the file body.
|
|
230
|
+
// `git` and `scaffold` are null unless `init` built the vault: git is
|
|
231
|
+
// {done: true[, inside]}, {skipped: reason} or {failed: message, remedy};
|
|
232
|
+
// scaffold is {created, exists} or {failed: message, remedy}.
|
|
145
233
|
export function bindResult(plan, done) {
|
|
146
234
|
return {
|
|
147
235
|
state: plan.state, vault_path: plan.vault, vault_exists: Boolean(done && done.created_vault) || plan.vaultExists, layout: plan.layout, language: plan.language,
|
|
148
236
|
config_path: plan.configPath, wrote: Boolean(done && done.wrote_config), created_vault: Boolean(done && done.created_vault),
|
|
149
237
|
kept_keys: plan.keptKeys, ignored: plan.ignored, refusals: plan.refusals,
|
|
238
|
+
git: (done && done.git) || null, scaffold: (done && done.scaffold) || null,
|
|
150
239
|
};
|
|
151
240
|
}
|
|
152
241
|
|
|
153
|
-
|
|
242
|
+
// Whether a run left the vault unfinished: a failed git init or scaffold is
|
|
243
|
+
// exit 1 with `ok: false`, though everything before it was written.
|
|
244
|
+
export function bindFailed(done) {
|
|
245
|
+
return Boolean(done && ((done.git && done.git.failed) || (done.scaffold && done.scaffold.failed)));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// A line of prose, wrapped on a live terminal with a hanging indent of two
|
|
249
|
+
// (presentation spec contract 5); `lead` is what the first line starts
|
|
250
|
+
// with. A path or a command inside it is one word and never broken.
|
|
251
|
+
const prose = (c, text, lead = "") => lead + wrap(text, c.live ? c.width : 0, lead + " ");
|
|
252
|
+
|
|
253
|
+
// The bind Next lines, one decision: the scaffold plan has a `create` row.
|
|
254
|
+
// Both forms — the session's and the bin's — because the bind interview
|
|
255
|
+
// relays this to a person who runs slash commands, and a git-marketplace
|
|
256
|
+
// install has no bin on PATH: the terminal's form is the package this run was
|
|
257
|
+
// invoked as, through npx (harness.mjs invokedAs — a shell's own name when it
|
|
258
|
+
// came through one). `--write`: at a terminal that form shows the plan and
|
|
259
|
+
// asks anyway. Each command stands on its own line, after a blank one
|
|
260
|
+
// (presentation spec contract 6), so a copy takes exactly the command.
|
|
261
|
+
function nextLines(p, env, c) {
|
|
262
|
+
const { cmd } = invokedAs(env);
|
|
263
|
+
return p.scaffold && p.scaffold.ok && p.scaffold.creates > 0
|
|
264
|
+
? ["", prose(c, "Next: create the layout's missing folders and READMEs, in a session or from a terminal:"), ...commandBlock(c, [commandForm("scaffold", { env }), `${cmd} scaffold --write`])]
|
|
265
|
+
: ["", prose(c, "Next: see where the project stands:"), ...commandBlock(c, [`${cmd} status`])];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// One line per step that ran. The whole-vault path's own Next lines: the
|
|
269
|
+
// derived views are reconcile's, offered rather than run.
|
|
270
|
+
function vaultSteps(p, done, c, env) {
|
|
271
|
+
const lines = [];
|
|
272
|
+
const g = done.git;
|
|
273
|
+
if (g && g.done) lines.push(prose(c, `Initialised a git repository in ${p.vault} (no commit).`));
|
|
274
|
+
if (g && g.inside) lines.push(prose(c, `\`${p.vault}\` is inside the git work tree at \`${g.inside}\`; the vault gets its own repository, which that repository will see as an embedded one.`));
|
|
275
|
+
if (g && g.skipped) lines.push(prose(c, `Skipped git init: ${g.skipped}. The vault works without one; doctor's vault-git finding stays until it is a repository.`));
|
|
276
|
+
if (g && g.failed) lines.push(prose(c, `git init failed: ${g.failed}`), prose(c, g.remedy, " "));
|
|
277
|
+
const s = done.scaffold;
|
|
278
|
+
if (s && s.failed) lines.push(prose(c, `Scaffold failed: ${s.failed}`), prose(c, s.remedy, " "));
|
|
279
|
+
else if (s) lines.push(prose(c, `Scaffolded the ${p.layout} layout (${p.language}): ${s.created.length} folders and READMEs created${s.exists.length ? `, ${s.exists.length} already there` : ""}.`));
|
|
280
|
+
if (!bindFailed(done)) lines.push("", prose(c, `Next, optional: generate the derived views ${icon(c, "dash")} kanban.md, graph.md and code-map.md:`), ...commandBlock(c, [`${invokedAs(env).cmd} reconcile --write`]));
|
|
281
|
+
return lines;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// bind's and init's screen: what was written, the binding as key–value lines,
|
|
285
|
+
// then what to run next (presentation spec contracts 5 and 6). Its prose
|
|
286
|
+
// wraps on a live terminal. A refusal prints its message as it is. `caps` is
|
|
287
|
+
// the stream's (term.mjs); the default is plain text.
|
|
288
|
+
export function renderBindPlan(p, done = null, { env = process.env, caps: c = PLAIN } = {}) {
|
|
154
289
|
const lines = [];
|
|
155
290
|
if (!p.ok) { for (const r of p.refusals) lines.push(r.message); return lines.join("\n") + "\n"; }
|
|
156
|
-
|
|
157
|
-
|
|
291
|
+
const ignoredNote = p.ignored.length ? ` ${icon(c, "dash")} --${p.ignored.join(" and --")} ignored: a change of ${p.ignored.join("/")} is not a rebind (edit the config, or rebind to another vault)` : "";
|
|
292
|
+
if (p.state === "same" && !(done && p.buildsVault)) {
|
|
293
|
+
lines.push(prose(c, `Already bound to ${p.vault}${ignoredNote}.`), ...nextLines(p, env, c));
|
|
158
294
|
return lines.join("\n") + "\n";
|
|
159
295
|
}
|
|
160
|
-
if (done && done.created_vault) lines.push(`Created ${p.vault}`);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
296
|
+
if (done && done.created_vault) lines.push(prose(c, `Created ${p.vault}`));
|
|
297
|
+
if (p.state === "same") lines.push(prose(c, `Already bound to ${p.vault}${ignoredNote}; its directory was missing, so init made the vault again.`));
|
|
298
|
+
else {
|
|
299
|
+
lines.push(prose(c, `Wrote ${p.configPath}${p.state === "different" ? ` (rebind from ${p.before.vault_path}; kept: ${p.keptKeys.join(", ") || "nothing else"})` : ""}`));
|
|
300
|
+
lines.push(...kv(c, [["vault_path", p.vault], ["layout", p.layout], ["language", p.language]]));
|
|
301
|
+
}
|
|
302
|
+
if (done && p.buildsVault) lines.push(...vaultSteps(p, done, c, env));
|
|
303
|
+
else if (done) lines.push(...nextLines(p, env, c));
|
|
164
304
|
return lines.join("\n") + "\n";
|
|
165
305
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
7
7
|
import { dirname, join, relative, resolve } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { emittingHarnesses, sourceHarness, invocation, uiWordPatterns } from "./harness.mjs";
|
|
9
|
+
import { emittingHarnesses, sourceHarness, invocation, uiWordPatterns, hasBand } from "./harness.mjs";
|
|
10
10
|
import { writeFileAtomic } from "./lib.mjs";
|
|
11
11
|
|
|
12
12
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -128,17 +128,24 @@ hand.
|
|
|
128
128
|
|
|
129
129
|
1. Resolve the requested vault path, and the layout and language when the
|
|
130
130
|
request passes \`--layout <name>\` or \`--language <code>\`. If the vault
|
|
131
|
-
exists, use \`bind
|
|
132
|
-
\`init\`.
|
|
131
|
+
exists and holds files, use \`bind\`. If it exists and is empty, use
|
|
132
|
+
\`init\`. If it is missing, use \`init\` when the user asks to create it.
|
|
133
|
+
\`init\` creates the whole vault: the directory (unless it is there and
|
|
134
|
+
empty), its git repository with no commit, and the layout's folders and
|
|
135
|
+
READMEs. Ask for layout and language only when the request has not
|
|
133
136
|
supplied them.
|
|
134
137
|
2. Show the resolved project, vault, verb, layout and language. Ask for explicit
|
|
135
138
|
approval. Naming the vault is the core's non-interactive confirmation.
|
|
136
139
|
3. Run \`node "\${PROJECTSTORE_CORE_ROOT}/bin/projectstore.mjs" <bind|init>
|
|
137
140
|
"<vault-path>" --layout <layout> --language <language> --project "$PWD"
|
|
138
141
|
--json\` and report the result. Use \`--rebind\` only when the user explicitly
|
|
139
|
-
approved replacing an existing binding.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
+
approved replacing an existing binding. If the result carries
|
|
143
|
+
\`git.failed\` or \`scaffold.failed\`, the binding is written; relay the
|
|
144
|
+
remedy and continue.
|
|
145
|
+
4. Run \`status --json\` to verify the stored path and policies. After
|
|
146
|
+
\`bind\` (an \`init\` vault is already scaffolded), run \`scaffold --json
|
|
147
|
+
--project "$PWD"\` and offer \`$projectstore-scaffold\` only when a row
|
|
148
|
+
reads \`create\`.
|
|
142
149
|
|
|
143
150
|
Codex plugin updates are performed with the \`projectstore-codex upgrade\`
|
|
144
151
|
installer command; this skill never sends the user to another harness's plugin
|
|
@@ -159,6 +166,14 @@ running it; then run \`upgrade --harness codex --json\`, whose envelope is the
|
|
|
159
166
|
result — \`--json\` never waits on a terminal's question. Unsupported
|
|
160
167
|
surfaces remain unsupported; do not create host configuration by hand. Never
|
|
161
168
|
claim a fix after a non-zero exit.`,
|
|
169
|
+
},
|
|
170
|
+
bar: {
|
|
171
|
+
description: "Explain ProjectStore band availability for Codex. This surface is unsupported and the skill never writes configuration. Arguments: on | off | status | epic [<ID>].",
|
|
172
|
+
body: `ProjectStore's band above the prompt is not supported by the Codex
|
|
173
|
+
harness: Codex has no surface that draws above the prompt and no module
|
|
174
|
+
runtime to draw it. Do not run the band verb's writes and do not edit project
|
|
175
|
+
or host configuration. Report the surface as unsupported. For project state
|
|
176
|
+
use \`$projectstore-status\`.`,
|
|
162
177
|
},
|
|
163
178
|
statusline: {
|
|
164
179
|
description: "Explain ProjectStore status-line availability for Codex. This surface is unsupported and the skill never writes configuration. Arguments: on | off | status.",
|
|
@@ -209,8 +224,12 @@ function renderRole(file) {
|
|
|
209
224
|
return [join("skills", name, "SKILL.md"), skill(name, description, orchestration + body)];
|
|
210
225
|
}
|
|
211
226
|
|
|
212
|
-
function renderHooks(manifest, root = ROOT) {
|
|
227
|
+
export function renderHooks(manifest, root = ROOT) {
|
|
213
228
|
const source = json(join(root, "hooks", "hooks.json"));
|
|
229
|
+
// The band is a hooks module only a harness with a band runs (the band
|
|
230
|
+
// spec, contract 1): elsewhere `modules` would name a file that harness
|
|
231
|
+
// cannot load, so its tree carries the classic hooks alone.
|
|
232
|
+
if (!hasBand(manifest)) delete source.modules;
|
|
214
233
|
const post = source.hooks?.PostToolUse || [];
|
|
215
234
|
for (const group of post) if (group.matcher) group.matcher = (manifest.tools?.write_tools || []).join("|");
|
|
216
235
|
const rewrite = (value) => {
|