refacil-sdd-ai 4.4.1 → 4.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -23
- package/agents/validator.md +1 -1
- package/bin/cli.js +239 -47
- package/lib/hooks.js +43 -7
- package/lib/ignore-files.js +13 -4
- package/lib/installer.js +179 -15
- package/lib/opencode-plugin/index.js +262 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
**SDD-AI** (Specification-Driven Development with AI) packaged as a CLI.
|
|
4
4
|
|
|
5
|
-
Installs **skills** and **sub-agents** for **Claude Code** and **
|
|
5
|
+
Installs **skills** and **sub-agents** for **Claude Code**, **Cursor**, and **OpenCode** that guide the developer through a structured AI-assisted development workflow, using **`refacil-sdd/`** as the specification store, plus a **local bus** so agents across different repos can communicate with each other.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## Requirements
|
|
10
10
|
|
|
11
11
|
- **Node.js >= 20.0.0**
|
|
12
|
-
- **Claude Code >= 2.1.89**
|
|
12
|
+
- One or more supported IDEs: **Claude Code >= 2.1.89**, **Cursor**, or **OpenCode**
|
|
13
13
|
|
|
14
|
-
`refacil-sdd-ai init` checks the Claude Code version and warns if it is below 2.1.89. With an older version the rest of the methodology works, but `compact-bash` will have no effect.
|
|
14
|
+
`refacil-sdd-ai init` checks the Claude Code version and warns if it is below 2.1.89. With an older version the rest of the methodology works, but `compact-bash` will have no effect (Claude Code only — OpenCode and Cursor have their own hook delivery mechanisms).
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -25,10 +25,12 @@ npm install -g refacil-sdd-ai
|
|
|
25
25
|
|
|
26
26
|
# 2. In the repo root
|
|
27
27
|
refacil-sdd-ai init
|
|
28
|
-
#
|
|
29
|
-
#
|
|
28
|
+
# Interactive IDE selector (Claude Code / Cursor / OpenCode) — pre-selects IDEs
|
|
29
|
+
# whose folder already exists. Use --all to install for all three without prompting.
|
|
30
|
+
# Copies skills and sub-agents to the selected IDEs, configures hooks,
|
|
31
|
+
# and creates/updates .claudeignore, .cursorignore and .opencodeignore.
|
|
30
32
|
|
|
31
|
-
# 3. Restart your
|
|
33
|
+
# 3. Restart your IDE session
|
|
32
34
|
# (new skills are not detected until you restart)
|
|
33
35
|
|
|
34
36
|
# 4. In the IDE
|
|
@@ -43,12 +45,12 @@ npm update -g refacil-sdd-ai
|
|
|
43
45
|
refacil-sdd-ai update # in each repo where it is used
|
|
44
46
|
```
|
|
45
47
|
|
|
46
|
-
In Claude Code
|
|
48
|
+
`update` detects which IDEs are installed by folder presence (`.claude/`, `.cursor/`, `.opencode/`) and only updates those — it never creates IDE directories that did not exist before. In Claude Code and Cursor the `check-update` hook (every session) syncs skills and `compact-guidance`. In OpenCode the equivalent runs via the `session.created` handler of the embedded plugin (`.opencode/plugins/refacil-hooks.js`). Only if the automatic detection (`lib/methodology-migration-pending.js`) finds a pending methodology migration does it write the flag and allow `notify-update` / `tui.prompt.append` to prompt `/refacil:update`. If there is no migration, the user is not interrupted. The `/refacil:update` skill uses `refacil-sdd-ai migration-pending` as the same criterion.
|
|
47
49
|
|
|
48
50
|
### Uninstall
|
|
49
51
|
|
|
50
52
|
```bash
|
|
51
|
-
refacil-sdd-ai clean # in the repo (removes skills + SDD-AI hooks)
|
|
53
|
+
refacil-sdd-ai clean # in the repo (removes skills + SDD-AI hooks for all IDEs)
|
|
52
54
|
npm uninstall -g refacil-sdd-ai
|
|
53
55
|
```
|
|
54
56
|
|
|
@@ -133,7 +135,7 @@ Run **`refacil-sdd-ai help`** for the full list including `bus` and `compact` su
|
|
|
133
135
|
|
|
134
136
|
## Available IDE Skills
|
|
135
137
|
|
|
136
|
-
All invoked as `/refacil:<name>` in Claude Code or
|
|
138
|
+
All invoked as `/refacil:<name>` in Claude Code, Cursor, or OpenCode.
|
|
137
139
|
|
|
138
140
|
### SDD cycle
|
|
139
141
|
|
|
@@ -170,7 +172,7 @@ Some skills delegate their heavy work to **sub-agents** that run in isolated con
|
|
|
170
172
|
|
|
171
173
|
**Model**: `refacil-proposer` runs with `model: opusplan` (uses Opus during plan mode for highest-stakes planning, then switches to Sonnet for execution). Other sub-agents use `model: sonnet` by default.
|
|
172
174
|
|
|
173
|
-
**
|
|
175
|
+
**Triple-platform**: `.claude/agents/refacil-*.md` uses `tools:` (granular allowlist). `.cursor/agents/refacil-*.md` is auto-generated: `readonly: true` for agents without `Edit`/`Write`, `readonly: false` for those that have them; always `model: inherit`. `.opencode/agents/refacil-*.md` is auto-generated via `transformFrontmatterForOpenCode()`: converts `tools:` to a `permission:` block (`edit: allow/deny`, `bash: allow/deny`, `webfetch: deny`), adds `mode: subagent`, adds `hidden: true` for internal agents, and removes `model:`. The installer transforms the frontmatter automatically for all three IDEs.
|
|
174
176
|
|
|
175
177
|
**Two-pass `refacil:bug` flow**: the wrapper first invokes the sub-agent in `investigation` mode (writes nothing) → the user confirms the hypothesis and approves the fix → the wrapper validates the working branch → invokes the sub-agent in `fix` mode to implement.
|
|
176
178
|
|
|
@@ -259,16 +261,23 @@ From there, the full cycle is:
|
|
|
259
261
|
|
|
260
262
|
## Automatic Hooks
|
|
261
263
|
|
|
262
|
-
Installed
|
|
264
|
+
Installed during `init` / `update` for each selected IDE. The same four behaviors are active in Claude Code, Cursor, and OpenCode — each through its own delivery mechanism.
|
|
263
265
|
|
|
264
|
-
|
|
|
265
|
-
|
|
266
|
-
|
|
|
267
|
-
|
|
|
268
|
-
|
|
|
269
|
-
|
|
|
266
|
+
| Behavior | Claude Code | Cursor | OpenCode |
|
|
267
|
+
|---|---|---|---|
|
|
268
|
+
| **check-update** | `SessionStart` hook in `.claude/settings.json` | `SessionStart` hook in `.cursor/settings.json` | `session.created` handler in `.opencode/plugins/refacil-hooks.js` |
|
|
269
|
+
| **notify-update** | `UserPromptSubmit` hook | `beforeSubmitPrompt` hook | `tui.prompt.append` handler |
|
|
270
|
+
| **compact-bash** | `PreToolUse` (Bash) hook | `PreToolUse` (Bash) hook | `tool.execute.before` handler for bash tool |
|
|
271
|
+
| **check-review** | `PreToolUse` (Bash) hook | `PreToolUse` (Bash) hook | `tool.execute.before` handler for bash tool |
|
|
272
|
+
|
|
273
|
+
| Behavior | What it does |
|
|
274
|
+
|---|---|
|
|
275
|
+
| `check-update` | On startup: deletes `.refacil-pending-update` if no migration is pending (stale flags). Then: npm check, sync skills, **compact-guidance**. If skills were synced **and** a migration is pending, writes the flag for `notify-update`. Always refreshes the flag content when a migration is pending (keeps the `to` version current). |
|
|
276
|
+
| `notify-update` | If the flag exists **and** a methodology migration is pending (same table as `/refacil:update`), injects the instruction before the agent processes the next user message; if the sync happened without a migration, the flag is not created or is discarded silently. |
|
|
277
|
+
| `compact-bash` | Silently rewrites bare Bash commands. No extra turns, the IDE does not see the change. Requires Claude Code >= 2.1.89 for the `updatedInput` path. |
|
|
278
|
+
| `check-review` | Intercepts `git push` and blocks if `.review-passed` is missing in any active change. |
|
|
270
279
|
|
|
271
|
-
|
|
280
|
+
> **OpenCode plugin**: a single file (`.opencode/plugins/refacil-hooks.js`) implements all four behaviors. It loads `lib/compact/rules.js` from the package to reuse the same rewrite rules — no duplicated logic. If the rules file is not resolvable, compact-bash is disabled gracefully with a warning to stderr; the plugin never crashes the session.
|
|
272
281
|
|
|
273
282
|
> **Why two hooks for updates?** `SessionStart` does the silent sync when opening the session without user interaction. `notify-update` on `UserPromptSubmit` / `beforeSubmitPrompt` injects the instruction just before the agent processes the next user message, ensuring it is not ignored.
|
|
274
283
|
|
|
@@ -406,18 +415,35 @@ Local bus (WebSocket over `127.0.0.1`) so agents across different repos can comm
|
|
|
406
415
|
|
|
407
416
|
## What Gets Installed in Your Repo
|
|
408
417
|
|
|
418
|
+
Only the IDEs selected during `init` (or detected during `update`) receive files. The three IDE targets are independent — selecting only `.opencode` does not create `.claude/` or `.cursor/` directories.
|
|
419
|
+
|
|
409
420
|
```
|
|
410
|
-
|
|
421
|
+
# Claude Code (if selected)
|
|
422
|
+
.claude/skills/refacil-*/ # Skills (includes refacil-prereqs: METHODOLOGY-CONTRACT.md, BUS-CROSS-REPO.md, …)
|
|
411
423
|
.claude/agents/refacil-*.md # Read-only sub-agents: auditor, investigator, validator
|
|
412
424
|
# Write sub-agents: tester, implementer, debugger, proposer
|
|
413
|
-
.claude/settings.json # Hooks: check-update + check-review + compact-bash
|
|
425
|
+
.claude/settings.json # Hooks: check-update + notify-update + check-review + compact-bash
|
|
426
|
+
.claude/.sdd-version # Installed methodology version (used by check-update)
|
|
427
|
+
|
|
428
|
+
# Cursor (if selected)
|
|
414
429
|
.cursor/skills/refacil-*/ # Cursor skills (equivalent)
|
|
415
|
-
.cursor/agents/refacil-*.md # Cursor sub-agents (readonly:true/false + model:inherit auto-generated)
|
|
416
|
-
.cursor/settings.json # Hooks: check-update + check-review + compact-bash
|
|
430
|
+
.cursor/agents/refacil-*.md # Cursor sub-agents (readonly:true/false + model:inherit, auto-generated)
|
|
431
|
+
.cursor/settings.json # Hooks: check-update + notify-update + check-review + compact-bash
|
|
432
|
+
.cursor/.sdd-version # Installed methodology version
|
|
433
|
+
|
|
434
|
+
# OpenCode (if selected)
|
|
435
|
+
.opencode/skills/refacil-*/ # OpenCode skills (byte-for-byte copy — same spec as Claude Code)
|
|
436
|
+
.opencode/agents/refacil-*.md # OpenCode sub-agents (permission block + mode:subagent, auto-generated)
|
|
437
|
+
.opencode/plugins/refacil-hooks.js # Embedded plugin: session.created + tui.prompt.append + tool.execute.before
|
|
438
|
+
.opencode/opencode.json # Created/merged with $schema (user keys preserved)
|
|
439
|
+
.opencode/.sdd-version # Installed methodology version
|
|
440
|
+
|
|
441
|
+
# Shared (IDE-agnostic)
|
|
417
442
|
CLAUDE.md # Minimal index → points to AGENTS.md
|
|
418
|
-
.cursorrules #
|
|
443
|
+
.cursorrules # Cursor format equivalent of CLAUDE.md
|
|
419
444
|
.claudeignore # Base exclusions (node_modules, dist, .env, *.key, etc.)
|
|
420
445
|
.cursorignore # Same content as .claudeignore
|
|
446
|
+
.opencodeignore # Same content as .claudeignore
|
|
421
447
|
AGENTS.md # Project index → generated by /refacil:setup
|
|
422
448
|
# Points to .agents/ + includes auto-managed blocks
|
|
423
449
|
# (compact-guidance and bus presentation)
|
|
@@ -436,6 +462,7 @@ refacil-sdd/ # SDD artifacts store
|
|
|
436
462
|
- [AGENTS.md](https://agents.md/) — universal AI instructions standard
|
|
437
463
|
- [Claude Code](https://claude.ai/code) — Anthropic CLI
|
|
438
464
|
- [Cursor](https://cursor.sh) — AI IDE
|
|
465
|
+
- [OpenCode](https://opencode.ai) — open-source AI development agent
|
|
439
466
|
|
|
440
467
|
## License
|
|
441
468
|
|
package/agents/validator.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: refacil-validator
|
|
3
|
-
description: Validates implementation against SDD specs (CA/CR) and tests. Delegated by /refacil:verify — do not invoke directly.
|
|
3
|
+
description: Validates implementation against SDD specs (CA/CR) and tests. Delegated by /refacil:verify — do not invoke directly. Never modifies files.
|
|
4
4
|
tools: Read, Grep, Glob, Bash
|
|
5
5
|
model: sonnet
|
|
6
6
|
---
|
package/bin/cli.js
CHANGED
|
@@ -12,7 +12,9 @@ const compactBash = require('../lib/compact/bash');
|
|
|
12
12
|
const {
|
|
13
13
|
installSkills,
|
|
14
14
|
installAgents,
|
|
15
|
+
installOpenCodeJson,
|
|
15
16
|
removeSkills,
|
|
17
|
+
removeOpenCodeArtifacts,
|
|
16
18
|
removeOpenspecLegacyAssets,
|
|
17
19
|
createClaudeMd,
|
|
18
20
|
createCursorRules,
|
|
@@ -22,7 +24,7 @@ const {
|
|
|
22
24
|
checkNodeVersion,
|
|
23
25
|
checkClaudeCodeVersion,
|
|
24
26
|
} = require('../lib/installer');
|
|
25
|
-
const { installHooks, uninstallHooks, cleanLegacySettingsHooks } = require('../lib/hooks');
|
|
27
|
+
const { installHooks, uninstallHooks, cleanLegacySettingsHooks, installOpenCodePlugin, uninstallOpenCodePlugin } = require('../lib/hooks');
|
|
26
28
|
const { handleCompact } = require('../lib/commands/compact');
|
|
27
29
|
const { handleBus } = require('../lib/commands/bus');
|
|
28
30
|
const { handleSdd, autoMigrateOpenspec, findProjectRoot } = require('../lib/commands/sdd');
|
|
@@ -119,10 +121,108 @@ function notifyUpdate() {
|
|
|
119
121
|
function repoIsInitialized() {
|
|
120
122
|
return (
|
|
121
123
|
fs.existsSync(path.join(projectRoot, '.claude', 'skills')) ||
|
|
122
|
-
fs.existsSync(path.join(projectRoot, '.cursor', 'skills'))
|
|
124
|
+
fs.existsSync(path.join(projectRoot, '.cursor', 'skills')) ||
|
|
125
|
+
fs.existsSync(path.join(projectRoot, '.opencode', 'skills'))
|
|
123
126
|
);
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Inline readline-based multi-select for IDE targets.
|
|
131
|
+
* Shows a checklist of available IDEs and returns the selected ones.
|
|
132
|
+
* Falls back to all IDEs if stdin is not a TTY.
|
|
133
|
+
* @param {Array<{label: string, value: string, selected: boolean}>} options
|
|
134
|
+
* @returns {Promise<string[]>} selected values
|
|
135
|
+
*/
|
|
136
|
+
function readlineMultiSelect(options) {
|
|
137
|
+
return new Promise((resolve) => {
|
|
138
|
+
const readline = require('readline');
|
|
139
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
140
|
+
|
|
141
|
+
const selected = options.map((o) => o.selected);
|
|
142
|
+
let renderCount = 0;
|
|
143
|
+
|
|
144
|
+
function render() {
|
|
145
|
+
if (renderCount > 0) {
|
|
146
|
+
process.stdout.write(`\x1B[${options.length + 7}A\x1B[J`);
|
|
147
|
+
}
|
|
148
|
+
renderCount++;
|
|
149
|
+
console.log('\n Select IDEs to install refacil-sdd-ai into:');
|
|
150
|
+
console.log(' (space = toggle, enter = confirm, a = toggle all)\n');
|
|
151
|
+
for (let i = 0; i < options.length; i++) {
|
|
152
|
+
const check = selected[i] ? '[x]' : '[ ]';
|
|
153
|
+
console.log(` ${check} ${options[i].label}`);
|
|
154
|
+
}
|
|
155
|
+
console.log('\n Enter numbers to toggle (e.g. 1,2,3) or "a" for all, then Enter to confirm:');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
render();
|
|
159
|
+
|
|
160
|
+
rl.on('line', (line) => {
|
|
161
|
+
const input = line.trim().toLowerCase();
|
|
162
|
+
if (input === '') {
|
|
163
|
+
rl.close();
|
|
164
|
+
resolve(options.filter((_, i) => selected[i]).map((o) => o.value));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (input === 'a') {
|
|
168
|
+
const anySelected = selected.some(Boolean);
|
|
169
|
+
for (let i = 0; i < selected.length; i++) selected[i] = !anySelected;
|
|
170
|
+
} else {
|
|
171
|
+
const nums = input.split(/[\s,]+/).map(Number).filter((n) => !isNaN(n) && n >= 1 && n <= options.length);
|
|
172
|
+
for (const n of nums) selected[n - 1] = !selected[n - 1];
|
|
173
|
+
}
|
|
174
|
+
render();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
rl.on('close', () => {
|
|
178
|
+
resolve(options.filter((_, i) => selected[i]).map((o) => o.value));
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Show interactive IDE selector if TTY and --all not in args.
|
|
185
|
+
* Pre-selects IDEs by folder presence.
|
|
186
|
+
* Returns selected IDE dirs (e.g. ['.claude', '.cursor', '.opencode']).
|
|
187
|
+
*/
|
|
188
|
+
async function selectIDEs() {
|
|
189
|
+
const allFlag = process.argv.includes('--all');
|
|
190
|
+
const allIDEs = ['.claude', '.cursor', '.opencode'];
|
|
191
|
+
|
|
192
|
+
// --all or non-TTY: install all three
|
|
193
|
+
if (allFlag || !process.stdout.isTTY) {
|
|
194
|
+
return allIDEs;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const options = [
|
|
198
|
+
{ label: 'Claude Code (.claude/)', value: '.claude', selected: fs.existsSync(path.join(projectRoot, '.claude')) },
|
|
199
|
+
{ label: 'Cursor (.cursor/)', value: '.cursor', selected: fs.existsSync(path.join(projectRoot, '.cursor')) },
|
|
200
|
+
{ label: 'OpenCode (.opencode/)', value: '.opencode', selected: fs.existsSync(path.join(projectRoot, '.opencode')) },
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
// Try @clack/prompts first, fall back to inline readline
|
|
204
|
+
let selected;
|
|
205
|
+
try {
|
|
206
|
+
// @clack/prompts is an optional peer dep — try to load it without crashing if absent
|
|
207
|
+
const clack = require('@clack/prompts');
|
|
208
|
+
const result = await clack.multiselect({
|
|
209
|
+
message: 'Select IDEs to install refacil-sdd-ai into:',
|
|
210
|
+
options: options.map((o) => ({ label: o.label, value: o.value, selected: o.selected })),
|
|
211
|
+
required: false,
|
|
212
|
+
});
|
|
213
|
+
if (clack.isCancel(result)) {
|
|
214
|
+
console.log('\n Installation cancelled.\n');
|
|
215
|
+
process.exit(0);
|
|
216
|
+
}
|
|
217
|
+
selected = result;
|
|
218
|
+
} catch (_) {
|
|
219
|
+
// @clack/prompts not available — use inline readline fallback
|
|
220
|
+
selected = await readlineMultiSelect(options);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return selected;
|
|
224
|
+
}
|
|
225
|
+
|
|
126
226
|
function semverGt(a, b) {
|
|
127
227
|
if (!a || !b) return false;
|
|
128
228
|
const pa = a.split('.').map(Number);
|
|
@@ -297,7 +397,7 @@ function checkReview() {
|
|
|
297
397
|
|
|
298
398
|
// --- High-level commands ---
|
|
299
399
|
|
|
300
|
-
function init() {
|
|
400
|
+
async function init() {
|
|
301
401
|
console.log('\n refacil-sdd-ai: Initializing SDD-AI methodology...\n');
|
|
302
402
|
|
|
303
403
|
const nodeOk = checkNodeVersion();
|
|
@@ -313,35 +413,69 @@ function init() {
|
|
|
313
413
|
console.log(' Update with: npm install -g @anthropic-ai/claude-code\n');
|
|
314
414
|
}
|
|
315
415
|
|
|
316
|
-
|
|
317
|
-
|
|
416
|
+
// Select target IDEs (interactive selector or --all / non-TTY)
|
|
417
|
+
const selectedIDEs = await selectIDEs();
|
|
418
|
+
|
|
419
|
+
if (selectedIDEs.length === 0) {
|
|
420
|
+
console.log('\n No IDEs selected. Nothing installed.\n');
|
|
421
|
+
console.log(' Re-run with: refacil-sdd-ai init --all to install for all IDEs');
|
|
422
|
+
process.exit(0);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const installClaude = selectedIDEs.includes('.claude');
|
|
427
|
+
const installCursor = selectedIDEs.includes('.cursor');
|
|
428
|
+
const installOpenCode = selectedIDEs.includes('.opencode');
|
|
318
429
|
|
|
319
|
-
const
|
|
430
|
+
const count = installSkills(packageRoot, projectRoot, selectedIDEs);
|
|
431
|
+
const ideList = selectedIDEs.map((d) => `${d}/skills/`).join(', ');
|
|
432
|
+
console.log(` ${count} skills installed in ${ideList}`);
|
|
433
|
+
|
|
434
|
+
const agentsCount = installAgents(packageRoot, projectRoot, selectedIDEs);
|
|
320
435
|
if (agentsCount > 0) {
|
|
321
|
-
|
|
436
|
+
const agentList = selectedIDEs.map((d) => `${d}/agents/`).join(', ');
|
|
437
|
+
console.log(` ${agentsCount} sub-agents installed in ${agentList}`);
|
|
322
438
|
}
|
|
323
439
|
|
|
324
440
|
writeRepoVersion(projectRoot, getPackageVersion(packageRoot));
|
|
325
441
|
|
|
326
|
-
if (
|
|
327
|
-
|
|
442
|
+
if (installClaude) {
|
|
443
|
+
if (createClaudeMd(packageRoot, projectRoot)) console.log(' CLAUDE.md OK');
|
|
444
|
+
if (installHooks('.claude', projectRoot)) {
|
|
445
|
+
console.log(' Hook check-update added to .claude/settings.json');
|
|
446
|
+
}
|
|
447
|
+
}
|
|
328
448
|
|
|
329
|
-
if (
|
|
330
|
-
console.log('
|
|
449
|
+
if (installCursor) {
|
|
450
|
+
if (createCursorRules(packageRoot, projectRoot)) console.log(' .cursorrules OK');
|
|
451
|
+
if (installHooks('.cursor', projectRoot)) {
|
|
452
|
+
console.log(' Hook check-update added to .cursor/hooks.json');
|
|
453
|
+
}
|
|
331
454
|
}
|
|
332
|
-
|
|
333
|
-
|
|
455
|
+
|
|
456
|
+
if (installOpenCode) {
|
|
457
|
+
try {
|
|
458
|
+
installOpenCodeJson(projectRoot);
|
|
459
|
+
console.log(' .opencode/opencode.json created/updated');
|
|
460
|
+
} catch (err) {
|
|
461
|
+
console.error(` Warning: could not create opencode.json: ${err.message}`);
|
|
462
|
+
}
|
|
463
|
+
if (installHooks('.opencode', projectRoot)) {
|
|
464
|
+
console.log(' OpenCode plugin installed to .opencode/plugins/refacil-hooks.js');
|
|
465
|
+
}
|
|
334
466
|
}
|
|
335
467
|
|
|
336
468
|
try {
|
|
337
|
-
const
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
console.log(`
|
|
343
|
-
} else {
|
|
344
|
-
console.log(
|
|
469
|
+
const IDE_TO_IGNORE = { '.claude': '.claudeignore', '.cursor': '.cursorignore', '.opencode': '.opencodeignore' };
|
|
470
|
+
const ignoreResult = syncIgnoreFiles(projectRoot, selectedIDEs);
|
|
471
|
+
const ignoreNames = selectedIDEs.map((d) => IDE_TO_IGNORE[d]).filter(Boolean).join(', ');
|
|
472
|
+
const s = ignoreResult.claude || ignoreResult.cursor || ignoreResult.opencode;
|
|
473
|
+
if (s && s.status === 'created') {
|
|
474
|
+
console.log(` ${ignoreNames} created`);
|
|
475
|
+
} else if (s && s.status === 'updated') {
|
|
476
|
+
console.log(` ${ignoreNames} updated (${s.added} entries added)`);
|
|
477
|
+
} else if (ignoreNames) {
|
|
478
|
+
console.log(` ${ignoreNames} are up to date`);
|
|
345
479
|
}
|
|
346
480
|
} catch (err) {
|
|
347
481
|
console.error(` Warning: could not sync ignore files: ${err.message}`);
|
|
@@ -359,7 +493,7 @@ function init() {
|
|
|
359
493
|
}
|
|
360
494
|
|
|
361
495
|
console.log('\n Next steps:\n');
|
|
362
|
-
console.log(' 1. RESTART your
|
|
496
|
+
console.log(' 1. RESTART your IDE session');
|
|
363
497
|
console.log(' (new skills are not detected until the session is restarted)\n');
|
|
364
498
|
console.log(' 2. Run: /refacil:setup');
|
|
365
499
|
console.log(' (generates AGENTS.md for your project)\n');
|
|
@@ -368,12 +502,29 @@ function init() {
|
|
|
368
502
|
function update() {
|
|
369
503
|
console.log('\n refacil-sdd-ai: Updating skills...\n');
|
|
370
504
|
|
|
371
|
-
|
|
372
|
-
|
|
505
|
+
// Detect installed IDEs by folder presence
|
|
506
|
+
const hasClaudeDir = fs.existsSync(path.join(projectRoot, '.claude'));
|
|
507
|
+
const hasCursorDir = fs.existsSync(path.join(projectRoot, '.cursor'));
|
|
508
|
+
const hasOpenCodeDir = fs.existsSync(path.join(projectRoot, '.opencode'));
|
|
373
509
|
|
|
374
|
-
const
|
|
510
|
+
const detectedIDEs = [
|
|
511
|
+
hasClaudeDir && '.claude',
|
|
512
|
+
hasCursorDir && '.cursor',
|
|
513
|
+
hasOpenCodeDir && '.opencode',
|
|
514
|
+
].filter(Boolean);
|
|
515
|
+
|
|
516
|
+
const count = installSkills(packageRoot, projectRoot, detectedIDEs);
|
|
517
|
+
const installedDirs = detectedIDEs.map((d) => `${d}/skills/`);
|
|
518
|
+
console.log(` ${count} skills updated in ${installedDirs.join(', ') || '(none detected)'}`);
|
|
519
|
+
|
|
520
|
+
const agentsCount = installAgents(packageRoot, projectRoot, detectedIDEs);
|
|
375
521
|
if (agentsCount > 0) {
|
|
376
|
-
|
|
522
|
+
const agentDirs = [
|
|
523
|
+
hasClaudeDir && '.claude/agents/',
|
|
524
|
+
hasCursorDir && '.cursor/agents/',
|
|
525
|
+
hasOpenCodeDir && '.opencode/agents/',
|
|
526
|
+
].filter(Boolean);
|
|
527
|
+
console.log(` ${agentsCount} sub-agents updated in ${agentDirs.join(', ')}`);
|
|
377
528
|
}
|
|
378
529
|
|
|
379
530
|
try {
|
|
@@ -387,25 +538,42 @@ function update() {
|
|
|
387
538
|
|
|
388
539
|
writeRepoVersion(projectRoot, getPackageVersion(packageRoot));
|
|
389
540
|
|
|
390
|
-
|
|
391
|
-
|
|
541
|
+
if (hasClaudeDir) {
|
|
542
|
+
createClaudeMd(packageRoot, projectRoot);
|
|
543
|
+
if (installHooks('.claude', projectRoot)) {
|
|
544
|
+
console.log(' Hook check-update added to .claude/settings.json');
|
|
545
|
+
}
|
|
546
|
+
}
|
|
392
547
|
|
|
393
|
-
if (
|
|
394
|
-
|
|
548
|
+
if (hasCursorDir) {
|
|
549
|
+
createCursorRules(packageRoot, projectRoot);
|
|
550
|
+
if (installHooks('.cursor', projectRoot)) {
|
|
551
|
+
console.log(' Hook check-update added to .cursor/hooks.json');
|
|
552
|
+
}
|
|
395
553
|
}
|
|
396
|
-
|
|
397
|
-
|
|
554
|
+
|
|
555
|
+
if (hasOpenCodeDir) {
|
|
556
|
+
try {
|
|
557
|
+
installOpenCodeJson(projectRoot);
|
|
558
|
+
} catch (err) {
|
|
559
|
+
console.error(` Warning: could not update opencode.json: ${err.message}`);
|
|
560
|
+
}
|
|
561
|
+
if (installHooks('.opencode', projectRoot)) {
|
|
562
|
+
console.log(' OpenCode plugin updated at .opencode/plugins/refacil-hooks.js');
|
|
563
|
+
}
|
|
398
564
|
}
|
|
399
565
|
|
|
400
566
|
try {
|
|
401
|
-
const
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
console.log(`
|
|
407
|
-
} else {
|
|
408
|
-
console.log(
|
|
567
|
+
const IDE_TO_IGNORE = { '.claude': '.claudeignore', '.cursor': '.cursorignore', '.opencode': '.opencodeignore' };
|
|
568
|
+
const ignoreResult = syncIgnoreFiles(projectRoot, detectedIDEs);
|
|
569
|
+
const ignoreNames = detectedIDEs.map((d) => IDE_TO_IGNORE[d]).filter(Boolean).join(', ');
|
|
570
|
+
const s = ignoreResult.claude || ignoreResult.cursor || ignoreResult.opencode;
|
|
571
|
+
if (s && s.status === 'created') {
|
|
572
|
+
console.log(` ${ignoreNames} created`);
|
|
573
|
+
} else if (s && s.status === 'updated') {
|
|
574
|
+
console.log(` ${ignoreNames} updated (${s.added} entries added)`);
|
|
575
|
+
} else if (ignoreNames) {
|
|
576
|
+
console.log(` ${ignoreNames} are up to date`);
|
|
409
577
|
}
|
|
410
578
|
} catch (err) {
|
|
411
579
|
console.error(` Warning: could not sync ignore files: ${err.message}`);
|
|
@@ -422,7 +590,7 @@ function update() {
|
|
|
422
590
|
console.error(` Warning: could not sync compact-guidance: ${err.message}`);
|
|
423
591
|
}
|
|
424
592
|
|
|
425
|
-
console.log('\n RESTART your
|
|
593
|
+
console.log('\n RESTART your IDE session to apply the changes.\n');
|
|
426
594
|
}
|
|
427
595
|
|
|
428
596
|
function clean() {
|
|
@@ -440,6 +608,19 @@ function clean() {
|
|
|
440
608
|
console.log(' SDD-AI hooks removed from .cursor/settings.json');
|
|
441
609
|
}
|
|
442
610
|
|
|
611
|
+
// Clean OpenCode artifacts if .opencode/ directory is present
|
|
612
|
+
if (fs.existsSync(path.join(projectRoot, '.opencode'))) {
|
|
613
|
+
try {
|
|
614
|
+
removeOpenCodeArtifacts(projectRoot);
|
|
615
|
+
console.log(' OpenCode skills and agents removed from .opencode/');
|
|
616
|
+
} catch (err) {
|
|
617
|
+
console.error(` Warning: could not remove OpenCode artifacts: ${err.message}`);
|
|
618
|
+
}
|
|
619
|
+
if (uninstallOpenCodePlugin(projectRoot)) {
|
|
620
|
+
console.log(' OpenCode plugin removed from .opencode/plugins/');
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
443
624
|
try {
|
|
444
625
|
const result = removeCompactGuidance(projectRoot);
|
|
445
626
|
if (result.status === 'removed') {
|
|
@@ -460,8 +641,10 @@ function help() {
|
|
|
460
641
|
refacil-sdd-ai — SDD-AI Methodology
|
|
461
642
|
|
|
462
643
|
Commands:
|
|
463
|
-
init Install skills in .claude/ and .
|
|
464
|
-
|
|
644
|
+
init Install skills in .claude/, .cursor/ and/or .opencode/ (interactive IDE selector).
|
|
645
|
+
Use --all to install for all three IDEs without prompting.
|
|
646
|
+
Creates CLAUDE.md, .cursorrules and .opencode/opencode.json as appropriate.
|
|
647
|
+
update Re-copy skills for all detected IDEs (.claude/, .cursor/, .opencode/)
|
|
465
648
|
migration-pending Same validation as hooks/notify-update: list migrations (exit 1 if any; --json)
|
|
466
649
|
check-update Sync skills and compact-guidance at session start (SessionStart hook)
|
|
467
650
|
notify-update Notify methodology migration only if applicable (UserPromptSubmit hook)
|
|
@@ -496,18 +679,24 @@ function help() {
|
|
|
496
679
|
sdd mark-reviewed <name> Write .review-passed (requires --verdict and --summary)
|
|
497
680
|
sdd tasks-update <name> Mark task N as completed (--task N --done)
|
|
498
681
|
sdd validate-name <name> Validate change name format
|
|
499
|
-
clean Remove skills and SDD-AI hooks from
|
|
682
|
+
clean Remove skills and SDD-AI hooks from all detected IDEs
|
|
683
|
+
(.claude/settings.json, .cursor/hooks.json, .opencode/plugins/)
|
|
500
684
|
help Show this help
|
|
501
685
|
|
|
502
686
|
Full flow:
|
|
503
687
|
1. npm install -g refacil-sdd-ai
|
|
504
688
|
2. refacil-sdd-ai init
|
|
505
|
-
3. RESTART
|
|
689
|
+
3. RESTART your IDE session (Claude Code, Cursor, or OpenCode)
|
|
506
690
|
4. Run: /refacil:setup (generates AGENTS.md for your project)
|
|
507
691
|
|
|
692
|
+
IDE support:
|
|
693
|
+
- Claude Code: .claude/skills/, .claude/agents/, .claude/settings.json hooks
|
|
694
|
+
- Cursor: .cursor/skills/, .cursor/agents/, .cursor/hooks.json hooks
|
|
695
|
+
- OpenCode: .opencode/skills/, .opencode/agents/, .opencode/plugins/refacil-hooks.js
|
|
696
|
+
|
|
508
697
|
Requirements:
|
|
509
698
|
- Node.js >= 20.0.0
|
|
510
|
-
- Claude Code >= 2.1.89 (required by compact-bash for silent rewrite) or
|
|
699
|
+
- Claude Code >= 2.1.89 (required by compact-bash for silent rewrite), Cursor, or OpenCode
|
|
511
700
|
`);
|
|
512
701
|
}
|
|
513
702
|
|
|
@@ -522,7 +711,10 @@ if (command === '--version' || command === '-v') {
|
|
|
522
711
|
|
|
523
712
|
switch (command) {
|
|
524
713
|
case 'init':
|
|
525
|
-
init()
|
|
714
|
+
init().catch((err) => {
|
|
715
|
+
console.error(` Error during init: ${err.message}`);
|
|
716
|
+
process.exit(1);
|
|
717
|
+
});
|
|
526
718
|
break;
|
|
527
719
|
case 'update':
|
|
528
720
|
update();
|
package/lib/hooks.js
CHANGED
|
@@ -219,18 +219,54 @@ function cleanLegacySettingsHooks(projectRoot) {
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
// ── OpenCode: plugin file ────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
function installOpenCodePlugin(projectRoot) {
|
|
225
|
+
const pluginsDir = path.join(projectRoot, '.opencode', 'plugins');
|
|
226
|
+
fs.mkdirSync(pluginsDir, { recursive: true });
|
|
227
|
+
|
|
228
|
+
const srcPlugin = path.join(__dirname, 'opencode-plugin', 'index.js');
|
|
229
|
+
const destPlugin = path.join(pluginsDir, 'refacil-hooks.js');
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
fs.copyFileSync(srcPlugin, destPlugin);
|
|
233
|
+
return true;
|
|
234
|
+
} catch (err) {
|
|
235
|
+
process.stderr.write(`[refacil-sdd-ai] Could not install OpenCode plugin: ${err.message}\n`);
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function uninstallOpenCodePlugin(projectRoot) {
|
|
241
|
+
const pluginPath = path.join(projectRoot, '.opencode', 'plugins', 'refacil-hooks.js');
|
|
242
|
+
if (!fs.existsSync(pluginPath)) return false;
|
|
243
|
+
try {
|
|
244
|
+
fs.unlinkSync(pluginPath);
|
|
245
|
+
return true;
|
|
246
|
+
} catch (err) {
|
|
247
|
+
process.stderr.write(`[refacil-sdd-ai] Could not remove OpenCode plugin: ${err.message}\n`);
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
222
252
|
// ── Fachada pública ──────────────────────────────────────────────────────────
|
|
223
253
|
|
|
224
254
|
function installHooks(ideDir, projectRoot) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
255
|
+
if (ideDir === '.cursor') return installCursorHooks(projectRoot);
|
|
256
|
+
if (ideDir === '.opencode') return installOpenCodePlugin(projectRoot);
|
|
257
|
+
return installClaudeHooks(projectRoot);
|
|
228
258
|
}
|
|
229
259
|
|
|
230
260
|
function uninstallHooks(ideDir, projectRoot) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
261
|
+
if (ideDir === '.cursor') return uninstallCursorHooks(projectRoot);
|
|
262
|
+
if (ideDir === '.opencode') return uninstallOpenCodePlugin(projectRoot);
|
|
263
|
+
return uninstallClaudeHooks(projectRoot);
|
|
234
264
|
}
|
|
235
265
|
|
|
236
|
-
module.exports = {
|
|
266
|
+
module.exports = {
|
|
267
|
+
installHooks,
|
|
268
|
+
uninstallHooks,
|
|
269
|
+
cleanLegacySettingsHooks,
|
|
270
|
+
installOpenCodePlugin,
|
|
271
|
+
uninstallOpenCodePlugin,
|
|
272
|
+
};
|
package/lib/ignore-files.js
CHANGED
|
@@ -79,10 +79,19 @@ function syncIgnoreFile(filePath) {
|
|
|
79
79
|
return { status: 'updated', added: missing.length };
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
function syncIgnoreFiles(projectRoot) {
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
|
|
82
|
+
function syncIgnoreFiles(projectRoot, ideDirs) {
|
|
83
|
+
const dirs = ideDirs || ['.claude', '.cursor', '.opencode'];
|
|
84
|
+
const result = {};
|
|
85
|
+
if (dirs.includes('.claude')) {
|
|
86
|
+
result.claude = syncIgnoreFile(path.join(projectRoot, '.claudeignore'));
|
|
87
|
+
}
|
|
88
|
+
if (dirs.includes('.cursor')) {
|
|
89
|
+
result.cursor = syncIgnoreFile(path.join(projectRoot, '.cursorignore'));
|
|
90
|
+
}
|
|
91
|
+
if (dirs.includes('.opencode')) {
|
|
92
|
+
result.opencode = syncIgnoreFile(path.join(projectRoot, '.opencodeignore'));
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
module.exports = { syncIgnoreFiles, BASE_ENTRIES };
|
package/lib/installer.js
CHANGED
|
@@ -35,7 +35,18 @@ const AGENTS = [
|
|
|
35
35
|
'proposer',
|
|
36
36
|
];
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// All 7 refacil agents are internal sub-agents — hidden from the agent picker in OpenCode
|
|
39
|
+
const INTERNAL_AGENTS = [
|
|
40
|
+
'investigator',
|
|
41
|
+
'validator',
|
|
42
|
+
'auditor',
|
|
43
|
+
'tester',
|
|
44
|
+
'implementer',
|
|
45
|
+
'debugger',
|
|
46
|
+
'proposer',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const REPO_VERSION_FILES = ['.claude/.sdd-version', '.cursor/.sdd-version', '.opencode/.sdd-version'];
|
|
39
50
|
|
|
40
51
|
function copyDir(src, dest) {
|
|
41
52
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -51,23 +62,59 @@ function copyDir(src, dest) {
|
|
|
51
62
|
}
|
|
52
63
|
}
|
|
53
64
|
|
|
54
|
-
function installSkills(packageRoot, projectRoot) {
|
|
65
|
+
function installSkills(packageRoot, projectRoot, ideDirs) {
|
|
66
|
+
const dirs = ideDirs || ['.claude', '.cursor', '.opencode'];
|
|
55
67
|
let installed = 0;
|
|
56
68
|
for (const skill of SKILLS) {
|
|
57
69
|
const srcDir = path.join(packageRoot, 'skills', skill);
|
|
58
70
|
if (!fs.existsSync(srcDir)) continue;
|
|
59
71
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
if (dirs.includes('.claude')) {
|
|
73
|
+
copyDir(srcDir, path.join(projectRoot, '.claude', 'skills', `refacil-${skill}`));
|
|
74
|
+
}
|
|
75
|
+
if (dirs.includes('.cursor')) {
|
|
76
|
+
copyDir(srcDir, path.join(projectRoot, '.cursor', 'skills', `refacil-${skill}`));
|
|
77
|
+
}
|
|
78
|
+
if (dirs.includes('.opencode')) {
|
|
79
|
+
// OpenCode: byte-for-byte copy (same as Claude Code — no transformation needed)
|
|
80
|
+
copyDir(srcDir, path.join(projectRoot, '.opencode', 'skills', `refacil-${skill}`));
|
|
81
|
+
}
|
|
65
82
|
|
|
66
83
|
installed++;
|
|
67
84
|
}
|
|
68
85
|
return installed;
|
|
69
86
|
}
|
|
70
87
|
|
|
88
|
+
// Create or safely merge .opencode/opencode.json with SDD-AI managed keys
|
|
89
|
+
// Preserves any pre-existing keys — never destructive
|
|
90
|
+
function installOpenCodeJson(projectRoot) {
|
|
91
|
+
const ocDir = path.join(projectRoot, '.opencode');
|
|
92
|
+
fs.mkdirSync(ocDir, { recursive: true });
|
|
93
|
+
const ocJsonPath = path.join(ocDir, 'opencode.json');
|
|
94
|
+
|
|
95
|
+
let existing = {};
|
|
96
|
+
if (fs.existsSync(ocJsonPath)) {
|
|
97
|
+
try {
|
|
98
|
+
existing = JSON.parse(fs.readFileSync(ocJsonPath, 'utf8'));
|
|
99
|
+
} catch (_) {
|
|
100
|
+
existing = {};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// SDD-AI managed keys (minimal — only $schema)
|
|
105
|
+
const sddKeys = {
|
|
106
|
+
'$schema': 'https://opencode.ai/config.json',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const merged = Object.assign({}, sddKeys, existing);
|
|
110
|
+
// Ensure $schema is always the SDD-AI value if not already set by user
|
|
111
|
+
if (!existing['$schema']) {
|
|
112
|
+
merged['$schema'] = sddKeys['$schema'];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fs.writeFileSync(ocJsonPath, JSON.stringify(merged, null, 2) + '\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
71
118
|
// Claude Code: tools allowlist granular, model: sonnet|opus|haiku
|
|
72
119
|
// Cursor: readonly: true|false (booleano), model: inherit (default)
|
|
73
120
|
function transformFrontmatterForCursor(content) {
|
|
@@ -113,13 +160,72 @@ function transformFrontmatterForCursor(content) {
|
|
|
113
160
|
return `---\n${out.join('\n')}\n---\n${body}`;
|
|
114
161
|
}
|
|
115
162
|
|
|
116
|
-
|
|
163
|
+
// OpenCode: tools → permission mapping, adds mode: subagent, hidden: true for internal agents, removes model:
|
|
164
|
+
// tools:[Edit,Write,NotebookEdit] → edit:allow, tools:[Bash] → bash:allow, WebFetch → always deny
|
|
165
|
+
function transformFrontmatterForOpenCode(content) {
|
|
166
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
167
|
+
const match = normalized.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
168
|
+
if (!match) return content;
|
|
169
|
+
|
|
170
|
+
const [, frontmatterRaw, body] = match;
|
|
171
|
+
const lines = frontmatterRaw.split('\n');
|
|
172
|
+
const out = [];
|
|
173
|
+
let toolsLine = null;
|
|
174
|
+
let agentName = null;
|
|
175
|
+
|
|
176
|
+
for (const line of lines) {
|
|
177
|
+
if (line.startsWith('tools:')) {
|
|
178
|
+
toolsLine = line;
|
|
179
|
+
// Do not emit tools: line — OpenCode uses permission block instead
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (line.startsWith('model:')) {
|
|
183
|
+
// Remove model: line — OpenCode manages model selection separately
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (line.startsWith('name:')) {
|
|
187
|
+
const nameVal = line.slice('name:'.length).trim();
|
|
188
|
+
// Extract the base agent name from "refacil-<name>" or plain "<name>"
|
|
189
|
+
const nameMatch = nameVal.match(/refacil-(\S+)/);
|
|
190
|
+
agentName = nameMatch ? nameMatch[1] : nameVal;
|
|
191
|
+
out.push(line);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
out.push(line);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Determine permission values from tools list
|
|
198
|
+
const toolsList = toolsLine ? toolsLine.slice('tools:'.length).trim() : '';
|
|
199
|
+
const canEdit = /\b(Edit|Write|NotebookEdit)\b/.test(toolsList);
|
|
200
|
+
const canBash = /\bBash\b/.test(toolsList);
|
|
201
|
+
|
|
202
|
+
// Build permission block
|
|
203
|
+
out.push(`permission:`);
|
|
204
|
+
out.push(` edit: ${canEdit ? 'allow' : 'deny'}`);
|
|
205
|
+
out.push(` bash: ${canBash ? 'allow' : 'deny'}`);
|
|
206
|
+
out.push(` webfetch: deny`);
|
|
207
|
+
|
|
208
|
+
// Add mode: subagent
|
|
209
|
+
out.push(`mode: subagent`);
|
|
210
|
+
|
|
211
|
+
// Add hidden: true for internal agents
|
|
212
|
+
if (agentName && INTERNAL_AGENTS.includes(agentName)) {
|
|
213
|
+
out.push(`hidden: true`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return `---\n${out.join('\n')}\n---\n${body}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function installAgents(packageRoot, projectRoot, ideDirs) {
|
|
220
|
+
const dirs = ideDirs || ['.claude', '.cursor', '.opencode'];
|
|
117
221
|
let installed = 0;
|
|
118
222
|
|
|
119
223
|
const claudeDir = path.join(projectRoot, '.claude', 'agents');
|
|
120
224
|
const cursorDir = path.join(projectRoot, '.cursor', 'agents');
|
|
121
|
-
|
|
122
|
-
fs.mkdirSync(
|
|
225
|
+
const openCodeDir = path.join(projectRoot, '.opencode', 'agents');
|
|
226
|
+
if (dirs.includes('.claude')) fs.mkdirSync(claudeDir, { recursive: true });
|
|
227
|
+
if (dirs.includes('.cursor')) fs.mkdirSync(cursorDir, { recursive: true });
|
|
228
|
+
if (dirs.includes('.opencode')) fs.mkdirSync(openCodeDir, { recursive: true });
|
|
123
229
|
|
|
124
230
|
for (const agent of AGENTS) {
|
|
125
231
|
const srcFile = path.join(packageRoot, 'agents', `${agent}.md`);
|
|
@@ -127,11 +233,21 @@ function installAgents(packageRoot, projectRoot) {
|
|
|
127
233
|
|
|
128
234
|
const content = fs.readFileSync(srcFile, 'utf8');
|
|
129
235
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
236
|
+
if (dirs.includes('.claude')) {
|
|
237
|
+
fs.writeFileSync(path.join(claudeDir, `refacil-${agent}.md`), content);
|
|
238
|
+
}
|
|
239
|
+
if (dirs.includes('.cursor')) {
|
|
240
|
+
fs.writeFileSync(
|
|
241
|
+
path.join(cursorDir, `refacil-${agent}.md`),
|
|
242
|
+
transformFrontmatterForCursor(content),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (dirs.includes('.opencode')) {
|
|
246
|
+
fs.writeFileSync(
|
|
247
|
+
path.join(openCodeDir, `refacil-${agent}.md`),
|
|
248
|
+
transformFrontmatterForOpenCode(content),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
135
251
|
|
|
136
252
|
installed++;
|
|
137
253
|
}
|
|
@@ -139,6 +255,50 @@ function installAgents(packageRoot, projectRoot) {
|
|
|
139
255
|
return installed;
|
|
140
256
|
}
|
|
141
257
|
|
|
258
|
+
function removeOpenCodeArtifacts(projectRoot) {
|
|
259
|
+
// Remove .opencode/skills/refacil-*/
|
|
260
|
+
for (const skill of SKILLS) {
|
|
261
|
+
const skillDir = path.join(projectRoot, '.opencode', 'skills', `refacil-${skill}`);
|
|
262
|
+
if (fs.existsSync(skillDir)) {
|
|
263
|
+
try { fs.rmSync(skillDir, { recursive: true }); } catch (_) {}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Remove .opencode/agents/refacil-*.md
|
|
268
|
+
const agentsDir = path.join(projectRoot, '.opencode', 'agents');
|
|
269
|
+
if (fs.existsSync(agentsDir)) {
|
|
270
|
+
try {
|
|
271
|
+
const entries = fs.readdirSync(agentsDir, { withFileTypes: true });
|
|
272
|
+
for (const entry of entries) {
|
|
273
|
+
if (entry.isFile() && entry.name.startsWith('refacil-') && entry.name.endsWith('.md')) {
|
|
274
|
+
fs.unlinkSync(path.join(agentsDir, entry.name));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} catch (_) {}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Remove .opencode/plugins/refacil-hooks.js
|
|
281
|
+
const pluginFile = path.join(projectRoot, '.opencode', 'plugins', 'refacil-hooks.js');
|
|
282
|
+
if (fs.existsSync(pluginFile)) {
|
|
283
|
+
try { fs.unlinkSync(pluginFile); } catch (_) {}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Revert SDD-AI keys from .opencode/opencode.json (currently only $schema key, leave file if other keys remain)
|
|
287
|
+
const ocJsonPath = path.join(projectRoot, '.opencode', 'opencode.json');
|
|
288
|
+
if (fs.existsSync(ocJsonPath)) {
|
|
289
|
+
try {
|
|
290
|
+
const json = JSON.parse(fs.readFileSync(ocJsonPath, 'utf8'));
|
|
291
|
+
delete json['$schema'];
|
|
292
|
+
const remaining = Object.keys(json);
|
|
293
|
+
if (remaining.length === 0) {
|
|
294
|
+
fs.unlinkSync(ocJsonPath);
|
|
295
|
+
} else {
|
|
296
|
+
fs.writeFileSync(ocJsonPath, JSON.stringify(json, null, 2) + '\n');
|
|
297
|
+
}
|
|
298
|
+
} catch (_) {}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
142
302
|
function writeGuideFile(destPath, header, label) {
|
|
143
303
|
const content =
|
|
144
304
|
`# ${header}\n\n` +
|
|
@@ -309,10 +469,14 @@ function checkNodeVersion() {
|
|
|
309
469
|
module.exports = {
|
|
310
470
|
SKILLS,
|
|
311
471
|
AGENTS,
|
|
472
|
+
INTERNAL_AGENTS,
|
|
312
473
|
copyDir,
|
|
313
474
|
installSkills,
|
|
475
|
+
installOpenCodeJson,
|
|
314
476
|
transformFrontmatterForCursor,
|
|
477
|
+
transformFrontmatterForOpenCode,
|
|
315
478
|
installAgents,
|
|
479
|
+
removeOpenCodeArtifacts,
|
|
316
480
|
createClaudeMd,
|
|
317
481
|
createCursorRules,
|
|
318
482
|
readRepoVersion,
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* refacil-sdd-ai OpenCode plugin
|
|
5
|
+
*
|
|
6
|
+
* Provides 4 hook equivalents for OpenCode:
|
|
7
|
+
* - session.created → check-update logic (sync compact-guidance, flag pending migrations)
|
|
8
|
+
* - tui.prompt.append → notify-update logic (prompt user to run /refacil:update if pending)
|
|
9
|
+
* - tool.execute.before → check-review + compact-bash logic
|
|
10
|
+
*
|
|
11
|
+
* This file is installed as .opencode/plugins/refacil-hooks.js.
|
|
12
|
+
* It resolves lib/compact/rules.js relative to its own __dirname at install time.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
|
|
18
|
+
// ── Resolve compact rules ────────────────────────────────────────────────────
|
|
19
|
+
// When installed, this file lives at .opencode/plugins/refacil-hooks.js.
|
|
20
|
+
// The compact rules live at <package>/lib/compact/rules.js.
|
|
21
|
+
// We walk up from __dirname looking for the package (node_modules/refacil-sdd-ai or
|
|
22
|
+
// the package root directly), falling back gracefully if not found.
|
|
23
|
+
|
|
24
|
+
let findRule = null;
|
|
25
|
+
|
|
26
|
+
(function loadCompactRules() {
|
|
27
|
+
const candidates = [
|
|
28
|
+
// Installed as plugin in .opencode/plugins/ — package is in node_modules
|
|
29
|
+
path.resolve(__dirname, '..', '..', 'node_modules', 'refacil-sdd-ai', 'lib', 'compact', 'rules.js'),
|
|
30
|
+
// Running from source (lib/opencode-plugin/index.js)
|
|
31
|
+
path.resolve(__dirname, '..', 'compact', 'rules.js'),
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
for (const candidate of candidates) {
|
|
35
|
+
try {
|
|
36
|
+
if (fs.existsSync(candidate)) {
|
|
37
|
+
const rules = require(candidate);
|
|
38
|
+
if (typeof rules.findRule === 'function') {
|
|
39
|
+
findRule = rules.findRule;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch (_) {
|
|
44
|
+
// Try next candidate
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!findRule) {
|
|
49
|
+
process.stderr.write('[refacil-sdd-ai] WARNING: Could not load compact/rules.js — compact-bash hook disabled.\n');
|
|
50
|
+
}
|
|
51
|
+
})();
|
|
52
|
+
|
|
53
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
function getPendingUpdateFlagPath(projectRoot) {
|
|
56
|
+
return path.join(projectRoot, '.refacil-pending-update');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readPendingUpdateFlag(projectRoot) {
|
|
60
|
+
const flagPath = getPendingUpdateFlagPath(projectRoot);
|
|
61
|
+
if (!fs.existsSync(flagPath)) return null;
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(fs.readFileSync(flagPath, 'utf8'));
|
|
64
|
+
} catch (_) {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writePendingUpdateFlag(projectRoot, from, to) {
|
|
70
|
+
try {
|
|
71
|
+
fs.writeFileSync(getPendingUpdateFlagPath(projectRoot), JSON.stringify({ from, to }));
|
|
72
|
+
} catch (_) {}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function clearPendingUpdateFlag(projectRoot) {
|
|
76
|
+
try {
|
|
77
|
+
const flagPath = getPendingUpdateFlagPath(projectRoot);
|
|
78
|
+
if (fs.existsSync(flagPath)) fs.unlinkSync(flagPath);
|
|
79
|
+
} catch (_) {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readRepoVersion(projectRoot) {
|
|
83
|
+
const versionFiles = ['.opencode/.sdd-version', '.claude/.sdd-version', '.cursor/.sdd-version'];
|
|
84
|
+
for (const rel of versionFiles) {
|
|
85
|
+
try {
|
|
86
|
+
const raw = fs.readFileSync(path.join(projectRoot, rel), 'utf8').trim();
|
|
87
|
+
if (raw) return raw;
|
|
88
|
+
} catch (_) {}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function methodologyMigrationPending(projectRoot) {
|
|
94
|
+
// Look for refacil-sdd/changes with active (non-archived) tasks that still have pending migrations
|
|
95
|
+
// This is a lightweight check: look for changes that have tasks.md but no .review-passed
|
|
96
|
+
const changesDir = path.join(projectRoot, 'refacil-sdd', 'changes');
|
|
97
|
+
if (!fs.existsSync(changesDir)) return { pending: false, reasons: [] };
|
|
98
|
+
|
|
99
|
+
let entries;
|
|
100
|
+
try {
|
|
101
|
+
entries = fs.readdirSync(changesDir, { withFileTypes: true });
|
|
102
|
+
} catch (_) {
|
|
103
|
+
return { pending: false, reasons: [] };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const reasons = [];
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (!entry.isDirectory() || entry.name === 'archive') continue;
|
|
109
|
+
const tasksPath = path.join(changesDir, entry.name, 'tasks.md');
|
|
110
|
+
const tasksContent = fs.existsSync(tasksPath) ? fs.readFileSync(tasksPath, 'utf8') : '';
|
|
111
|
+
// Look for unchecked tasks: "- [ ]" pattern
|
|
112
|
+
if (/- \[ \]/.test(tasksContent)) {
|
|
113
|
+
reasons.push(`Change '${entry.name}' has pending tasks`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { pending: reasons.length > 0, reasons };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Hook handlers ────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* session.created — equivalent of check-update (SessionStart hook)
|
|
124
|
+
* Checks if the installed skills are out of date and flags a pending update.
|
|
125
|
+
*/
|
|
126
|
+
async function checkUpdateHandler(event) {
|
|
127
|
+
const projectRoot = event.projectRoot || process.cwd();
|
|
128
|
+
|
|
129
|
+
// Check if there is a pending methodology migration
|
|
130
|
+
try {
|
|
131
|
+
const mig = methodologyMigrationPending(projectRoot);
|
|
132
|
+
const repoVersion = readRepoVersion(projectRoot);
|
|
133
|
+
|
|
134
|
+
// Try to get the current package version via refacil-sdd-ai CLI
|
|
135
|
+
let packageVersion = null;
|
|
136
|
+
try {
|
|
137
|
+
const { execSync } = require('child_process');
|
|
138
|
+
packageVersion = execSync('refacil-sdd-ai --version', {
|
|
139
|
+
encoding: 'utf8',
|
|
140
|
+
timeout: 5000,
|
|
141
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
142
|
+
}).trim();
|
|
143
|
+
} catch (_) {}
|
|
144
|
+
|
|
145
|
+
const existingFlag = readPendingUpdateFlag(projectRoot);
|
|
146
|
+
|
|
147
|
+
if (mig.pending) {
|
|
148
|
+
writePendingUpdateFlag(projectRoot, repoVersion, packageVersion);
|
|
149
|
+
} else if (existingFlag) {
|
|
150
|
+
clearPendingUpdateFlag(projectRoot);
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
process.stderr.write(`[refacil-sdd-ai] check-update handler error: ${err.message}\n`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* tui.prompt.append — equivalent of notify-update (UserPromptSubmit hook)
|
|
159
|
+
* Returns an instruction string if there is a pending update, otherwise returns nothing.
|
|
160
|
+
* Also clears the flag if the user is running /refacil:update.
|
|
161
|
+
*/
|
|
162
|
+
async function notifyUpdateHandler(event) {
|
|
163
|
+
const projectRoot = event.projectRoot || process.cwd();
|
|
164
|
+
const prompt = (event.prompt || '').trim().toLowerCase();
|
|
165
|
+
|
|
166
|
+
// If user is running /refacil:update, clear the flag and let it through
|
|
167
|
+
if (prompt.includes('refacil:update') || prompt.includes('refacil/update')) {
|
|
168
|
+
clearPendingUpdateFlag(projectRoot);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const flagInfo = readPendingUpdateFlag(projectRoot);
|
|
173
|
+
if (!flagInfo) return;
|
|
174
|
+
|
|
175
|
+
const mig = methodologyMigrationPending(projectRoot);
|
|
176
|
+
if (!mig.pending) {
|
|
177
|
+
clearPendingUpdateFlag(projectRoot);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const fromLabel = flagInfo.from ? `v${flagInfo.from}` : 'previous version';
|
|
182
|
+
const toLabel = flagInfo.to ? `v${flagInfo.to}` : 'latest';
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
`[refacil-sdd-ai] Methodology update detected (${fromLabel} → ${toLabel}). ` +
|
|
186
|
+
`Run /refacil:update to apply pending migrations before continuing.`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* tool.execute.before — handles Bash tool calls:
|
|
192
|
+
* (a) check-review: blocks git push if any active change is missing .review-passed
|
|
193
|
+
* (b) compact-bash: rewrites matched commands to reduce token usage
|
|
194
|
+
*/
|
|
195
|
+
async function toolExecuteBeforeHandler(event) {
|
|
196
|
+
// Only handle Bash tool calls
|
|
197
|
+
if (!event || !event.tool || event.tool !== 'bash') return;
|
|
198
|
+
|
|
199
|
+
const command = (event.input && event.input.command) || (event.params && event.params.command) || '';
|
|
200
|
+
if (!command) return;
|
|
201
|
+
|
|
202
|
+
const projectRoot = event.projectRoot || process.cwd();
|
|
203
|
+
|
|
204
|
+
// (a) check-review: block git push if missing .review-passed
|
|
205
|
+
if (/git\s+push/.test(command)) {
|
|
206
|
+
const sddChangesDir = path.join(projectRoot, 'refacil-sdd', 'changes');
|
|
207
|
+
if (fs.existsSync(sddChangesDir)) {
|
|
208
|
+
let entries;
|
|
209
|
+
try {
|
|
210
|
+
entries = fs.readdirSync(sddChangesDir, { withFileTypes: true });
|
|
211
|
+
} catch (_) {
|
|
212
|
+
entries = [];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const activeChanges = entries.filter(
|
|
216
|
+
(e) => e.isDirectory() && e.name !== 'archive',
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (activeChanges.length > 0) {
|
|
220
|
+
const missing = activeChanges.filter(
|
|
221
|
+
(e) => !fs.existsSync(path.join(sddChangesDir, e.name, '.review-passed')),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if (missing.length > 0) {
|
|
225
|
+
const names = missing.map((e) => e.name).join(', ');
|
|
226
|
+
const reason =
|
|
227
|
+
missing.length === 1
|
|
228
|
+
? `[refacil-sdd-ai] Review pending for: ${names}. ` +
|
|
229
|
+
'Stop the push and run /refacil:review on that change before pushing code. ' +
|
|
230
|
+
'If the review passes, retry the git push.'
|
|
231
|
+
: `[refacil-sdd-ai] Multiple changes without approved review: ${names}. ` +
|
|
232
|
+
'Stop the push and ask the user to explicitly select which change they want to push. ' +
|
|
233
|
+
'Then run /refacil:review <change-name> for that specific change and retry the push.';
|
|
234
|
+
|
|
235
|
+
throw new Error(reason);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// (b) compact-bash: rewrite matched commands to reduce token usage
|
|
242
|
+
// Skip if COMPACT=0 is set or findRule is not available
|
|
243
|
+
if (!findRule) return;
|
|
244
|
+
if (/\bCOMPACT=0\b/.test(command)) return;
|
|
245
|
+
|
|
246
|
+
const rule = findRule(command);
|
|
247
|
+
if (!rule) return;
|
|
248
|
+
|
|
249
|
+
const rewritten = rule.rewrite(command);
|
|
250
|
+
// Return the rewritten command for OpenCode to use instead
|
|
251
|
+
return { command: rewritten };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Plugin export ────────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
module.exports = {
|
|
257
|
+
hooks: {
|
|
258
|
+
'session.created': checkUpdateHandler,
|
|
259
|
+
'tui.prompt.append': notifyUpdateHandler,
|
|
260
|
+
'tool.execute.before': toolExecuteBeforeHandler,
|
|
261
|
+
},
|
|
262
|
+
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "refacil-sdd-ai",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "SDD-AI: Specification-Driven Development with AI — development methodology using AI with Claude Code and
|
|
3
|
+
"version": "4.5.1",
|
|
4
|
+
"description": "SDD-AI: Specification-Driven Development with AI — development methodology using AI with Claude Code, Cursor and OpenCode",
|
|
5
5
|
"bin": {
|
|
6
6
|
"refacil-sdd-ai": "./bin/cli.js"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"bin/",
|
|
10
10
|
"lib/",
|
|
11
|
+
"lib/opencode-plugin/",
|
|
11
12
|
"skills/",
|
|
12
13
|
"agents/",
|
|
13
14
|
"templates/",
|
|
@@ -35,9 +36,12 @@
|
|
|
35
36
|
"node": ">=20.0.0"
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
|
-
"test": "node --test test/hooks.test.js test/installer.test.js test/ignore-files.test.js test/methodology-migration-pending.test.js test/sdd.test.js test/refactor-integrar-openspec-nativo.test.js test/refactor-rutas-refacil-sdd.test.js test/refactor-agents-english.test.js test/remove-openspec-legacy.test.js test/find-project-root.test.js"
|
|
39
|
+
"test": "node --test test/hooks.test.js test/installer.test.js test/ignore-files.test.js test/methodology-migration-pending.test.js test/sdd.test.js test/refactor-integrar-openspec-nativo.test.js test/refactor-rutas-refacil-sdd.test.js test/refactor-agents-english.test.js test/remove-openspec-legacy.test.js test/find-project-root.test.js test/opencode-installer.test.js test/opencode-plugin.test.js"
|
|
39
40
|
},
|
|
40
41
|
"dependencies": {
|
|
41
42
|
"ws": "^8.18.0"
|
|
43
|
+
},
|
|
44
|
+
"optionalDependencies": {
|
|
45
|
+
"@clack/prompts": "^0.9.0"
|
|
42
46
|
}
|
|
43
47
|
}
|