projectstore-codex 0.28.2 → 0.29.0
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 +1 -1
- package/README.md +1 -1
- package/bin/projectstore-codex.mjs +14 -9
- package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
- package/node_modules/projectstore/.claude-plugin/plugin.json +1 -1
- package/node_modules/projectstore/README.md +2 -2
- package/node_modules/projectstore/harnesses/claude-code.json +3 -0
- package/node_modules/projectstore/harnesses/codex.json +4 -0
- package/node_modules/projectstore/package.json +1 -1
- package/node_modules/projectstore/scripts/build-adapters.mjs +8 -5
- package/node_modules/projectstore/scripts/cli.mjs +173 -26
- package/node_modules/projectstore/scripts/install-harness.mjs +336 -93
- package/node_modules/projectstore/scripts/term.mjs +147 -0
- package/package.json +2 -2
- package/skills/projectstore-agents/SKILL.md +5 -4
- package/skills/projectstore-doctor/SKILL.md +3 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// projectstore — term.mjs: terminal presentation for the write verbs, with no
|
|
2
|
+
// dependency (the zero-dependency property the MCP and link-graph ADRs keep).
|
|
3
|
+
//
|
|
4
|
+
// One question decides everything here: is this a person at a terminal? Then
|
|
5
|
+
// colour and in-place step lines. Otherwise — a pipe, an agent's tool call,
|
|
6
|
+
// CI — the same layout as plain text, so a reader that is not a terminal loses
|
|
7
|
+
// nothing but the escapes (install spec, contract 18).
|
|
8
|
+
//
|
|
9
|
+
// caps(stream, env) → { color, live, width, ascii }
|
|
10
|
+
// painter(caps) → paint(style, text)
|
|
11
|
+
// icon(caps, name) → the glyph for an action or a result
|
|
12
|
+
// duration(ms) → "0.4s", "12s", "1m 03s"
|
|
13
|
+
// wrap(text, width, indent) → prose broken at spaces, hanging indent
|
|
14
|
+
// stepReporter(stream, caps) → { start(label), end(ok, note), abort() }
|
|
15
|
+
// askLine(question, in, out) → the answer, or null on end of input
|
|
16
|
+
|
|
17
|
+
import { createInterface } from "node:readline/promises";
|
|
18
|
+
|
|
19
|
+
// FORCE_COLOR decides when it is set, as Node's own tty reads it: "", "1",
|
|
20
|
+
// "2", "3" or "true" turn colour on even without a terminal, anything else
|
|
21
|
+
// ("0", "false") turns it off. Otherwise NO_COLOR (https://no-color.org) turns
|
|
22
|
+
// colour off when set to anything non-empty, and TERM=dumb means a terminal
|
|
23
|
+
// that understands no escapes.
|
|
24
|
+
export function caps(stream = process.stdout, env = process.env) {
|
|
25
|
+
const tty = Boolean(stream && stream.isTTY);
|
|
26
|
+
const dumb = env.TERM === "dumb";
|
|
27
|
+
const off = env.NO_COLOR !== undefined && env.NO_COLOR !== "";
|
|
28
|
+
const color = env.FORCE_COLOR !== undefined ? ["", "1", "2", "3", "true"].includes(String(env.FORCE_COLOR)) : tty && !dumb && !off;
|
|
29
|
+
const columns = tty && Number(stream.columns) > 0 ? Number(stream.columns) : 100;
|
|
30
|
+
// `width` lays out prose (never below 40); `columns` is the terminal's
|
|
31
|
+
// real width, which a live line must not reach.
|
|
32
|
+
return { color, live: tty && !dumb, width: Math.max(40, Math.min(columns, 120)), columns, ascii: dumb };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SGR = {
|
|
36
|
+
bold: [1, 22], dim: [2, 22], red: [31, 39], green: [32, 39], yellow: [33, 39],
|
|
37
|
+
blue: [34, 39], magenta: [35, 39], cyan: [36, 39], gray: [90, 39],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Raw SGR rather than util.styleText: that arrived in Node 20.12 and 21.7, and
|
|
41
|
+
// the package promises >=20.0.0.
|
|
42
|
+
export function painter(c) {
|
|
43
|
+
return (style, text) => {
|
|
44
|
+
const s = String(text);
|
|
45
|
+
if (!c.color || !SGR[style] || !s) return s;
|
|
46
|
+
return `\x1b[${SGR[style][0]}m${s}\x1b[${SGR[style][1]}m`;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const ICONS = {
|
|
51
|
+
create: ["+", "+"], update: ["↻", "~"], migrate: ["↻", "~"], refresh: ["↻", "~"],
|
|
52
|
+
remove: ["✕", "x"], cleanup: ["✕", "x"], skip: ["·", "."], refuse: ["!", "!"],
|
|
53
|
+
ok: ["✓", "ok"], fail: ["✗", "FAIL"], running: ["…", "..."], note: ["!", "!"],
|
|
54
|
+
};
|
|
55
|
+
export function icon(c, name) {
|
|
56
|
+
const pair = ICONS[name] || ICONS.update;
|
|
57
|
+
return c.ascii ? pair[1] : pair[0];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function duration(ms) {
|
|
61
|
+
if (ms < 10_000) return `${(ms / 1000).toFixed(1)}s`;
|
|
62
|
+
if (ms < 60_000) return `${Math.round(ms / 1000)}s`;
|
|
63
|
+
const m = Math.floor(ms / 60_000), s = Math.round((ms % 60_000) / 1000);
|
|
64
|
+
return `${m}m ${String(s).padStart(2, "0")}s`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Strip SGR escapes — for measuring a painted string and for tests.
|
|
68
|
+
export const plain = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, "");
|
|
69
|
+
|
|
70
|
+
// Prose only: broken at spaces to `width` columns, every line after the first
|
|
71
|
+
// indented by `indent`. A word longer than the line — a path, a command — is
|
|
72
|
+
// never broken: a path cut in two cannot be copied back. Width 0 is a reader
|
|
73
|
+
// that is not a terminal: nothing is broken, so a search never meets a split.
|
|
74
|
+
export function wrap(text, width, indent = "") {
|
|
75
|
+
if (!width) return String(text);
|
|
76
|
+
const room = Math.max(20, width - indent.length);
|
|
77
|
+
const lines = [];
|
|
78
|
+
let line = "";
|
|
79
|
+
for (const word of String(text).split(" ")) {
|
|
80
|
+
if (line && plain(line).length + 1 + plain(word).length > room) { lines.push(line); line = word; }
|
|
81
|
+
else line = line ? `${line} ${word}` : word;
|
|
82
|
+
}
|
|
83
|
+
lines.push(line);
|
|
84
|
+
return lines.map((l, k) => (k ? indent + l : l)).join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A live line must fit one physical row: "\r\x1b[2K" clears only the row the
|
|
88
|
+
// cursor is on, so a label that wrapped would leave its first half behind.
|
|
89
|
+
const fit = (text, room) => (text.length <= room ? text : text.slice(0, Math.max(1, room - 1)) + "…");
|
|
90
|
+
|
|
91
|
+
// One line per step. On a live terminal the line appears when the step starts
|
|
92
|
+
// (ending in "…") and is rewritten in place with the result and duration when
|
|
93
|
+
// it ends; anything else gets the finished line only. Steps run synchronously
|
|
94
|
+
// (spawnSync), so nothing animates in between — the "…" is the honest state.
|
|
95
|
+
export function stepReporter(stream = process.stdout, c = caps(stream), { indent = 2 } = {}) {
|
|
96
|
+
const paint = painter(c);
|
|
97
|
+
let open = null;
|
|
98
|
+
// One column short of the terminal: a line that fills the last column
|
|
99
|
+
// leaves some terminals waiting to wrap.
|
|
100
|
+
const width = Math.min(c.width, c.columns || c.width, 100) - 1;
|
|
101
|
+
const line = (mark, label, right) => {
|
|
102
|
+
const room = width - indent - 2 - plain(right).length - 1;
|
|
103
|
+
const left = `${" ".repeat(indent)}${mark} ${c.live ? fit(label, room) : label}`;
|
|
104
|
+
const gap = Math.max(1, width - plain(left).length - plain(right).length);
|
|
105
|
+
return left + " ".repeat(gap) + right;
|
|
106
|
+
};
|
|
107
|
+
const self = {
|
|
108
|
+
start(label) {
|
|
109
|
+
// A step started while another is still open (a host command run by a
|
|
110
|
+
// rollback) closes the open line where it stands rather than overwrite it.
|
|
111
|
+
if (open && c.live) stream.write("\n");
|
|
112
|
+
open = { label, t0: Date.now() };
|
|
113
|
+
if (c.live) stream.write(line(paint("gray", icon(c, "running")), label, ""));
|
|
114
|
+
},
|
|
115
|
+
end(ok = true, note = "", { kept = false } = {}) {
|
|
116
|
+
if (!open) return;
|
|
117
|
+
const ms = Date.now() - open.t0;
|
|
118
|
+
// A step that ran and left its target in place is neither ✓ nor ✗.
|
|
119
|
+
const mark = !ok ? paint("red", icon(c, "fail")) : kept ? paint("gray", icon(c, "skip")) : paint("green", icon(c, "ok"));
|
|
120
|
+
const right = paint("gray", [note, duration(ms)].filter(Boolean).join(" "));
|
|
121
|
+
const text = line(mark, open.label, right);
|
|
122
|
+
stream.write((c.live ? "\r\x1b[2K" : "") + text + "\n");
|
|
123
|
+
open = null;
|
|
124
|
+
},
|
|
125
|
+
// An exception left a step open: end its line as failed, so whatever is
|
|
126
|
+
// printed next starts on a line of its own.
|
|
127
|
+
abort() { if (open) self.end(false); },
|
|
128
|
+
};
|
|
129
|
+
return self;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// One line of input, read the way a shell's own prompt is: in the terminal's
|
|
133
|
+
// line mode, so readline writes nothing but the question (no cursor escapes),
|
|
134
|
+
// the terminal does the editing, and Ctrl+C is a real SIGINT (exit 130, before
|
|
135
|
+
// anything is written). End of input — Ctrl+D, a closed pipe — is null, which
|
|
136
|
+
// every caller reads as no; it never leaves the question pending (an unsettled
|
|
137
|
+
// promise is exit 13 with a warning). The promise API rejects on close in
|
|
138
|
+
// some Node versions and stays pending in others; both settle here.
|
|
139
|
+
export async function askLine(question, input, output) {
|
|
140
|
+
const rl = createInterface({ input, output, terminal: false });
|
|
141
|
+
return await new Promise((settle) => {
|
|
142
|
+
let done = false;
|
|
143
|
+
const finish = (answer) => { if (done) return; done = true; settle(answer); rl.close(); };
|
|
144
|
+
rl.on("close", () => finish(null));
|
|
145
|
+
rl.question(question).then((a) => finish(a), () => finish(null));
|
|
146
|
+
});
|
|
147
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectstore-codex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "ProjectStore for Codex: portable project memory, rendered workflow skills and lifecycle hooks, with the core pinned and bundled.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"projectstore",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"hooks/"
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"projectstore": "=0.
|
|
43
|
+
"projectstore": "=0.29.0"
|
|
44
44
|
},
|
|
45
45
|
"bundleDependencies": [
|
|
46
46
|
"projectstore"
|
|
@@ -26,10 +26,11 @@ You are managing ProjectStore's Codex agent integration. Require a bound project
|
|
|
26
26
|
|
|
27
27
|
## register / unregister
|
|
28
28
|
|
|
29
|
-
Preview the requested change
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
Preview the requested change with `plan --harness codex --surface agents_block
|
|
30
|
+
--project "$PWD"` and ask for explicit approval. On approval, run the core's
|
|
31
|
+
`install` or `uninstall` verb with `--harness codex --surface agents_block
|
|
32
|
+
--project "$PWD" --json` — `--json` never waits on a terminal's question — and
|
|
33
|
+
report the envelope's result. Never edit the managed block by hand.
|
|
33
34
|
|
|
34
35
|
## status
|
|
35
36
|
|
|
@@ -28,6 +28,8 @@ arguments and `--json`. Summarize every finding without re-deriving it.
|
|
|
28
28
|
When `--fix` is absent, remain read-only. When it is present, separate fixes
|
|
29
29
|
by owner: derived vault views use `$projectstore-reconcile`; Codex plugin or
|
|
30
30
|
agents-block drift uses the core's `upgrade --harness codex` path. Preview
|
|
31
|
-
each mutation and ask for explicit approval before
|
|
31
|
+
each mutation with `plan --harness codex` and ask for explicit approval before
|
|
32
|
+
running it; then run `upgrade --harness codex --json`, whose envelope is the
|
|
33
|
+
result — `--json` never waits on a terminal's question. Unsupported
|
|
32
34
|
surfaces remain unsupported; do not create host configuration by hand. Never
|
|
33
35
|
claim a fix after a non-zero exit.
|