@boyingliu01/xp-gate 0.31.3 → 0.32.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/adapters/java.sh +256 -4
- package/adapters/powershell.sh +45 -6
- package/bin/xp-gate.js +8 -2
- package/build-integrity/__tests__/gate-10.test.ts +88 -1
- package/build-integrity/gate-10.ts +51 -16
- package/dsh.bundle.patch.yml +15 -0
- package/gate-13-compiler-strictness.sh +1031 -0
- package/gate-3.sh +48 -13
- package/hooks/gate-13-compiler-strictness.sh +1031 -0
- package/hooks/gate-3.sh +48 -13
- package/hooks/lib/coverage-total.sh +18 -2
- package/hooks/lib/java-test-selection.sh +251 -0
- package/hooks/lib/jscpd-run.sh +9 -0
- package/hooks/lib/validate-code-walkthrough.cjs +186 -29
- package/hooks/pre-commit +269 -13
- package/lib/__tests__/detect-deps-dsh-profile.test.js +106 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +140 -0
- package/lib/__tests__/gate-runner.test.js +29 -2
- package/lib/__tests__/issue-586-principles-cmdline.test.js +35 -0
- package/lib/__tests__/issue-618-gate-not-run.test.js +100 -0
- package/lib/__tests__/shared-utils.test.js +21 -0
- package/lib/check.js +6 -2
- package/lib/detect-deps.js +42 -10
- package/lib/doctor-tui.js +111 -21
- package/lib/gate-runner.js +111 -37
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +48 -6
- package/lib/shared-utils.js +8 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mock-policy/gate-m3.ts +57 -23
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/pitest-gradle-availability.test.ts +74 -0
- package/mutation/__tests__/test-filter-parity.test.ts +5 -2
- package/mutation/gate-m.ts +30 -24
- package/mutation/runners/mutmut-runner.ts +80 -30
- package/mutation/runners/pitest-runner.ts +60 -42
- package/package.json +14 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/lib/command.d.ts +38 -0
- package/plugins/dsh/lib/command.d.ts.map +1 -0
- package/plugins/dsh/lib/command.js +87 -0
- package/plugins/dsh/lib/credentials.d.ts +39 -0
- package/plugins/dsh/lib/credentials.d.ts.map +1 -0
- package/plugins/dsh/lib/credentials.js +57 -0
- package/plugins/dsh/lib/delphi-config.d.ts +129 -0
- package/plugins/dsh/lib/delphi-config.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-config.js +369 -0
- package/plugins/dsh/lib/delphi-run.d.ts +24 -0
- package/plugins/dsh/lib/delphi-run.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-run.js +113 -0
- package/plugins/dsh/lib/delphi.d.ts +94 -0
- package/plugins/dsh/lib/delphi.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi.js +207 -0
- package/plugins/dsh/lib/dialect.d.ts +24 -0
- package/plugins/dsh/lib/dialect.d.ts.map +1 -0
- package/plugins/dsh/lib/dialect.js +36 -0
- package/plugins/dsh/lib/evidence.d.ts +70 -0
- package/plugins/dsh/lib/evidence.d.ts.map +1 -0
- package/plugins/dsh/lib/evidence.js +92 -0
- package/plugins/dsh/lib/gate-runner.d.ts +49 -0
- package/plugins/dsh/lib/gate-runner.d.ts.map +1 -0
- package/plugins/dsh/lib/gate-runner.js +95 -0
- package/plugins/dsh/lib/git-context.d.ts +26 -0
- package/plugins/dsh/lib/git-context.d.ts.map +1 -0
- package/plugins/dsh/lib/git-context.js +30 -0
- package/plugins/dsh/lib/hooks.d.ts +42 -0
- package/plugins/dsh/lib/hooks.d.ts.map +1 -0
- package/plugins/dsh/lib/hooks.js +113 -0
- package/plugins/dsh/lib/index.d.ts +20 -0
- package/plugins/dsh/lib/index.d.ts.map +1 -0
- package/plugins/dsh/lib/index.js +430 -0
- package/plugins/dsh/lib/skill-provider.d.ts +15 -0
- package/plugins/dsh/lib/skill-provider.d.ts.map +1 -0
- package/plugins/dsh/lib/skill-provider.js +205 -0
- package/plugins/dsh/lib/sprint.d.ts +60 -0
- package/plugins/dsh/lib/sprint.d.ts.map +1 -0
- package/plugins/dsh/lib/sprint.js +92 -0
- package/plugins/dsh/package.json +14 -8
- package/plugins/dsh/scripts/verify-skill-provider.mjs +113 -0
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/command-single-form.test.ts +67 -0
- package/plugins/dsh/src/command.test.ts +21 -17
- package/plugins/dsh/src/command.ts +19 -21
- package/plugins/dsh/src/compat.test.ts +200 -0
- package/plugins/dsh/src/contract.test.ts +7 -1
- package/plugins/dsh/src/credentials.test.ts +93 -0
- package/plugins/dsh/src/credentials.ts +71 -0
- package/plugins/dsh/src/delphi-cancellation.test.ts +71 -0
- package/plugins/dsh/src/delphi-config-apikey-override.test.ts +94 -0
- package/plugins/dsh/src/delphi-config-credential-refs.test.ts +75 -0
- package/plugins/dsh/src/delphi-config.ts +146 -53
- package/plugins/dsh/src/delphi-run.ts +27 -4
- package/plugins/dsh/src/delphi.ts +133 -65
- package/plugins/dsh/src/evidence.test.ts +74 -0
- package/plugins/dsh/src/evidence.ts +141 -0
- package/plugins/dsh/src/gate-runner.test.ts +14 -6
- package/plugins/dsh/src/gate-runner.ts +68 -5
- package/plugins/dsh/src/git-context.test.ts +39 -0
- package/plugins/dsh/src/git-context.ts +38 -0
- package/plugins/dsh/src/hooks.test.ts +87 -0
- package/plugins/dsh/src/hooks.ts +126 -0
- package/plugins/dsh/src/index.test.ts +63 -6
- package/plugins/dsh/src/index.ts +340 -13
- package/plugins/dsh/src/skill-provider.test.ts +194 -0
- package/plugins/dsh/src/skill-provider.ts +231 -0
- package/plugins/dsh/src/sprint.test.ts +64 -0
- package/plugins/dsh/src/sprint.ts +122 -0
- package/plugins/dsh/src/tri-state.test.ts +123 -0
- package/plugins/opencode/index.ts +192 -58
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/__tests__/analyze-read-failure.test.ts +70 -0
- package/principles/__tests__/baseline-entry-total-warnings.test.ts +76 -0
- package/principles/__tests__/baseline-prune.test.ts +61 -0
- package/principles/__tests__/cli-files-separators.test.ts +69 -0
- package/principles/__tests__/config-merge-warnings.test.ts +148 -0
- package/principles/adapters/__tests__/cpp-code-block.test.ts +123 -0
- package/principles/adapters/__tests__/cpp-scanner-equivalence.test.ts +67 -0
- package/principles/adapters/__tests__/typescript-block-comment-lines.test.ts +53 -0
- package/principles/adapters/__tests__/typescript-masking.test.ts +96 -0
- package/principles/adapters/cpp.ts +102 -50
- package/principles/adapters/java.ts +22 -3
- package/principles/adapters/typescript.ts +548 -178
- package/principles/analyzer.ts +36 -3
- package/principles/baseline-prune.ts +19 -7
- package/principles/baseline.ts +58 -70
- package/principles/config.ts +56 -35
- package/principles/index.ts +122 -40
- package/principles/rules/clean-code/deep-nesting.ts +36 -22
- package/principles/rules/clean-code/unused-imports.ts +30 -19
- package/principles/rules/solid/dip.ts +30 -20
- package/principles/rules/solid/lsp.ts +17 -2
- package/principles/rules/solid/ocp.ts +33 -23
- package/principles/rules/solid/srp.ts +65 -23
- package/scripts/delphi-external-review.cjs +25 -2
- package/scripts/sync-package-content.js +28 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-635
|
|
3
|
+
* @intent `xp-gate init` 会向 ~/.dsh/skills 部署技能、npm 包也带 plugins/dsh,
|
|
4
|
+
* 但 PLATFORM_PROFILES 里没有 dsh 条目,getSkillsDirs('dsh') 静默回落到
|
|
5
|
+
* opencode 的路径,导致 DSH 机器的技能从不被解析(#635)
|
|
6
|
+
* @covers AC-635-04, AC-635-05
|
|
7
|
+
*/
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
describe('PLATFORM_PROFILES covers dsh (#635)', () => {
|
|
13
|
+
let tmpHome;
|
|
14
|
+
let originalHome;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
originalHome = process.env.HOME;
|
|
18
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-dsh-'));
|
|
19
|
+
process.env.HOME = tmpHome;
|
|
20
|
+
vi.resetModules();
|
|
21
|
+
delete require.cache[require.resolve('../detect-deps')];
|
|
22
|
+
delete require.cache[require.resolve('../shared-paths')];
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
process.env.HOME = originalHome;
|
|
27
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// AC-635-04: asking for dsh skills must return the dsh directory, not a
|
|
31
|
+
// silent fallback to opencode's.
|
|
32
|
+
it('getSkillsDirs("dsh") resolves ~/.dsh, not the opencode fallback', () => {
|
|
33
|
+
const { getSkillsDirs } = require('../detect-deps');
|
|
34
|
+
const dirs = getSkillsDirs('dsh');
|
|
35
|
+
|
|
36
|
+
expect(dirs.length).toBeGreaterThan(0);
|
|
37
|
+
expect(dirs[0]).toBe(path.join(tmpHome, '.dsh', 'skills'));
|
|
38
|
+
// The pre-fix behaviour: falls through to PLATFORM_PROFILES.opencode.
|
|
39
|
+
expect(dirs[0]).not.toContain(path.join('.config', 'opencode'));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// AC-635-05: the profile is a real entry, so a caller enumerating platforms
|
|
43
|
+
// sees dsh alongside the three that were already there.
|
|
44
|
+
it('dsh is an enumerable platform profile with skillsDirs and requiredDeps', () => {
|
|
45
|
+
const { PLATFORM_PROFILES, getSkillsDirs } = require('../detect-deps');
|
|
46
|
+
|
|
47
|
+
expect(Object.keys(PLATFORM_PROFILES)).toContain('dsh');
|
|
48
|
+
expect(Object.keys(PLATFORM_PROFILES)).toEqual(
|
|
49
|
+
expect.arrayContaining(['opencode', 'claude-code', 'qoder', 'dsh'])
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const profile = PLATFORM_PROFILES.dsh;
|
|
53
|
+
expect(Array.isArray(profile.skillsDirs)).toBe(true);
|
|
54
|
+
expect(profile.requiredDeps).toBeDefined();
|
|
55
|
+
// Profiles store home-relative segments; getSkillsDirs resolves them.
|
|
56
|
+
expect(profile.skillsDirs[0]).toEqual(['.dsh', 'skills']);
|
|
57
|
+
expect(getSkillsDirs('dsh')[0]).toBe(path.join(tmpHome, '.dsh', 'skills'));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Every profile must be plain data — no getters mixed in with values, which
|
|
61
|
+
// would break Object.keys()/JSON round-trips and made the object's contract
|
|
62
|
+
// depend on which key you touched (Delphi R1 CI-01).
|
|
63
|
+
it('every profile is plain enumerable data, with no accessor properties', () => {
|
|
64
|
+
const { PLATFORM_PROFILES } = require('../detect-deps');
|
|
65
|
+
|
|
66
|
+
for (const name of Object.keys(PLATFORM_PROFILES)) {
|
|
67
|
+
const descriptor = Object.getOwnPropertyDescriptor(PLATFORM_PROFILES, name);
|
|
68
|
+
expect(descriptor.get, `${name} must not be an accessor`).toBeUndefined();
|
|
69
|
+
// A plain value also survives a JSON round-trip unchanged.
|
|
70
|
+
expect(JSON.parse(JSON.stringify(PLATFORM_PROFILES[name]))).toEqual(
|
|
71
|
+
PLATFORM_PROFILES[name]
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Paths must follow process.env.HOME rather than pinning the home directory
|
|
77
|
+
// at require() time — the trap doctor.js documents at its top.
|
|
78
|
+
it('resolved paths follow a HOME change after require', () => {
|
|
79
|
+
const { getSkillsDirs } = require('../detect-deps');
|
|
80
|
+
// Loaded above with the first tmpHome; move HOME and resolve again.
|
|
81
|
+
const secondHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-dsh2-'));
|
|
82
|
+
try {
|
|
83
|
+
process.env.HOME = secondHome;
|
|
84
|
+
expect(getSkillsDirs('dsh')[0]).toBe(path.join(secondHome, '.dsh', 'skills'));
|
|
85
|
+
// The pre-existing profiles must follow too — they used to be frozen at
|
|
86
|
+
// require() time from a destructured HOME_DIR.
|
|
87
|
+
expect(getSkillsDirs('opencode')[0]).toBe(
|
|
88
|
+
path.join(secondHome, '.config', 'opencode', 'skills')
|
|
89
|
+
);
|
|
90
|
+
} finally {
|
|
91
|
+
fs.rmSync(secondHome, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Anti-vacuity: the three pre-existing profiles must keep their own paths,
|
|
96
|
+
// proving the getter did not disturb them.
|
|
97
|
+
it('leaves the pre-existing profiles unchanged', () => {
|
|
98
|
+
const { getSkillsDirs } = require('../detect-deps');
|
|
99
|
+
|
|
100
|
+
expect(getSkillsDirs('opencode')[0]).toBe(
|
|
101
|
+
path.join(tmpHome, '.config', 'opencode', 'skills')
|
|
102
|
+
);
|
|
103
|
+
expect(getSkillsDirs('qoder')[0]).toBe(path.join(tmpHome, '.qoder', 'skills'));
|
|
104
|
+
expect(getSkillsDirs('claude-code')[0]).toBe(path.join(tmpHome, '.claude', 'skills'));
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -154,3 +154,143 @@ describe('doctor-tui skill freshness (#439)', () => {
|
|
|
154
154
|
expect(missing, `skills missing version frontmatter: ${missing.join(', ')}`).toEqual([]);
|
|
155
155
|
});
|
|
156
156
|
});
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @test REQ-635
|
|
160
|
+
* @intent doctor 的 "Installed skills" 检查从 config.installedSkills 取清单,
|
|
161
|
+
* 而 xp-gate init 从不写该字段,导致全新安装的机器(12/12 技能已部署)
|
|
162
|
+
* 稳定报 "No skills installed" 并在读盘前就返回;同时只解析单一平台,
|
|
163
|
+
* init 实际部署的另外两个平台(含 .dsh)从不校验(#635)
|
|
164
|
+
* @covers AC-635-01, AC-635-02, AC-635-03
|
|
165
|
+
*/
|
|
166
|
+
describe('doctor-tui installed skills enumerate the filesystem (#635)', () => {
|
|
167
|
+
let tmpHome;
|
|
168
|
+
let originalHome;
|
|
169
|
+
|
|
170
|
+
// Same dir doctor-tui resolves as PKG_DIR (path.dirname of lib/). Re-declared
|
|
171
|
+
// here because the outer describe's copy is scoped to that callback.
|
|
172
|
+
const bundledSkillsDir = path.join(__dirname, '..', '..', 'skills');
|
|
173
|
+
|
|
174
|
+
// Every directory init deploys to. `.dsh` is the one detectPlatform() cannot
|
|
175
|
+
// report, and the one that went unverified entirely.
|
|
176
|
+
const DEPLOY_DIRS = [
|
|
177
|
+
['opencode', ['.config', 'opencode', 'skills']],
|
|
178
|
+
['dsh', ['.dsh', 'skills']],
|
|
179
|
+
['qoder', ['.qoder', 'skills']],
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
beforeEach(() => {
|
|
183
|
+
originalHome = process.env.HOME;
|
|
184
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-635-'));
|
|
185
|
+
process.env.HOME = tmpHome;
|
|
186
|
+
vi.resetModules();
|
|
187
|
+
delete require.cache[require.resolve('../doctor-tui')];
|
|
188
|
+
delete require.cache[require.resolve('../shared-paths')];
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
afterEach(() => {
|
|
192
|
+
process.env.HOME = originalHome;
|
|
193
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
/** Deploy a real bundled skill into one platform's skills dir. */
|
|
197
|
+
function deploy(platformDirs, name) {
|
|
198
|
+
const dir = path.join(tmpHome, ...platformDirs, name);
|
|
199
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
200
|
+
const bundled = fs.readFileSync(path.join(bundledSkillsDir, name, 'SKILL.md'), 'utf8');
|
|
201
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), bundled);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// AC-635-01: a machine with skills deployed and NO installedSkills config key
|
|
205
|
+
// must be verified, not reported as "No skills installed".
|
|
206
|
+
it('verifies deployed skills even when config.installedSkills is absent', () => {
|
|
207
|
+
deploy(['.config', 'opencode', 'skills'], 'grilling');
|
|
208
|
+
const { diagnoseInstalledSkills } = require('../doctor-tui');
|
|
209
|
+
const checks = [];
|
|
210
|
+
// init writes exactly this shape: no installedSkills key at all.
|
|
211
|
+
const issues = diagnoseInstalledSkills({ mode: 'global', templateDir: '/x' }, checks);
|
|
212
|
+
|
|
213
|
+
expect(issues).toBe(0);
|
|
214
|
+
const skills = checks.filter(c => c.name.startsWith('Skill '));
|
|
215
|
+
expect(skills.length).toBeGreaterThan(0);
|
|
216
|
+
expect(skills.every(c => c.status === 'PASS')).toBe(true);
|
|
217
|
+
// The old behaviour: a single SKIP row and zero skills examined.
|
|
218
|
+
expect(checks.find(c => c.name === 'Installed skills')).toBeUndefined();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// AC-635-02: `.dsh` is a first-class deploy target and must be checked, even
|
|
222
|
+
// though detectPlatform() never returns 'dsh'.
|
|
223
|
+
it('checks the .dsh platform, which detectPlatform() cannot report', () => {
|
|
224
|
+
deploy(['.dsh', 'skills'], 'grilling');
|
|
225
|
+
const { diagnoseInstalledSkills } = require('../doctor-tui');
|
|
226
|
+
const checks = [];
|
|
227
|
+
diagnoseInstalledSkills({ mode: 'global' }, checks);
|
|
228
|
+
|
|
229
|
+
const dshRows = checks.filter(c => c.name.startsWith('Skill [dsh]'));
|
|
230
|
+
expect(dshRows.length).toBeGreaterThan(0);
|
|
231
|
+
expect(dshRows[0].status).toBe('PASS');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// AC-635-03: all deployed platforms are verified in one run, not just the
|
|
235
|
+
// one detectPlatform() happens to pick.
|
|
236
|
+
it('verifies every deployed platform, not only the detected one', () => {
|
|
237
|
+
for (const [, dirs] of DEPLOY_DIRS) deploy(dirs, 'grilling');
|
|
238
|
+
const { diagnoseInstalledSkills } = require('../doctor-tui');
|
|
239
|
+
const checks = [];
|
|
240
|
+
diagnoseInstalledSkills({ mode: 'global' }, checks);
|
|
241
|
+
|
|
242
|
+
for (const [platform] of DEPLOY_DIRS) {
|
|
243
|
+
const rows = checks.filter(c => c.name.startsWith(`Skill [${platform}]`));
|
|
244
|
+
expect(rows.length, `no checks emitted for platform ${platform}`).toBeGreaterThan(0);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Anti-vacuity: enumeration must still surface real drift. A stale copy in
|
|
249
|
+
// .dsh specifically must WARN — proving the new rows are not decorative.
|
|
250
|
+
it('reports drift found in a non-detected platform (anti-vacuity)', () => {
|
|
251
|
+
deploy(['.config', 'opencode', 'skills'], 'grilling');
|
|
252
|
+
const staleDir = path.join(tmpHome, '.dsh', 'skills', 'grilling');
|
|
253
|
+
fs.mkdirSync(staleDir, { recursive: true });
|
|
254
|
+
fs.writeFileSync(
|
|
255
|
+
path.join(staleDir, 'SKILL.md'),
|
|
256
|
+
'---\nname: grilling\nversion: 0.0.1\n---\n\nstale body\n'
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const { diagnoseInstalledSkills } = require('../doctor-tui');
|
|
260
|
+
const checks = [];
|
|
261
|
+
const issues = diagnoseInstalledSkills({ mode: 'global' }, checks);
|
|
262
|
+
|
|
263
|
+
expect(issues).toBeGreaterThan(0);
|
|
264
|
+
const stale = checks.find(c => c.name === 'Skill [dsh]: grilling');
|
|
265
|
+
expect(stale).toBeDefined();
|
|
266
|
+
expect(stale.status).toBe('WARN');
|
|
267
|
+
expect(stale.detail).toContain('Outdated');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Honest SKIP: nothing deployed anywhere must say so, and name the locations
|
|
271
|
+
// it looked at — the old message could not be told apart from "not wired up".
|
|
272
|
+
it('reports SKIP naming every searched location when nothing is deployed', () => {
|
|
273
|
+
const { diagnoseInstalledSkills } = require('../doctor-tui');
|
|
274
|
+
const checks = [];
|
|
275
|
+
const issues = diagnoseInstalledSkills({ mode: 'global' }, checks);
|
|
276
|
+
|
|
277
|
+
expect(issues).toBe(0);
|
|
278
|
+
const row = checks.find(c => c.name === 'Installed skills');
|
|
279
|
+
expect(row).toBeDefined();
|
|
280
|
+
expect(row.status).toBe('SKIP');
|
|
281
|
+
expect(row.detail).toContain('dsh');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Backwards compatibility: an explicit list keeps the legacy single-target
|
|
285
|
+
// contract and the unprefixed check names existing callers rely on.
|
|
286
|
+
it('honours an explicit installedSkills list without the platform prefix', () => {
|
|
287
|
+
deploy(['.config', 'opencode', 'skills'], 'grilling');
|
|
288
|
+
const { diagnoseInstalledSkills } = require('../doctor-tui');
|
|
289
|
+
const checks = [];
|
|
290
|
+
diagnoseInstalledSkills({ installedSkills: { grilling: {} } }, checks);
|
|
291
|
+
|
|
292
|
+
const row = checks.find(c => c.name === 'Skill: grilling');
|
|
293
|
+
expect(row).toBeDefined();
|
|
294
|
+
expect(row.status).toBe('PASS');
|
|
295
|
+
});
|
|
296
|
+
});
|
|
@@ -67,10 +67,11 @@ function initializeGitProject() {
|
|
|
67
67
|
|
|
68
68
|
// Runs runGateAdapter in a child node process (worker threads forbid process
|
|
69
69
|
// chdir, and runGateAdapter execs bash), capturing stdout/exit status.
|
|
70
|
-
function runChildAdapter(gateRunnerAbsPath, fragmentPath, cwd = tmpProject) {
|
|
70
|
+
function runChildAdapter(gateRunnerAbsPath, fragmentPath, cwd = tmpProject, opts = null) {
|
|
71
|
+
const optsArgument = opts ? `, ${JSON.stringify(opts)}` : '';
|
|
71
72
|
const script = [
|
|
72
73
|
`const { runGateAdapter } = require(${JSON.stringify(gateRunnerAbsPath)});`,
|
|
73
|
-
`runGateAdapter(${JSON.stringify(fragmentPath)}, ${JSON.stringify(cwd)});`,
|
|
74
|
+
`runGateAdapter(${JSON.stringify(fragmentPath)}, ${JSON.stringify(cwd)}${optsArgument});`,
|
|
74
75
|
"console.log('__ADAPTER_DONE__');",
|
|
75
76
|
].join('\n');
|
|
76
77
|
try {
|
|
@@ -146,6 +147,32 @@ describe('gate-runner.js — runGateAdapter', () => {
|
|
|
146
147
|
expect(stdout).toContain('__ADAPTER_DONE__');
|
|
147
148
|
}, 30000);
|
|
148
149
|
|
|
150
|
+
it('propagates the whole tracked tree under scope=all, one file per line (#589/#<new>)', () => {
|
|
151
|
+
const runnerAbsPath = path.resolve(__dirname, '../gate-runner.js');
|
|
152
|
+
writeAdapterCommon();
|
|
153
|
+
initializeGitProject();
|
|
154
|
+
fs.writeFileSync(path.join(tmpProject, 'untracked.ts'), 'const value = 3;\n');
|
|
155
|
+
// Two tracked files so a one-line join (the old JSON.stringify form) would fail
|
|
156
|
+
// the equality check even though the list was non-empty.
|
|
157
|
+
fs.writeFileSync(path.join(tmpProject, 'second.ts'), 'const value = 4;\n');
|
|
158
|
+
execFileSync('git', ['add', 'second.ts'], { cwd: tmpProject });
|
|
159
|
+
execFileSync('git', ['-c', 'core.hooksPath=/dev/null', 'commit', '-qm', 'second'], {
|
|
160
|
+
cwd: tmpProject,
|
|
161
|
+
});
|
|
162
|
+
writeGateFragment([
|
|
163
|
+
`printf '%s\\n' "${shellParameter('CHANGED_FILES')}" | grep -qx 'tracked.ts' || exit 87`,
|
|
164
|
+
`printf '%s\\n' "${shellParameter('CHANGED_FILES')}" | grep -qx 'second.ts' || exit 86`,
|
|
165
|
+
`[ "$(printf '%s\\n' "${shellParameter('CHANGED_FILES')}" | wc -l)" -eq 2 ] || exit 85`,
|
|
166
|
+
`if [ "${shellParameter('GATE_SCOPE')}" != "all" ]; then exit 84; fi`,
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
const fragPath = path.join(tmpAdapters, 'gate-9.sh');
|
|
170
|
+
const { status, stdout } = runChildAdapter(runnerAbsPath, fragPath, tmpProject, { scope: 'all' });
|
|
171
|
+
|
|
172
|
+
expect(status).toBe(0);
|
|
173
|
+
expect(stdout).toContain('__ADAPTER_DONE__');
|
|
174
|
+
}, 30000);
|
|
175
|
+
|
|
149
176
|
it('runs a standalone gate bash fallback through the public CLI (REQ-standalone-gates-05)', () => {
|
|
150
177
|
const packageFixture = path.join(tmpProject, 'package');
|
|
151
178
|
fs.cpSync(path.resolve(__dirname, '../..'), packageFixture, { recursive: true });
|
|
@@ -140,6 +140,41 @@ describe('REQ-613: Gate 4 must not blow the Windows command-line limit', () => {
|
|
|
140
140
|
expect(fs.existsSync(refPath) && plan.cleanup === false).toBe(false);
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
+
it('AC-613-02: without a local tsx, npm\u2019s own npx entry runs under node.exe', () => {
|
|
144
|
+
// `spawnSync('npx', ..., { shell: false })` is ENOENT on Windows: CreateProcess
|
|
145
|
+
// resolves only `.exe`, while the installed entry point is `npx.cmd`. Running
|
|
146
|
+
// npm's JS entry with the current node binary is the same program, without a shim
|
|
147
|
+
// — and without handing the file list to cmd.exe.
|
|
148
|
+
const existsSpy = vi.spyOn(fs, 'existsSync').mockImplementation((candidate) =>
|
|
149
|
+
String(candidate).endsWith(path.join('npm', 'bin', 'npx-cli.js'))
|
|
150
|
+
);
|
|
151
|
+
try {
|
|
152
|
+
const plan = principles.buildPrinciplesInvocation(['a.ts'], 'json');
|
|
153
|
+
expect(plan.command).toBe(process.execPath);
|
|
154
|
+
expect(plan.args[0]).toMatch(/npx-cli\.js$/);
|
|
155
|
+
expect(plan.useShell).toBe(false);
|
|
156
|
+
} finally {
|
|
157
|
+
existsSpy.mockRestore();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('AC-613-02: the bare-npx fallback asks for a shell exactly where the shim needs one', () => {
|
|
162
|
+
// Last resort when neither tsx nor npm's entry can be found. The shell flag must
|
|
163
|
+
// still travel with the plan: hard-coding false is what produced ENOENT on
|
|
164
|
+
// Windows, hard-coding true is what produced the 8191-character limit.
|
|
165
|
+
const existsSpy = vi.spyOn(fs, 'existsSync').mockReturnValue(false);
|
|
166
|
+
try {
|
|
167
|
+
const direct = principles.buildPrinciplesInvocation(['a.ts'], 'json');
|
|
168
|
+
expect(direct.command).toBe('npx');
|
|
169
|
+
expect(direct.useShell).toBe(process.platform === 'win32');
|
|
170
|
+
|
|
171
|
+
const byReference = principles.buildPrinciplesInvocation(longFileList(LARGE_FILE_COUNT), 'json');
|
|
172
|
+
expect(byReference.useShell).toBe(process.platform === 'win32');
|
|
173
|
+
} finally {
|
|
174
|
+
existsSpy.mockRestore();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
143
178
|
it('AC-613-02: the WIRE test \u2014 the call site actually uses the plan it builds', () => {
|
|
144
179
|
// This case exists because the first version of this suite had a hole: every
|
|
145
180
|
// other case asserts on `buildPrinciplesInvocation`'s OUTPUT, and reverting the
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-618 `check --all` must not count a gate that ran NOTHING as passed
|
|
3
|
+
* @intent Three gates the CLI advertises as runnable analysed nothing and were
|
|
4
|
+
* counted as passes. Measured on xp-gate 0.31.3:
|
|
5
|
+
*
|
|
6
|
+
* Gate python-health: TypeScript module not found and no bash fallback available.
|
|
7
|
+
* Gate test-layers: TypeScript module not found and no bash fallback available.
|
|
8
|
+
* Gate pbt: TypeScript module not found and no bash fallback available.
|
|
9
|
+
*
|
|
10
|
+
* [xp-gate check --all] 8 passed / 2 failed / 5 skipped / 15 total
|
|
11
|
+
*
|
|
12
|
+
* None of the three analysed anything, and all three were inside the
|
|
13
|
+
* `8 passed`. The root cause was `runTsGate()`'s no-entry branch: it
|
|
14
|
+
* printed a message and then fell through to `return 0`, which is
|
|
15
|
+
* indistinguishable from "ran and found nothing". #568 taught the
|
|
16
|
+
* summary to separate `preCommitOnly` skips from passes; this is the
|
|
17
|
+
* same distinction one level down -- a gate that could not run is a
|
|
18
|
+
* skip, not a pass.
|
|
19
|
+
*
|
|
20
|
+
* The second defect on the same path: the bash-fallback branch called
|
|
21
|
+
* `runGateAdapter(...)` and DISCARDED its verdict, so a fallback that
|
|
22
|
+
* failed still reported 0.
|
|
23
|
+
* @covers AC-618-01, AC-618-02, AC-618-03
|
|
24
|
+
*/
|
|
25
|
+
const gateRunner = require('../gate-runner.js');
|
|
26
|
+
const { checkAllGates } = require('../check.js');
|
|
27
|
+
|
|
28
|
+
const { GATE_REGISTRY, GATE_NOT_RUN } = gateRunner;
|
|
29
|
+
|
|
30
|
+
/** Original registry contents, restored after every test. */
|
|
31
|
+
let ORIGINAL_REGISTRY;
|
|
32
|
+
|
|
33
|
+
const SENTINEL_GATE_ID = '618-sentinel';
|
|
34
|
+
const PASS_GATE_ID = '618-pass';
|
|
35
|
+
const FAIL_GATE_ID = '618-fail';
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
ORIGINAL_REGISTRY = { ...GATE_REGISTRY };
|
|
39
|
+
for (const k of Object.keys(GATE_REGISTRY)) delete GATE_REGISTRY[k];
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
for (const k of Object.keys(GATE_REGISTRY)) delete GATE_REGISTRY[k];
|
|
44
|
+
Object.assign(GATE_REGISTRY, ORIGINAL_REGISTRY);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function installGates() {
|
|
48
|
+
GATE_REGISTRY[SENTINEL_GATE_ID] = {
|
|
49
|
+
name: 'ran nothing',
|
|
50
|
+
description: 'no module, no fallback',
|
|
51
|
+
aliases: [],
|
|
52
|
+
run: () => GATE_NOT_RUN,
|
|
53
|
+
};
|
|
54
|
+
GATE_REGISTRY[PASS_GATE_ID] = {
|
|
55
|
+
name: 'ran and passed',
|
|
56
|
+
description: 'ordinary passing gate',
|
|
57
|
+
aliases: [],
|
|
58
|
+
run: () => 0,
|
|
59
|
+
};
|
|
60
|
+
GATE_REGISTRY[FAIL_GATE_ID] = {
|
|
61
|
+
name: 'ran and failed',
|
|
62
|
+
description: 'ordinary failing gate',
|
|
63
|
+
aliases: [],
|
|
64
|
+
run: () => 1,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
test('AC-618-01: the sentinel is distinct from every exit code', () => {
|
|
69
|
+
// It must not be a number: 0 means "ran, found nothing" and 1 means "failed".
|
|
70
|
+
expect(typeof GATE_NOT_RUN).not.toBe('number');
|
|
71
|
+
expect(GATE_NOT_RUN).not.toBe(0);
|
|
72
|
+
expect(GATE_NOT_RUN).not.toBe(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('AC-618-02: a gate that ran nothing is counted as skipped, not passed', async () => {
|
|
76
|
+
installGates();
|
|
77
|
+
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
78
|
+
try {
|
|
79
|
+
await checkAllGates('.');
|
|
80
|
+
const printed = log.mock.calls.map(c => String(c[0])).join('\n');
|
|
81
|
+
// 3 gates total: one skipped (ran nothing), one passed, one failed.
|
|
82
|
+
expect(printed).toMatch(/1 passed \/ 1 failed \/ 1 skipped \/ 3 total/);
|
|
83
|
+
} finally {
|
|
84
|
+
log.mockRestore();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('AC-618-03: runGate propagates the sentinel instead of flattening it to 0', async () => {
|
|
89
|
+
installGates();
|
|
90
|
+
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
91
|
+
try {
|
|
92
|
+
// The sentinel must survive runGate(), otherwise check.js cannot see it.
|
|
93
|
+
expect(await gateRunner.runGate(SENTINEL_GATE_ID, '.')).toBe(GATE_NOT_RUN);
|
|
94
|
+
// And the ordinary verdicts must be untouched by the change.
|
|
95
|
+
expect(await gateRunner.runGate(PASS_GATE_ID, '.')).toBe(0);
|
|
96
|
+
expect(await gateRunner.runGate(FAIL_GATE_ID, '.')).toBe(1);
|
|
97
|
+
} finally {
|
|
98
|
+
log.mockRestore();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
@@ -65,5 +65,26 @@ describe('shared-utils', () => {
|
|
|
65
65
|
expect(fs.existsSync(path.join(destDir, 'verify.sh'))).toBe(false);
|
|
66
66
|
expect(fs.existsSync(path.join(destDir, 'VERSION'))).toBe(false);
|
|
67
67
|
});
|
|
68
|
+
it('copies hooks/lib so gate-3 can source the complexity ratchet library', () => {
|
|
69
|
+
// gate-3.sh resolves $GATE_DIR/lib/ccn-ratchet.sh, and GATE_DIR is this same
|
|
70
|
+
// adapters directory (init.js: copyAdapters(srcDir, GLOBAL_ADAPTERS_DIR)). The
|
|
71
|
+
// installer used to skip hooks/lib entirely, so the source failed with "No such
|
|
72
|
+
// file or directory", evaluate_ccn_ratchet stayed undefined and the ratchet
|
|
73
|
+
// graded every CCN regression as PASS.
|
|
74
|
+
const pkgRoot = path.join(tmpRoot, 'pkg');
|
|
75
|
+
fs.mkdirSync(path.join(pkgRoot, 'hooks', 'lib'), { recursive: true });
|
|
76
|
+
fs.writeFileSync(path.join(pkgRoot, 'gate-3.sh'), '# gate-3');
|
|
77
|
+
fs.writeFileSync(path.join(pkgRoot, 'hooks', 'lib', 'ccn-ratchet.sh'), '# ratchet');
|
|
78
|
+
fs.writeFileSync(path.join(pkgRoot, 'hooks', 'lib', 'now-ms.sh'), '# now-ms');
|
|
79
|
+
|
|
80
|
+
const destDir = path.join(tmpRoot, 'adapters');
|
|
81
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
82
|
+
|
|
83
|
+
const { copyAdapters } = require('../shared-utils');
|
|
84
|
+
copyAdapters(pkgRoot, destDir);
|
|
85
|
+
|
|
86
|
+
expect(fs.existsSync(path.join(destDir, 'lib', 'ccn-ratchet.sh'))).toBe(true);
|
|
87
|
+
expect(fs.existsSync(path.join(destDir, 'lib', 'now-ms.sh'))).toBe(true);
|
|
88
|
+
});
|
|
68
89
|
});
|
|
69
90
|
});
|
package/lib/check.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { principles } = require('./principles.js');
|
|
4
4
|
const { arch } = require('./arch.js');
|
|
5
|
-
const { getGateInfo, runGate, resolveAlias, getAliases } = require('./gate-runner.js');
|
|
5
|
+
const { getGateInfo, runGate, resolveAlias, getAliases, GATE_NOT_RUN } = require('./gate-runner.js');
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Resolve a token (gate ID or alias) to a canonical gate ID.
|
|
@@ -151,7 +151,11 @@ async function checkAllGates(target) {
|
|
|
151
151
|
// read CHANGED_FILES from scanning an empty diff and reporting SKIP — which
|
|
152
152
|
// read like a pass while checking LESS than an ordinary commit.
|
|
153
153
|
const code = await runGate(gate.id, target, { scope: 'all' });
|
|
154
|
-
|
|
154
|
+
// #618: a gate that ran nothing (no TS module, no bash fallback) returns the
|
|
155
|
+
// GATE_NOT_RUN sentinel. Counting it as passed put three gates that analysed
|
|
156
|
+
// nothing inside "8 passed"; it is a skip, and the summary must say so.
|
|
157
|
+
if (code === GATE_NOT_RUN) tally.skipped += 1;
|
|
158
|
+
else if (code === 0) tally.passed += 1;
|
|
155
159
|
else tally.failed += 1;
|
|
156
160
|
console.log('');
|
|
157
161
|
}
|
package/lib/detect-deps.js
CHANGED
|
@@ -3,7 +3,10 @@ const path = require('path');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const { execSync } = require('child_process');
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// HOME_DIR is deliberately NOT destructured here: the platform profiles keep
|
|
7
|
+
// home-relative path segments and resolve them through getSkillsDirs() instead,
|
|
8
|
+
// so nothing in this module freezes the home directory at require() time.
|
|
9
|
+
const { detectPlatform } = require('./shared-paths.js');
|
|
7
10
|
|
|
8
11
|
const REQUIRED_DEPS = [
|
|
9
12
|
{ name: 'superpowers', repo: 'obra/superpowers', minVersion: '1.0.0' },
|
|
@@ -231,27 +234,51 @@ function getToolInstallCmd(toolEntry, platform) {
|
|
|
231
234
|
* Platform profiles — each AI agent platform has its own skills directory.
|
|
232
235
|
* All platforms require the same dependencies (superpowers, gstack).
|
|
233
236
|
*
|
|
237
|
+
* `skillsDirs` is a list of path SEGMENTS relative to the home directory, not
|
|
238
|
+
* an absolute path. It used to hold `path.join(HOME_DIR, ...)` evaluated at
|
|
239
|
+
* require() time, which pinned every profile to whatever HOME was when
|
|
240
|
+
* detect-deps was first loaded — the same trap doctor.js documents at its top.
|
|
241
|
+
* Keeping segments here and resolving them in getSkillsDirs() makes the
|
|
242
|
+
* profiles plain, uniform data (no mix of getters and values) that follows
|
|
243
|
+
* process.env.HOME, and gives the platform-to-directory mapping one source of
|
|
244
|
+
* truth (Delphi R1 CI-01 / MA-01 on #635).
|
|
245
|
+
*
|
|
234
246
|
* @covers Issue #128 Bug 1 — Qoder platform was excluded from dependency checks
|
|
247
|
+
* @covers Issue #635 — dsh was missing entirely
|
|
235
248
|
*/
|
|
236
249
|
const PLATFORM_PROFILES = {
|
|
237
250
|
opencode: {
|
|
238
251
|
skillsDirs: [
|
|
239
|
-
|
|
240
|
-
|
|
252
|
+
['.config', 'opencode', 'skills'],
|
|
253
|
+
['.config', 'opencode'],
|
|
241
254
|
],
|
|
242
255
|
requiredDeps: REQUIRED_DEPS,
|
|
243
256
|
},
|
|
244
257
|
'claude-code': {
|
|
245
258
|
skillsDirs: [
|
|
246
|
-
|
|
247
|
-
|
|
259
|
+
['.claude', 'skills'],
|
|
260
|
+
['.claude'],
|
|
248
261
|
],
|
|
249
262
|
requiredDeps: REQUIRED_DEPS,
|
|
250
263
|
},
|
|
251
264
|
qoder: {
|
|
252
265
|
skillsDirs: [
|
|
253
|
-
|
|
254
|
-
|
|
266
|
+
['.qoder', 'skills'],
|
|
267
|
+
['.qoder'],
|
|
268
|
+
],
|
|
269
|
+
requiredDeps: REQUIRED_DEPS,
|
|
270
|
+
},
|
|
271
|
+
// #635: `xp-gate init` deploys skills to ~/.dsh/skills, the npm package ships
|
|
272
|
+
// plugins/dsh (`@boyingliu01/dsh-plugin-xp-gate`), and
|
|
273
|
+
// scripts/sync-package-content.js lists `dsh` among PLUGINS — but this entry
|
|
274
|
+
// was missing, so getSkillsDirs('dsh') fell through to the opencode paths and
|
|
275
|
+
// DSH skills were never resolved. detectPlatform() still never returns 'dsh'
|
|
276
|
+
// (there is no marker it could key on that opencode's would not also match),
|
|
277
|
+
// so callers that must cover DSH ask for it by name.
|
|
278
|
+
dsh: {
|
|
279
|
+
skillsDirs: [
|
|
280
|
+
['.dsh', 'skills'],
|
|
281
|
+
['.dsh'],
|
|
255
282
|
],
|
|
256
283
|
requiredDeps: REQUIRED_DEPS,
|
|
257
284
|
},
|
|
@@ -262,12 +289,17 @@ const PLATFORM_PROFILES = {
|
|
|
262
289
|
|
|
263
290
|
/**
|
|
264
291
|
* Get the skills directories for a given platform.
|
|
265
|
-
*
|
|
266
|
-
*
|
|
292
|
+
*
|
|
293
|
+
* Resolves the home directory at call time so the result follows
|
|
294
|
+
* process.env.HOME, and so every profile is a plain data entry.
|
|
295
|
+
*
|
|
296
|
+
* @param {string} platform - 'opencode' | 'claude-code' | 'qoder' | 'dsh'
|
|
297
|
+
* @returns {string[]} absolute paths; skillsDirs[0] is the skills directory
|
|
267
298
|
*/
|
|
268
299
|
function getSkillsDirs(platform) {
|
|
269
300
|
const profile = PLATFORM_PROFILES[platform] || PLATFORM_PROFILES.opencode;
|
|
270
|
-
|
|
301
|
+
const home = require('./shared-paths.js').HOME_DIR;
|
|
302
|
+
return profile.skillsDirs.map((segments) => path.join(home, ...segments));
|
|
271
303
|
}
|
|
272
304
|
|
|
273
305
|
/**
|