@windyroad/style-guide 0.4.6 → 0.4.7-preview.916
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/.claude-plugin/plugin.json +1 -1
- package/hooks/hooks.json +2 -1
- package/hooks/staleness-check.sh +70 -0
- package/lib/install-utils.mjs +99 -23
- package/package.json +1 -1
package/hooks/hooks.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"hooks": {
|
|
3
3
|
"UserPromptSubmit": [
|
|
4
|
-
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/style-guide-eval.sh" }] }
|
|
4
|
+
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/style-guide-eval.sh" }] },
|
|
5
|
+
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/staleness-check.sh" }] }
|
|
5
6
|
],
|
|
6
7
|
"PreToolUse": [
|
|
7
8
|
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/style-guide-enforce-edit.sh" }] }
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Plugin-staleness surfacer — UserPromptSubmit hook (ADR-088 / RFC-036 / P045 / P375).
|
|
3
|
+
#
|
|
4
|
+
# Every turn, compare THIS session's running plugin version (inferred from the
|
|
5
|
+
# hook's own script path in the plugin cache) against the highest version
|
|
6
|
+
# installed on disk for the same plugin key. When the session is behind, emit
|
|
7
|
+
# ONE advisory line telling the user to restart to pick up the newer code.
|
|
8
|
+
#
|
|
9
|
+
# Network-free (own version from script path vs highest semver cache dir),
|
|
10
|
+
# warn-only (never installs or restarts — P343 restart-required means an
|
|
11
|
+
# auto-refresh can't help THIS session anyway), fails quiet on any error
|
|
12
|
+
# (fail-open), silent when the session is current, and emits at most once per
|
|
13
|
+
# newly-detected version (a version-keyed extension of ADR-038's per-session
|
|
14
|
+
# announcement marker) so it costs ~0 tokens on unchanged turns.
|
|
15
|
+
#
|
|
16
|
+
# Each plugin ships its own copy and emits its own line independently — no
|
|
17
|
+
# cross-plugin coordination (eliminates the P260 shared-marker race).
|
|
18
|
+
#
|
|
19
|
+
# Canonical source: packages/shared/hooks/staleness-check.sh. Synced to each
|
|
20
|
+
# consumer plugin by scripts/sync-staleness-check.sh (ADR-017); CI
|
|
21
|
+
# `npm run check:staleness-check` fails on drift.
|
|
22
|
+
|
|
23
|
+
# Fail-open: any unexpected error → silent success, never block a turn.
|
|
24
|
+
set +e
|
|
25
|
+
|
|
26
|
+
# AFK-launched sessions suppress the advisory: the orchestrator handles cache
|
|
27
|
+
# refresh (work-problems Step 6.5) and an AFK subprocess cannot restart itself,
|
|
28
|
+
# so the line would be pure noise. Reuses the established AFK-suppress marker.
|
|
29
|
+
[ "${WR_SUPPRESS_OVERSIGHT_NUDGE:-}" = "1" ] && exit 0
|
|
30
|
+
|
|
31
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" || exit 0
|
|
32
|
+
[ -n "$SCRIPT_DIR" ] || exit 0
|
|
33
|
+
|
|
34
|
+
# Installed layout: <cache>/windyroad/<key>/<version>/hooks/staleness-check.sh
|
|
35
|
+
# → version = basename(dirname(SCRIPT_DIR)); key = basename(dirname(dirname)).
|
|
36
|
+
VERSION_DIR="$(dirname "$SCRIPT_DIR")" # <cache>/windyroad/<key>/<version>
|
|
37
|
+
SELF_VERSION="$(basename "$VERSION_DIR")"
|
|
38
|
+
KEY_DIR="$(dirname "$VERSION_DIR")" # <cache>/windyroad/<key>
|
|
39
|
+
KEY="$(basename "$KEY_DIR")"
|
|
40
|
+
|
|
41
|
+
# Not running from the versioned plugin cache (e.g. source-repo dogfooding at
|
|
42
|
+
# packages/shared/hooks) → own version isn't strict semver → silent.
|
|
43
|
+
case "$SELF_VERSION" in
|
|
44
|
+
[0-9]*.[0-9]*.[0-9]*) : ;;
|
|
45
|
+
*) exit 0 ;;
|
|
46
|
+
esac
|
|
47
|
+
|
|
48
|
+
# Highest strict-semver cache dir for this key. The semver filter drops
|
|
49
|
+
# SHA-named git-source residual dirs that would otherwise win `sort -V` (the
|
|
50
|
+
# recurring cache-refresh trap — see /install-updates / feedback memory).
|
|
51
|
+
HIGHEST="$(ls "$KEY_DIR" 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)"
|
|
52
|
+
[ -n "$HIGHEST" ] || exit 0
|
|
53
|
+
|
|
54
|
+
# Session is current, or somehow ahead of the highest installed → silent.
|
|
55
|
+
[ "$SELF_VERSION" = "$HIGHEST" ] && exit 0
|
|
56
|
+
[ "$(printf '%s\n%s\n' "$SELF_VERSION" "$HIGHEST" | sort -V | tail -1)" = "$HIGHEST" ] || exit 0
|
|
57
|
+
|
|
58
|
+
# Emit once per (session, key, detected-version): a version-keyed extension of
|
|
59
|
+
# ADR-038's /tmp/${SYSTEM}-announced-${SESSION_ID} marker. A newer version
|
|
60
|
+
# detected mid-session yields a new marker key → re-emitted once. Empty
|
|
61
|
+
# SESSION_ID (test/manual) → no marker written, no dedup.
|
|
62
|
+
SESSION_ID="$(cat 2>/dev/null | jq -r '.session_id // empty' 2>/dev/null)"
|
|
63
|
+
if [ -n "$SESSION_ID" ]; then
|
|
64
|
+
MARKER="/tmp/wr-staleness-announced-${SESSION_ID}-${KEY}-${HIGHEST}"
|
|
65
|
+
[ -f "$MARKER" ] && exit 0
|
|
66
|
+
: > "$MARKER" 2>/dev/null
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
echo "${KEY}: this session is on ${SELF_VERSION}, ${HIGHEST} installed — restart to pick it up"
|
|
70
|
+
exit 0
|
package/lib/install-utils.mjs
CHANGED
|
@@ -7,10 +7,12 @@ import { execSync } from "node:child_process";
|
|
|
7
7
|
|
|
8
8
|
const MARKETPLACE_REPO = "windyroad/agent-plugins";
|
|
9
9
|
const MARKETPLACE_NAME = "windyroad";
|
|
10
|
+
const CODEX_MARKETPLACE_PATH = ".";
|
|
11
|
+
const CODEX_MARKETPLACE_NAME = "windyroad-local";
|
|
10
12
|
|
|
11
13
|
let _dryRun = false;
|
|
12
14
|
|
|
13
|
-
export { MARKETPLACE_REPO, MARKETPLACE_NAME };
|
|
15
|
+
export { MARKETPLACE_REPO, MARKETPLACE_NAME, CODEX_MARKETPLACE_PATH, CODEX_MARKETPLACE_NAME };
|
|
14
16
|
|
|
15
17
|
export function setDryRun(value) {
|
|
16
18
|
_dryRun = value;
|
|
@@ -35,16 +37,34 @@ export function run(cmd, label) {
|
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
function runtimesFor(runtime = "claude") {
|
|
41
|
+
if (runtime === "both") return ["claude", "codex"];
|
|
42
|
+
return [runtime];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function checkPrerequisites({ runtime = "claude" } = {}) {
|
|
39
46
|
if (_dryRun) return;
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
for (const currentRuntime of runtimesFor(runtime)) {
|
|
49
|
+
if (currentRuntime === "claude") {
|
|
50
|
+
try {
|
|
51
|
+
execSync("claude --version", { stdio: "pipe" });
|
|
52
|
+
} catch {
|
|
53
|
+
console.error(
|
|
54
|
+
"Error: 'claude' CLI not found. Install Claude Code first:\n https://docs.anthropic.com/en/docs/claude-code\n"
|
|
55
|
+
);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
} else if (currentRuntime === "codex") {
|
|
59
|
+
try {
|
|
60
|
+
execSync("codex --version", { stdio: "pipe" });
|
|
61
|
+
} catch {
|
|
62
|
+
console.error(
|
|
63
|
+
"Error: 'codex' CLI not found. Install Codex CLI first:\n https://developers.openai.com/codex\n"
|
|
64
|
+
);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
48
68
|
}
|
|
49
69
|
}
|
|
50
70
|
|
|
@@ -55,6 +75,13 @@ export function addMarketplace() {
|
|
|
55
75
|
);
|
|
56
76
|
}
|
|
57
77
|
|
|
78
|
+
export function addCodexMarketplace() {
|
|
79
|
+
return run(
|
|
80
|
+
`codex plugin marketplace add ${CODEX_MARKETPLACE_PATH}`,
|
|
81
|
+
`Codex marketplace: ${CODEX_MARKETPLACE_NAME}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
58
85
|
export function installPlugin(pluginName, { scope = "project" } = {}) {
|
|
59
86
|
return run(
|
|
60
87
|
`claude plugin install ${pluginName}@${MARKETPLACE_NAME} --scope ${scope}`,
|
|
@@ -62,6 +89,13 @@ export function installPlugin(pluginName, { scope = "project" } = {}) {
|
|
|
62
89
|
);
|
|
63
90
|
}
|
|
64
91
|
|
|
92
|
+
export function installCodexPlugin(pluginName) {
|
|
93
|
+
return run(
|
|
94
|
+
`codex plugin add ${pluginName}@${CODEX_MARKETPLACE_NAME}`,
|
|
95
|
+
pluginName
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
65
99
|
export function updatePlugin(pluginName, { scope = "project" } = {}) {
|
|
66
100
|
return run(
|
|
67
101
|
`claude plugin update "${pluginName}@${MARKETPLACE_NAME}" --scope ${scope}`,
|
|
@@ -69,18 +103,36 @@ export function updatePlugin(pluginName, { scope = "project" } = {}) {
|
|
|
69
103
|
);
|
|
70
104
|
}
|
|
71
105
|
|
|
106
|
+
export function updateCodexMarketplace() {
|
|
107
|
+
return run(
|
|
108
|
+
`codex plugin marketplace add ${CODEX_MARKETPLACE_PATH}`,
|
|
109
|
+
`Codex marketplace: ${CODEX_MARKETPLACE_NAME}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
72
113
|
export function uninstallPlugin(pluginName) {
|
|
73
114
|
return run(`claude plugin uninstall ${pluginName}`, `Removing ${pluginName}`);
|
|
74
115
|
}
|
|
75
116
|
|
|
117
|
+
export function uninstallCodexPlugin(pluginName) {
|
|
118
|
+
return run(`codex plugin remove ${pluginName}`, `Removing ${pluginName}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
76
121
|
/**
|
|
77
122
|
* Install a single package: marketplace add + plugin install.
|
|
78
123
|
*/
|
|
79
|
-
export function installPackage(pluginName, { deps = [], scope = "project" } = {}) {
|
|
124
|
+
export function installPackage(pluginName, { deps = [], scope = "project", runtime = "claude" } = {}) {
|
|
80
125
|
console.log(`\nInstalling @windyroad/${pluginName.replace("wr-", "")} (${scope} scope)...\n`);
|
|
81
126
|
|
|
82
|
-
|
|
83
|
-
|
|
127
|
+
if (runtime === "claude" || runtime === "both") {
|
|
128
|
+
addMarketplace();
|
|
129
|
+
installPlugin(pluginName, { scope });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (runtime === "codex" || runtime === "both") {
|
|
133
|
+
addCodexMarketplace();
|
|
134
|
+
installCodexPlugin(pluginName);
|
|
135
|
+
}
|
|
84
136
|
|
|
85
137
|
if (deps.length > 0) {
|
|
86
138
|
console.log(`\nNote: This plugin works best with:`);
|
|
@@ -90,34 +142,47 @@ export function installPackage(pluginName, { deps = [], scope = "project" } = {}
|
|
|
90
142
|
}
|
|
91
143
|
|
|
92
144
|
console.log(
|
|
93
|
-
`\nDone! Restart Claude Code to activate.\n`
|
|
145
|
+
`\nDone! Restart ${runtime === "codex" ? "Codex" : runtime === "both" ? "Claude Code and Codex" : "Claude Code"} to activate.\n`
|
|
94
146
|
);
|
|
95
147
|
}
|
|
96
148
|
|
|
97
149
|
/**
|
|
98
150
|
* Update a single package.
|
|
99
151
|
*/
|
|
100
|
-
export function updatePackage(pluginName, { scope = "project" } = {}) {
|
|
152
|
+
export function updatePackage(pluginName, { scope = "project", runtime = "claude" } = {}) {
|
|
101
153
|
console.log(`\nUpdating @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
102
154
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
155
|
+
if (runtime === "claude" || runtime === "both") {
|
|
156
|
+
run(
|
|
157
|
+
`claude plugin marketplace update ${MARKETPLACE_NAME}`,
|
|
158
|
+
"Updating marketplace"
|
|
159
|
+
);
|
|
160
|
+
updatePlugin(pluginName, { scope });
|
|
161
|
+
}
|
|
108
162
|
|
|
109
|
-
|
|
163
|
+
if (runtime === "codex" || runtime === "both") {
|
|
164
|
+
updateCodexMarketplace();
|
|
165
|
+
installCodexPlugin(pluginName);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log(`\nDone! Restart ${runtime === "codex" ? "Codex" : runtime === "both" ? "Claude Code and Codex" : "Claude Code"} to apply updates.\n`);
|
|
110
169
|
}
|
|
111
170
|
|
|
112
171
|
/**
|
|
113
172
|
* Uninstall a single package.
|
|
114
173
|
*/
|
|
115
|
-
export function uninstallPackage(pluginName) {
|
|
174
|
+
export function uninstallPackage(pluginName, { runtime = "claude" } = {}) {
|
|
116
175
|
console.log(`\nUninstalling @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
117
176
|
|
|
118
|
-
|
|
177
|
+
if (runtime === "claude" || runtime === "both") {
|
|
178
|
+
uninstallPlugin(pluginName);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (runtime === "codex" || runtime === "both") {
|
|
182
|
+
uninstallCodexPlugin(pluginName);
|
|
183
|
+
}
|
|
119
184
|
|
|
120
|
-
console.log(
|
|
185
|
+
console.log(`\nDone. Restart ${runtime === "codex" ? "Codex" : runtime === "both" ? "Claude Code and Codex" : "Claude Code"} to apply changes.\n`);
|
|
121
186
|
}
|
|
122
187
|
|
|
123
188
|
/**
|
|
@@ -131,6 +196,7 @@ export function parseStandardArgs(argv) {
|
|
|
131
196
|
update: args.includes("--update"),
|
|
132
197
|
dryRun: args.includes("--dry-run"),
|
|
133
198
|
scope: "project",
|
|
199
|
+
runtime: "claude",
|
|
134
200
|
};
|
|
135
201
|
const scopeIdx = args.indexOf("--scope");
|
|
136
202
|
if (scopeIdx !== -1 && args[scopeIdx + 1]) {
|
|
@@ -142,5 +208,15 @@ export function parseStandardArgs(argv) {
|
|
|
142
208
|
process.exit(1);
|
|
143
209
|
}
|
|
144
210
|
}
|
|
211
|
+
const runtimeIdx = args.indexOf("--runtime");
|
|
212
|
+
if (runtimeIdx !== -1 && args[runtimeIdx + 1]) {
|
|
213
|
+
const val = args[runtimeIdx + 1];
|
|
214
|
+
if (["claude", "codex", "both"].includes(val)) {
|
|
215
|
+
flags.runtime = val;
|
|
216
|
+
} else {
|
|
217
|
+
console.error("--runtime requires: claude, codex, or both");
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
145
221
|
return flags;
|
|
146
222
|
}
|