refacil-sdd-ai 5.2.2 → 5.3.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/NOTICE.md +46 -0
- package/README.md +209 -42
- package/agents/auditor.md +46 -0
- package/agents/debugger.md +41 -1
- package/agents/implementer.md +76 -10
- package/agents/investigator.md +36 -0
- package/agents/proposer.md +46 -2
- package/agents/tester.md +45 -8
- package/agents/validator.md +67 -13
- package/bin/cli.js +428 -83
- package/bin/postinstall.js +20 -0
- package/lib/bus/broker.js +121 -3
- package/lib/bus/spawn.js +189 -121
- package/lib/check-review.js +102 -0
- package/lib/codegraph-telemetry.js +135 -0
- package/lib/codegraph.js +273 -0
- package/lib/commands/autopilot.js +120 -0
- package/lib/commands/bus.js +29 -36
- package/lib/commands/compact.js +185 -46
- package/lib/commands/read-spec.js +352 -0
- package/lib/commands/sdd.js +429 -44
- package/lib/compact-guidance.js +122 -77
- package/lib/config.js +136 -0
- package/lib/global-paths.js +56 -20
- package/lib/hooks.js +32 -4
- package/lib/ide-detection.js +1 -1
- package/lib/ignore-files.js +5 -1
- package/lib/installer.js +202 -19
- package/lib/kapso.js +241 -0
- package/lib/methodology-migration-pending.js +13 -0
- package/lib/open-browser.js +32 -0
- package/lib/opencode-migrate.js +148 -0
- package/lib/opencode-plugin/index.js +84 -104
- package/lib/opencode-plugin/rules.js +236 -0
- package/lib/project-root.js +154 -0
- package/lib/repo-ide-sync.js +5 -0
- package/lib/spec-reader/lang.js +72 -0
- package/lib/spec-reader/md-parser.js +299 -0
- package/lib/spec-reader/session.js +139 -0
- package/lib/spec-reader/ui/app.js +685 -0
- package/lib/spec-reader/ui/index.html +59 -0
- package/lib/spec-reader/ui/mixed-lang.js +200 -0
- package/lib/spec-reader/ui/model-cache.js +117 -0
- package/lib/spec-reader/ui/style.css +294 -0
- package/lib/spec-reader/ui/supertonic-helper.js +565 -0
- package/lib/spec-sync.js +258 -0
- package/lib/test-scope.js +713 -0
- package/lib/testing-policy-sync.js +14 -2
- package/package.json +6 -3
- package/skills/apply/SKILL.md +39 -64
- package/skills/archive/SKILL.md +74 -48
- package/skills/ask/SKILL.md +43 -8
- package/skills/autopilot/SKILL.md +476 -0
- package/skills/bug/SKILL.md +52 -53
- package/skills/explore/SKILL.md +48 -1
- package/skills/guide/SKILL.md +31 -13
- package/skills/inbox/SKILL.md +9 -0
- package/skills/join/SKILL.md +1 -1
- package/skills/prereqs/BUS-CROSS-REPO.md +33 -16
- package/skills/prereqs/METHODOLOGY-CONTRACT.md +96 -17
- package/skills/prereqs/SKILL.md +1 -1
- package/skills/propose/SKILL.md +74 -19
- package/skills/read-spec/SKILL.md +76 -0
- package/skills/reply/SKILL.md +42 -9
- package/skills/review/SKILL.md +63 -25
- package/skills/review/checklist.md +2 -2
- package/skills/say/SKILL.md +40 -4
- package/skills/setup/SKILL.md +59 -5
- package/skills/setup/troubleshooting.md +11 -3
- package/skills/stats/SKILL.md +157 -0
- package/skills/test/SKILL.md +35 -10
- package/skills/up-code/SKILL.md +20 -13
- package/skills/update/SKILL.md +32 -1
- package/skills/verify/SKILL.md +78 -41
- package/templates/compact-guidance.md +10 -0
- package/templates/methodology-guide.md +5 -0
package/lib/compact-guidance.js
CHANGED
|
@@ -1,77 +1,122 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
const MARKER_START = '<!-- refacil-sdd-ai:compact-guidance:start -->';
|
|
5
|
-
const MARKER_END = '<!-- refacil-sdd-ai:compact-guidance:end -->';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function
|
|
51
|
-
const agentsPath = path.join(projectRoot, 'AGENTS.md');
|
|
52
|
-
if (!fs.existsSync(agentsPath)) {
|
|
53
|
-
return { status: 'skipped-no-agents-md' };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const MARKER_START = '<!-- refacil-sdd-ai:compact-guidance:start -->';
|
|
5
|
+
const MARKER_END = '<!-- refacil-sdd-ai:compact-guidance:end -->';
|
|
6
|
+
const LEGACY_MARKER_START = '<!-- compact-guidance:start -->';
|
|
7
|
+
const LEGACY_MARKER_END = '<!-- compact-guidance:end -->';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Removes obsolete empty placeholder pair (pre-refacil-sdd-ai naming).
|
|
11
|
+
* Preserves the block if non-whitespace content exists between legacy markers.
|
|
12
|
+
*/
|
|
13
|
+
function stripLegacyCompactGuidanceMarkers(content) {
|
|
14
|
+
const startIdx = content.indexOf(LEGACY_MARKER_START);
|
|
15
|
+
if (startIdx === -1) return content;
|
|
16
|
+
|
|
17
|
+
const endIdx = content.indexOf(LEGACY_MARKER_END, startIdx + LEGACY_MARKER_START.length);
|
|
18
|
+
if (endIdx === -1) return content;
|
|
19
|
+
|
|
20
|
+
const inner = content.substring(startIdx + LEGACY_MARKER_START.length, endIdx);
|
|
21
|
+
if (inner.trim() !== '') return content;
|
|
22
|
+
|
|
23
|
+
const before = content.substring(0, startIdx).trimEnd();
|
|
24
|
+
const after = content.substring(endIdx + LEGACY_MARKER_END.length).replace(/^\s+/, '');
|
|
25
|
+
if (!before) return after.trimStart();
|
|
26
|
+
if (!after) return `${before}\n`;
|
|
27
|
+
return `${before}\n\n${after}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readTemplate(packageRoot) {
|
|
31
|
+
const tplPath = path.join(packageRoot, 'templates', 'compact-guidance.md');
|
|
32
|
+
return fs.readFileSync(tplPath, 'utf8').trimEnd();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildBlock(templateContent) {
|
|
36
|
+
return `${MARKER_START}\n${templateContent}\n${MARKER_END}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Normalize line endings for idempotent compare (CA-02 / Windows CRLF). */
|
|
40
|
+
function normalizeEol(text) {
|
|
41
|
+
return text.replace(/\r\n/g, '\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function contentUnchanged(next, raw) {
|
|
45
|
+
const a = normalizeEol(next).trimEnd() + '\n';
|
|
46
|
+
const b = normalizeEol(raw).trimEnd() + '\n';
|
|
47
|
+
return a === b;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function syncCompactGuidance(projectRoot, packageRoot) {
|
|
51
|
+
const agentsPath = path.join(projectRoot, 'AGENTS.md');
|
|
52
|
+
if (!fs.existsSync(agentsPath)) {
|
|
53
|
+
return { status: 'skipped-no-agents-md' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const template = readTemplate(packageRoot);
|
|
57
|
+
const block = buildBlock(template);
|
|
58
|
+
const raw = fs.readFileSync(agentsPath, 'utf8');
|
|
59
|
+
const existing = stripLegacyCompactGuidanceMarkers(raw);
|
|
60
|
+
|
|
61
|
+
const startIdx = existing.indexOf(MARKER_START);
|
|
62
|
+
const endIdx = existing.indexOf(MARKER_END);
|
|
63
|
+
|
|
64
|
+
let next;
|
|
65
|
+
let action;
|
|
66
|
+
|
|
67
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
68
|
+
next = existing.trimEnd() + '\n\n' + block + '\n';
|
|
69
|
+
action = 'appended';
|
|
70
|
+
} else {
|
|
71
|
+
const before = existing.substring(0, startIdx);
|
|
72
|
+
const after = existing.substring(endIdx + MARKER_END.length);
|
|
73
|
+
next = before + block + after;
|
|
74
|
+
action = 'replaced';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const normalized = next.trimEnd() + '\n';
|
|
78
|
+
if (contentUnchanged(normalized, raw)) {
|
|
79
|
+
return { status: 'unchanged' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fs.writeFileSync(agentsPath, normalized);
|
|
83
|
+
return { status: action };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function removeCompactGuidance(projectRoot) {
|
|
87
|
+
const agentsPath = path.join(projectRoot, 'AGENTS.md');
|
|
88
|
+
if (!fs.existsSync(agentsPath)) {
|
|
89
|
+
return { status: 'skipped-no-agents-md' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const raw = fs.readFileSync(agentsPath, 'utf8');
|
|
93
|
+
const existing = stripLegacyCompactGuidanceMarkers(raw);
|
|
94
|
+
const startIdx = existing.indexOf(MARKER_START);
|
|
95
|
+
const endIdx = existing.indexOf(MARKER_END);
|
|
96
|
+
|
|
97
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
98
|
+
const legacyOnly = stripLegacyCompactGuidanceMarkers(raw);
|
|
99
|
+
if (legacyOnly !== raw) {
|
|
100
|
+
fs.writeFileSync(agentsPath, legacyOnly.trimEnd() + '\n');
|
|
101
|
+
return { status: 'legacy-removed' };
|
|
102
|
+
}
|
|
103
|
+
return { status: 'not-present' };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const before = existing.substring(0, startIdx).trimEnd();
|
|
107
|
+
const after = existing.substring(endIdx + MARKER_END.length);
|
|
108
|
+
const next = (before + '\n' + after.replace(/^\s+/, '')).trimEnd() + '\n';
|
|
109
|
+
|
|
110
|
+
fs.writeFileSync(agentsPath, next);
|
|
111
|
+
return { status: 'removed' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
syncCompactGuidance,
|
|
116
|
+
removeCompactGuidance,
|
|
117
|
+
stripLegacyCompactGuidanceMarkers,
|
|
118
|
+
MARKER_START,
|
|
119
|
+
MARKER_END,
|
|
120
|
+
LEGACY_MARKER_START,
|
|
121
|
+
LEGACY_MARKER_END,
|
|
122
|
+
};
|
package/lib/config.js
CHANGED
|
@@ -4,6 +4,9 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
|
|
7
|
+
const CODEGRAPH_MODES = ['enabled', 'per-repo', 'disabled'];
|
|
8
|
+
const DEFAULT_CODEGRAPH_MODE = 'enabled';
|
|
9
|
+
|
|
7
10
|
const DEFAULT_PROTECTED_BRANCHES = ['master', 'main', 'develop', 'dev', 'testing', 'qa'];
|
|
8
11
|
const DEFAULT_BASE_BRANCH = 'develop';
|
|
9
12
|
const SUPPORTED_LANGUAGES = ['english', 'spanish'];
|
|
@@ -236,14 +239,38 @@ function loadBranchConfigWithSources(projectRoot) {
|
|
|
236
239
|
artifactLanguageSource = 'default';
|
|
237
240
|
}
|
|
238
241
|
|
|
242
|
+
// --- codegraphMode ---
|
|
243
|
+
let codegraphMode = null;
|
|
244
|
+
let codegraphModeSource = 'default';
|
|
245
|
+
|
|
246
|
+
if (projectCfg !== null) {
|
|
247
|
+
const val = extractCodegraphMode(projectCfg, 'project');
|
|
248
|
+
if (val !== null) {
|
|
249
|
+
codegraphMode = val;
|
|
250
|
+
codegraphModeSource = 'project';
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (codegraphMode === null && globalCfg !== null) {
|
|
255
|
+
const val = extractCodegraphMode(globalCfg, 'global');
|
|
256
|
+
if (val !== null) {
|
|
257
|
+
codegraphMode = val;
|
|
258
|
+
codegraphModeSource = 'global';
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// codegraphMode has no default — null means "no preference yet" (suggestion not yet answered)
|
|
263
|
+
|
|
239
264
|
return {
|
|
240
265
|
protectedBranches,
|
|
241
266
|
baseBranch,
|
|
242
267
|
artifactLanguage,
|
|
268
|
+
codegraphMode,
|
|
243
269
|
sources: {
|
|
244
270
|
protectedBranches: protectedBranchesSource,
|
|
245
271
|
baseBranch: baseBranchSource,
|
|
246
272
|
artifactLanguage: artifactLanguageSource,
|
|
273
|
+
codegraphMode: codegraphModeSource,
|
|
247
274
|
},
|
|
248
275
|
};
|
|
249
276
|
}
|
|
@@ -260,14 +287,123 @@ function loadBranchConfig(projectRoot) {
|
|
|
260
287
|
return { protectedBranches, baseBranch, artifactLanguage };
|
|
261
288
|
}
|
|
262
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Validate `codegraphMode` from a parsed config object.
|
|
292
|
+
* Valid values: 'enabled' | 'per-repo' | 'disabled'. Default: 'enabled'.
|
|
293
|
+
* Returns the value if valid, or null + emits a warning if invalid.
|
|
294
|
+
* @param {object} cfg — parsed YAML object
|
|
295
|
+
* @param {string} src — source label for the warning ('project' | 'global')
|
|
296
|
+
* @returns {string|null}
|
|
297
|
+
*/
|
|
298
|
+
function extractCodegraphMode(cfg, src) {
|
|
299
|
+
if (!('codegraphMode' in cfg)) return null;
|
|
300
|
+
const val = cfg.codegraphMode;
|
|
301
|
+
if (typeof val !== 'string' || val.trim() === '') {
|
|
302
|
+
process.stderr.write(
|
|
303
|
+
`[refacil-sdd-ai] warning: codegraphMode in ${src} config must be a non-empty string — ignoring.\n`,
|
|
304
|
+
);
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
const trimmed = val.trim();
|
|
308
|
+
if (!CODEGRAPH_MODES.includes(trimmed)) {
|
|
309
|
+
process.stderr.write(
|
|
310
|
+
`[refacil-sdd-ai] warning: codegraphMode "${trimmed}" in ${src} config is not a valid value (${CODEGRAPH_MODES.join(', ')}) — ignoring.\n`,
|
|
311
|
+
);
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
return trimmed;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Read CodeGraph suggestion UI state from a parsed config object.
|
|
319
|
+
* Keys read: codegraph-suggest-shown (boolean), codegraph-suggest-snooze (ISO date string).
|
|
320
|
+
* Returns { shown: bool|null, snoozeUntil: string|null }.
|
|
321
|
+
* Never warns — these are internal state keys that may simply be absent.
|
|
322
|
+
* @param {object} cfg — parsed YAML object
|
|
323
|
+
* @param {string} _src — source label (unused, kept for API consistency)
|
|
324
|
+
* @returns {{ shown: boolean|null, snoozeUntil: string|null }}
|
|
325
|
+
*/
|
|
326
|
+
function extractCodegraphSuggestState(cfg, _src) {
|
|
327
|
+
let shown = null;
|
|
328
|
+
let snoozeUntil = null;
|
|
329
|
+
|
|
330
|
+
if ('codegraph-suggest-shown' in cfg) {
|
|
331
|
+
const raw = cfg['codegraph-suggest-shown'];
|
|
332
|
+
if (raw === 'true' || raw === true) shown = true;
|
|
333
|
+
else if (raw === 'false' || raw === false) shown = false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if ('codegraph-suggest-snooze' in cfg) {
|
|
337
|
+
const raw = cfg['codegraph-suggest-snooze'];
|
|
338
|
+
if (typeof raw === 'string' && raw.trim() !== '') {
|
|
339
|
+
snoozeUntil = raw.trim();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return { shown, snoozeUntil };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Write (set or update) a single flat key:value pair in a YAML config file.
|
|
348
|
+
* Uses the same minimal YAML format as the existing parser — only flat key:value scalars.
|
|
349
|
+
* If the key already exists, its line is replaced in-place (preserving other lines).
|
|
350
|
+
* If the key does not exist, it is appended.
|
|
351
|
+
* Creates the file and parent directories if they do not exist.
|
|
352
|
+
* Never throws — all errors are swallowed silently.
|
|
353
|
+
*
|
|
354
|
+
* @param {string} key — config key (e.g. 'codegraphMode')
|
|
355
|
+
* @param {string} value — scalar string value to set
|
|
356
|
+
* @param {string} homeDir — path to the user home directory (e.g. os.homedir())
|
|
357
|
+
*/
|
|
358
|
+
function writeConfigValue(key, value, homeDir) {
|
|
359
|
+
try {
|
|
360
|
+
const globalConfigPath = path.join(homeDir || os.homedir(), '.refacil-sdd-ai', 'config.yaml');
|
|
361
|
+
fs.mkdirSync(path.dirname(globalConfigPath), { recursive: true });
|
|
362
|
+
|
|
363
|
+
let lines = [];
|
|
364
|
+
if (fs.existsSync(globalConfigPath)) {
|
|
365
|
+
lines = fs.readFileSync(globalConfigPath, 'utf8').split('\n');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Determine if key already exists (match "key:" or "key: value" at line start)
|
|
369
|
+
const keyPattern = new RegExp(`^${key.replace(/[-]/g, '\\$&')}:`);
|
|
370
|
+
let found = false;
|
|
371
|
+
const updated = lines.map((line) => {
|
|
372
|
+
if (keyPattern.test(line)) {
|
|
373
|
+
found = true;
|
|
374
|
+
return `${key}: ${value}`;
|
|
375
|
+
}
|
|
376
|
+
return line;
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
if (!found) {
|
|
380
|
+
// Remove trailing empty line before appending to avoid double blank lines
|
|
381
|
+
while (updated.length > 0 && updated[updated.length - 1].trim() === '') {
|
|
382
|
+
updated.pop();
|
|
383
|
+
}
|
|
384
|
+
updated.push(`${key}: ${value}`);
|
|
385
|
+
updated.push('');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
fs.writeFileSync(globalConfigPath, updated.join('\n'));
|
|
389
|
+
} catch (_) {
|
|
390
|
+
// Silently swallow — config writes must never break caller flow
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
263
394
|
module.exports = {
|
|
264
395
|
parseYaml,
|
|
265
396
|
readConfigFile,
|
|
266
397
|
loadBranchConfig,
|
|
267
398
|
loadBranchConfigWithSources,
|
|
268
399
|
extractArtifactLanguage,
|
|
400
|
+
extractCodegraphMode,
|
|
401
|
+
extractCodegraphSuggestState,
|
|
402
|
+
writeConfigValue,
|
|
269
403
|
DEFAULT_PROTECTED_BRANCHES,
|
|
270
404
|
DEFAULT_BASE_BRANCH,
|
|
271
405
|
SUPPORTED_LANGUAGES,
|
|
272
406
|
DEFAULT_ARTIFACT_LANGUAGE,
|
|
407
|
+
CODEGRAPH_MODES,
|
|
408
|
+
DEFAULT_CODEGRAPH_MODE,
|
|
273
409
|
};
|
package/lib/global-paths.js
CHANGED
|
@@ -25,38 +25,72 @@ function globalCursorDir(homeDir) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Returns
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
|
|
28
|
+
* Returns legacy OpenCode config directories that may contain pre-migration artifacts.
|
|
29
|
+
* @param {string} [homeDir] - injectable for testing (default: os.homedir())
|
|
30
|
+
* @returns {string[]}
|
|
31
|
+
*/
|
|
32
|
+
function legacyOpenCodeDirs(homeDir) {
|
|
33
|
+
const resolvedHome = homeDir || os.homedir();
|
|
34
|
+
const dirs = [path.join(resolvedHome, '.opencode')];
|
|
35
|
+
if (process.platform === 'win32') {
|
|
36
|
+
const appData = process.env.APPDATA || path.join(resolvedHome, 'AppData', 'Roaming');
|
|
37
|
+
dirs.push(path.join(appData, 'opencode'));
|
|
38
|
+
}
|
|
39
|
+
return dirs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns the global OpenCode user directory (official upstream path).
|
|
44
|
+
* Production:
|
|
45
|
+
* - OPENCODE_CONFIG_DIR when set (absolute)
|
|
46
|
+
* - Otherwise ~/.config/opencode on all platforms (Windows: %USERPROFILE%\.config\opencode)
|
|
32
47
|
* Test injection:
|
|
33
|
-
* When
|
|
34
|
-
* When only homeDir is explicitly provided (no appDataDir), uses homeDir/.opencode
|
|
35
|
-
* for cross-platform test portability.
|
|
48
|
+
* When homeDir is explicitly provided, returns homeDir/.config/opencode.
|
|
36
49
|
* @param {string} [homeDir] - injectable for testing (default: os.homedir())
|
|
37
|
-
* @param {string} [appDataDir] - injectable Windows APPDATA dir for testing
|
|
38
50
|
* @returns {string}
|
|
39
51
|
*/
|
|
40
|
-
function globalOpenCodeDir(homeDir
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
return path.
|
|
52
|
+
function globalOpenCodeDir(homeDir) {
|
|
53
|
+
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
54
|
+
if (envDir && String(envDir).trim()) {
|
|
55
|
+
return path.resolve(envDir.trim());
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
// When homeDir is explicitly provided (test injection without appDataDir),
|
|
47
|
-
// always use homeDir/.opencode for cross-platform test portability
|
|
48
58
|
if (homeDir) {
|
|
49
|
-
return path.join(homeDir, '.opencode');
|
|
59
|
+
return path.join(homeDir, '.config', 'opencode');
|
|
50
60
|
}
|
|
51
61
|
|
|
52
|
-
// Production default (no injection)
|
|
53
|
-
if (process.platform === 'win32') {
|
|
54
|
-
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
55
|
-
return path.join(appData, 'opencode');
|
|
56
|
-
}
|
|
57
62
|
return path.join(os.homedir(), '.config', 'opencode');
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
/**
|
|
66
|
+
* When OPENCODE_CONFIG_DIR is set, verify the directory exists and is writable.
|
|
67
|
+
* Emits a clear stderr message on failure. No-op when the env var is unset.
|
|
68
|
+
* @returns {boolean} true when OpenCode global installs may proceed
|
|
69
|
+
*/
|
|
70
|
+
function validateOpenCodeConfigDir() {
|
|
71
|
+
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
72
|
+
if (!envDir || !String(envDir).trim()) return true;
|
|
73
|
+
|
|
74
|
+
const dir = path.resolve(envDir.trim());
|
|
75
|
+
if (!fs.existsSync(dir)) {
|
|
76
|
+
process.stderr.write(
|
|
77
|
+
`[refacil-sdd-ai] OPENCODE_CONFIG_DIR is not accessible: directory does not exist: ${dir}\n`,
|
|
78
|
+
);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
process.stderr.write(
|
|
86
|
+
`[refacil-sdd-ai] OPENCODE_CONFIG_DIR is not writable: ${dir} (${err.message})\n`,
|
|
87
|
+
);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
60
94
|
/**
|
|
61
95
|
* Returns the global Codex CLI user directory.
|
|
62
96
|
* Always ~/.codex regardless of OS.
|
|
@@ -118,6 +152,8 @@ module.exports = {
|
|
|
118
152
|
globalClaudeDir,
|
|
119
153
|
globalCursorDir,
|
|
120
154
|
globalOpenCodeDir,
|
|
155
|
+
validateOpenCodeConfigDir,
|
|
156
|
+
legacyOpenCodeDirs,
|
|
121
157
|
globalCodexDir,
|
|
122
158
|
globalSddVersionPath,
|
|
123
159
|
globalSelectedIDEsPath,
|
package/lib/hooks.js
CHANGED
|
@@ -54,6 +54,14 @@ function installCursorHooks(homeDir, projectRoot) {
|
|
|
54
54
|
command: 'refacil-sdd-ai check-update',
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
// Remove legacy workspaceOpen check-update (duplicated sessionStart → double CodeGraph index).
|
|
58
|
+
if (config.hooks.workspaceOpen) {
|
|
59
|
+
const before = config.hooks.workspaceOpen.length;
|
|
60
|
+
config.hooks.workspaceOpen = config.hooks.workspaceOpen.filter((h) => h._sdd_workspace !== true);
|
|
61
|
+
if (config.hooks.workspaceOpen.length !== before) changed = true;
|
|
62
|
+
if (config.hooks.workspaceOpen.length === 0) delete config.hooks.workspaceOpen;
|
|
63
|
+
}
|
|
64
|
+
|
|
57
65
|
// compact-bash must be BEFORE check-review
|
|
58
66
|
if (!config.hooks.preToolUse) config.hooks.preToolUse = [];
|
|
59
67
|
if (!config.hooks.preToolUse.some((h) => h._sdd_compact === true)) {
|
|
@@ -96,7 +104,7 @@ function uninstallCursorHooks(homeDir, projectRoot) {
|
|
|
96
104
|
try { config = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8')); } catch (_) { config = null; }
|
|
97
105
|
|
|
98
106
|
if (config && config.hooks) {
|
|
99
|
-
const sddMarkers = ['_sdd', '_sdd_compact', '_sdd_review', '_sdd_notify'];
|
|
107
|
+
const sddMarkers = ['_sdd', '_sdd_workspace', '_sdd_compact', '_sdd_review', '_sdd_notify'];
|
|
100
108
|
for (const event of Object.keys(config.hooks)) {
|
|
101
109
|
if (!Array.isArray(config.hooks[event])) continue;
|
|
102
110
|
const before = config.hooks[event].length;
|
|
@@ -249,7 +257,7 @@ function uninstallClaudeHooks(homeDir) {
|
|
|
249
257
|
// ── Limpieza de hooks SDD en settings.json (migracion legacy) ───────────────
|
|
250
258
|
|
|
251
259
|
function cleanLegacySettingsHooks(projectRoot) {
|
|
252
|
-
const sddMarkers = ['_sdd', '_sdd_compact', '_sdd_review', '_sdd_notify'];
|
|
260
|
+
const sddMarkers = ['_sdd', '_sdd_workspace', '_sdd_compact', '_sdd_review', '_sdd_notify'];
|
|
253
261
|
const evts = ['SessionStart', 'PreToolUse', 'UserPromptSubmit', 'beforeSubmitPrompt', 'Stop', 'afterAgentResponse'];
|
|
254
262
|
|
|
255
263
|
for (const ideDir of ['.cursor']) {
|
|
@@ -289,9 +297,17 @@ function installOpenCodePlugin(homeDir) {
|
|
|
289
297
|
|
|
290
298
|
const srcPlugin = path.join(__dirname, 'opencode-plugin', 'index.js');
|
|
291
299
|
const destPlugin = path.join(pluginsDir, 'refacil-hooks.js');
|
|
300
|
+
const srcCheckReview = path.join(__dirname, 'check-review.js');
|
|
301
|
+
const destCheckReview = path.join(pluginsDir, 'refacil-check-review.js');
|
|
302
|
+
const srcRules = path.join(__dirname, 'opencode-plugin', 'rules.js');
|
|
303
|
+
const destRules = path.join(pluginsDir, 'rules.js');
|
|
292
304
|
|
|
293
305
|
try {
|
|
294
306
|
fs.copyFileSync(srcPlugin, destPlugin);
|
|
307
|
+
fs.copyFileSync(srcCheckReview, destCheckReview);
|
|
308
|
+
if (fs.existsSync(srcRules)) {
|
|
309
|
+
fs.copyFileSync(srcRules, destRules);
|
|
310
|
+
}
|
|
295
311
|
return true;
|
|
296
312
|
} catch (err) {
|
|
297
313
|
process.stderr.write(`[refacil-sdd-ai] Could not install OpenCode plugin: ${err.message}\n`);
|
|
@@ -307,9 +323,15 @@ function uninstallOpenCodePlugin(homeDir) {
|
|
|
307
323
|
const resolvedHome = homeDir || os.homedir();
|
|
308
324
|
const ocDir = globalOpenCodeDir(resolvedHome);
|
|
309
325
|
const pluginPath = path.join(ocDir, 'plugins', 'refacil-hooks.js');
|
|
310
|
-
|
|
326
|
+
const checkReviewPath = path.join(ocDir, 'plugins', 'refacil-check-review.js');
|
|
327
|
+
const rulesPath = path.join(ocDir, 'plugins', 'rules.js');
|
|
328
|
+
if (!fs.existsSync(pluginPath) && !fs.existsSync(checkReviewPath) && !fs.existsSync(rulesPath)) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
311
331
|
try {
|
|
312
|
-
fs.unlinkSync(pluginPath);
|
|
332
|
+
if (fs.existsSync(pluginPath)) fs.unlinkSync(pluginPath);
|
|
333
|
+
if (fs.existsSync(checkReviewPath)) fs.unlinkSync(checkReviewPath);
|
|
334
|
+
if (fs.existsSync(rulesPath)) fs.unlinkSync(rulesPath);
|
|
313
335
|
return true;
|
|
314
336
|
} catch (err) {
|
|
315
337
|
process.stderr.write(`[refacil-sdd-ai] Could not remove OpenCode plugin: ${err.message}\n`);
|
|
@@ -326,6 +348,12 @@ function uninstallOpenCodePlugin(homeDir) {
|
|
|
326
348
|
* @param {string} projectRoot
|
|
327
349
|
*/
|
|
328
350
|
function removeProjectLevelHooks(projectRoot) {
|
|
351
|
+
// Safety guard: never strip hooks from the global ~/.claude/settings.json.
|
|
352
|
+
// If projectRoot is the home directory (findProjectRoot() fallback), skip entirely.
|
|
353
|
+
const resolvedRoot = require('path').resolve(projectRoot);
|
|
354
|
+
const resolvedHome = require('path').resolve(require('os').homedir());
|
|
355
|
+
if (resolvedRoot === resolvedHome) return;
|
|
356
|
+
|
|
329
357
|
const sddMarkers = ['_sdd', '_sdd_compact', '_sdd_review', '_sdd_notify'];
|
|
330
358
|
|
|
331
359
|
// .claude/settings.json
|
package/lib/ide-detection.js
CHANGED
|
@@ -6,7 +6,7 @@ const { spawnSync } = require('child_process');
|
|
|
6
6
|
* Detects which IDEs are installed on the current system.
|
|
7
7
|
* Uses `where` on Windows and `which` on macOS/Linux.
|
|
8
8
|
* Error-tolerant: catches per-IDE errors silently.
|
|
9
|
-
* @returns {string[]} subset of ['claude', 'cursor', 'opencode']
|
|
9
|
+
* @returns {string[]} subset of ['claude', 'cursor', 'opencode', 'codex']
|
|
10
10
|
*/
|
|
11
11
|
function detectInstalledIDEs() {
|
|
12
12
|
const candidates = [
|
package/lib/ignore-files.js
CHANGED
|
@@ -64,12 +64,16 @@ function syncIgnoreFile(filePath) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const existing = fs.readFileSync(filePath, 'utf8');
|
|
67
|
+
// Trim each line before comparison — handles CRLF (Windows) vs LF differences
|
|
68
|
+
// so a file that differs only in line endings is treated as up-to-date (CA-05).
|
|
67
69
|
const existingLines = existing.split('\n').map((l) => l.trim());
|
|
68
70
|
|
|
69
71
|
const missing = BASE_ENTRIES.filter(
|
|
70
72
|
(entry) => isSignificant(entry) && !existingLines.includes(entry.trim()),
|
|
71
73
|
);
|
|
72
74
|
|
|
75
|
+
// No new entries needed — noop path. No additional idempotency guard is required
|
|
76
|
+
// because the initial creation path (file absent) is always correct when missing.
|
|
73
77
|
if (missing.length === 0) {
|
|
74
78
|
return { status: 'noop', added: 0 };
|
|
75
79
|
}
|
|
@@ -94,4 +98,4 @@ function syncIgnoreFiles(projectRoot, ideDirs) {
|
|
|
94
98
|
return result;
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
module.exports = { syncIgnoreFiles, BASE_ENTRIES };
|
|
101
|
+
module.exports = { syncIgnoreFiles, syncIgnoreFile, BASE_ENTRIES };
|