xtrm-cli 2.1.11 → 2.1.14
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/dist/index.cjs +24 -1
- package/dist/index.cjs.map +1 -1
- package/package.json +1 -1
- package/src/commands/install-pi.ts +23 -1
- package/src/commands/install-project.ts +1 -0
- package/test/hooks.test.ts +96 -0
- package/test/install-pi.test.ts +32 -0
- package/cli/src/commands/install-pi.ts +0 -5
package/package.json
CHANGED
|
@@ -18,6 +18,23 @@ export function fillTemplate(template: string, values: Record<string, string>):
|
|
|
18
18
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => values[key] ?? '');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
|
|
22
|
+
export function readExistingPiValues(piAgentDir: string): Record<string, string> {
|
|
23
|
+
const values: Record<string, string> = {};
|
|
24
|
+
try {
|
|
25
|
+
const auth = JSON.parse(require('fs').readFileSync(path.join(piAgentDir, 'auth.json'), 'utf8'));
|
|
26
|
+
if (auth?.dashscope?.key) values['DASHSCOPE_API_KEY'] = auth.dashscope.key;
|
|
27
|
+
if (auth?.zai?.key) values['ZAI_API_KEY'] = auth.zai.key;
|
|
28
|
+
} catch { /* file doesn't exist or invalid */ }
|
|
29
|
+
try {
|
|
30
|
+
const models = JSON.parse(require('fs').readFileSync(path.join(piAgentDir, 'models.json'), 'utf8'));
|
|
31
|
+
if (!values['DASHSCOPE_API_KEY'] && models?.providers?.dashscope?.apiKey) {
|
|
32
|
+
values['DASHSCOPE_API_KEY'] = models.providers.dashscope.apiKey;
|
|
33
|
+
}
|
|
34
|
+
} catch { /* file doesn't exist or invalid */ }
|
|
35
|
+
return values;
|
|
36
|
+
}
|
|
37
|
+
|
|
21
38
|
function isPiInstalled(): boolean {
|
|
22
39
|
return spawnSync('pi', ['--version'], { encoding: 'utf8' }).status === 0;
|
|
23
40
|
}
|
|
@@ -48,10 +65,15 @@ export function createInstallPiCommand(): Command {
|
|
|
48
65
|
}
|
|
49
66
|
|
|
50
67
|
const schema: InstallSchema = await fs.readJson(path.join(piConfigDir, 'install-schema.json'));
|
|
51
|
-
const
|
|
68
|
+
const existing = readExistingPiValues(PI_AGENT_DIR);
|
|
69
|
+
const values: Record<string, string> = { ...existing };
|
|
52
70
|
|
|
53
71
|
console.log(t.bold(' API Keys\n'));
|
|
54
72
|
for (const field of schema.fields) {
|
|
73
|
+
if (existing[field.key]) {
|
|
74
|
+
console.log(t.success(` ${sym.ok} ${field.label} [already set]`));
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
55
77
|
if (!field.required && !yes) {
|
|
56
78
|
const { include } = await prompts({ type: 'confirm', name: 'include', message: ` Configure ${field.label}? (optional)`, initial: false });
|
|
57
79
|
if (!include) continue;
|
|
@@ -459,6 +459,7 @@ async function bootstrapProjectInit(): Promise<void> {
|
|
|
459
459
|
|
|
460
460
|
await runBdInitForProject(projectRoot);
|
|
461
461
|
await runGitNexusInitForProject(projectRoot);
|
|
462
|
+
await syncProjectMcpServers(projectRoot);
|
|
462
463
|
}
|
|
463
464
|
|
|
464
465
|
async function runBdInitForProject(projectRoot: string): Promise<void> {
|
package/test/hooks.test.ts
CHANGED
|
@@ -91,6 +91,7 @@ describe('main-guard.mjs — MAIN_GUARD_PROTECTED_BRANCHES', () => {
|
|
|
91
91
|
'git pull',
|
|
92
92
|
'gh pr list',
|
|
93
93
|
'bd list',
|
|
94
|
+
`git reset --hard origin/${CURRENT_BRANCH}`,
|
|
94
95
|
];
|
|
95
96
|
for (const command of safeCommands) {
|
|
96
97
|
const r = runHook(
|
|
@@ -139,6 +140,23 @@ describe('main-guard.mjs — MAIN_GUARD_PROTECTED_BRANCHES', () => {
|
|
|
139
140
|
const out = parseHookJson(r.stdout);
|
|
140
141
|
expect(out?.systemMessage).toContain('feature branch');
|
|
141
142
|
});
|
|
143
|
+
|
|
144
|
+
it('post-push hook sync guidance uses reset --hard, consistent with main-guard', () => {
|
|
145
|
+
const postPush = readFileSync(path.join(HOOKS_DIR, 'main-guard-post-push.mjs'), 'utf8');
|
|
146
|
+
expect(postPush).toContain('reset --hard');
|
|
147
|
+
expect(postPush).not.toContain('pull --ff-only');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('hooks.json wires Bash to main-guard so git commit protection fires', () => {
|
|
151
|
+
const hooksJson = JSON.parse(readFileSync(path.join(__dirname, '../../config/hooks.json'), 'utf8'));
|
|
152
|
+
const mainGuardEntries = hooksJson.hooks.PreToolUse.filter(
|
|
153
|
+
(h: { script: string }) => h.script === 'main-guard.mjs',
|
|
154
|
+
);
|
|
155
|
+
const matchers: string[] = mainGuardEntries.map((h: { matcher: string }) => h.matcher ?? '');
|
|
156
|
+
const coversBash = matchers.some((m: string) => m.split('|').includes('Bash'));
|
|
157
|
+
expect(coversBash, 'main-guard.mjs must have a PreToolUse entry with Bash in its matcher').toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
142
160
|
});
|
|
143
161
|
|
|
144
162
|
// ── main-guard-post-push.mjs ────────────────────────────────────────────────
|
|
@@ -358,6 +376,84 @@ exit 1
|
|
|
358
376
|
});
|
|
359
377
|
|
|
360
378
|
|
|
379
|
+
describe('beads-memory-gate.mjs', () => {
|
|
380
|
+
it('fails open (exit 0) when no .beads directory exists', () => {
|
|
381
|
+
const r = runHook('beads-memory-gate.mjs', { session_id: 'test', cwd: '/tmp' });
|
|
382
|
+
expect(r.status).toBe(0);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('allows stop (exit 0) when marker file exists', () => {
|
|
386
|
+
const projectDir = mkdtempSync(path.join(os.tmpdir(), 'xtrm-memgate-'));
|
|
387
|
+
mkdirSync(path.join(projectDir, '.beads'));
|
|
388
|
+
writeFileSync(path.join(projectDir, '.beads', '.memory-gate-done'), '');
|
|
389
|
+
try {
|
|
390
|
+
const r = runHook('beads-memory-gate.mjs', { session_id: 'test', cwd: projectDir });
|
|
391
|
+
expect(r.status).toBe(0);
|
|
392
|
+
} finally {
|
|
393
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('allows stop (exit 0) when no closed issues exist', () => {
|
|
398
|
+
const projectDir = mkdtempSync(path.join(os.tmpdir(), 'xtrm-memgate-'));
|
|
399
|
+
mkdirSync(path.join(projectDir, '.beads'));
|
|
400
|
+
const fake = withFakeBdDir(`#!/usr/bin/env bash
|
|
401
|
+
set -euo pipefail
|
|
402
|
+
if [[ "$1" == "list" ]]; then
|
|
403
|
+
cat <<'EOF'
|
|
404
|
+
|
|
405
|
+
--------------------------------------------------------------------------------
|
|
406
|
+
Total: 0 issues (0 open, 0 in progress)
|
|
407
|
+
EOF
|
|
408
|
+
exit 0
|
|
409
|
+
fi
|
|
410
|
+
exit 1
|
|
411
|
+
`);
|
|
412
|
+
try {
|
|
413
|
+
const r = runHook(
|
|
414
|
+
'beads-memory-gate.mjs',
|
|
415
|
+
{ session_id: 'test', cwd: projectDir },
|
|
416
|
+
{ PATH: `${fake.tempDir}:${process.env.PATH ?? ''}` },
|
|
417
|
+
);
|
|
418
|
+
expect(r.status).toBe(0);
|
|
419
|
+
} finally {
|
|
420
|
+
rmSync(fake.tempDir, { recursive: true, force: true });
|
|
421
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('blocks stop (exit 2) when closed issues exist and no marker', () => {
|
|
426
|
+
const projectDir = mkdtempSync(path.join(os.tmpdir(), 'xtrm-memgate-'));
|
|
427
|
+
mkdirSync(path.join(projectDir, '.beads'));
|
|
428
|
+
const fake = withFakeBdDir(`#!/usr/bin/env bash
|
|
429
|
+
set -euo pipefail
|
|
430
|
+
if [[ "$1" == "list" ]]; then
|
|
431
|
+
cat <<'EOF'
|
|
432
|
+
✓ issue-abc P2 Fix the thing
|
|
433
|
+
|
|
434
|
+
--------------------------------------------------------------------------------
|
|
435
|
+
Total: 1 issues (0 open, 0 in progress, 1 closed)
|
|
436
|
+
EOF
|
|
437
|
+
exit 0
|
|
438
|
+
fi
|
|
439
|
+
exit 1
|
|
440
|
+
`);
|
|
441
|
+
try {
|
|
442
|
+
const r = runHook(
|
|
443
|
+
'beads-memory-gate.mjs',
|
|
444
|
+
{ session_id: 'test', cwd: projectDir },
|
|
445
|
+
{ PATH: `${fake.tempDir}:${process.env.PATH ?? ''}` },
|
|
446
|
+
);
|
|
447
|
+
expect(r.status).toBe(2);
|
|
448
|
+
expect(r.stderr).toContain('MEMORY GATE');
|
|
449
|
+
} finally {
|
|
450
|
+
rmSync(fake.tempDir, { recursive: true, force: true });
|
|
451
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
|
|
361
457
|
// ── tdd-guard-pretool-bridge.cjs ─────────────────────────────────────────────
|
|
362
458
|
|
|
363
459
|
const TDD_BRIDGE_DIR = path.join(__dirname, '../../project-skills/tdd-guard/.claude/hooks');
|
package/test/install-pi.test.ts
CHANGED
|
@@ -84,4 +84,36 @@ describe('createInstallPiCommand', () => {
|
|
|
84
84
|
expect(fs.existsSync(p.join(base, 'index.ts'))).toBe(true);
|
|
85
85
|
expect(fs.existsSync(p.join(base, 'package.json'))).toBe(true);
|
|
86
86
|
});
|
|
87
|
+
|
|
88
|
+
it('readExistingPiValues extracts DASHSCOPE_API_KEY from existing auth.json', async () => {
|
|
89
|
+
const { readExistingPiValues } = await import('../src/commands/install-pi.js?t=' + Date.now());
|
|
90
|
+
const tmpDir = require('node:fs').mkdtempSync(require('node:path').join(require('node:os').tmpdir(), 'pi-test-'));
|
|
91
|
+
require('node:fs').writeFileSync(require('node:path').join(tmpDir, 'auth.json'), JSON.stringify({ dashscope: { type: 'api_key', key: 'sk-existing-123' } }));
|
|
92
|
+
const result = readExistingPiValues(tmpDir);
|
|
93
|
+
require('node:fs').rmSync(tmpDir, { recursive: true });
|
|
94
|
+
expect(result['DASHSCOPE_API_KEY']).toBe('sk-existing-123');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('readExistingPiValues extracts ZAI_API_KEY from existing auth.json', async () => {
|
|
98
|
+
const { readExistingPiValues } = await import('../src/commands/install-pi.js?t=' + Date.now());
|
|
99
|
+
const tmpDir = require('node:fs').mkdtempSync(require('node:path').join(require('node:os').tmpdir(), 'pi-test-'));
|
|
100
|
+
require('node:fs').writeFileSync(require('node:path').join(tmpDir, 'auth.json'), JSON.stringify({ zai: { type: 'api_key', key: 'zai-existing-456' } }));
|
|
101
|
+
const result = readExistingPiValues(tmpDir);
|
|
102
|
+
require('node:fs').rmSync(tmpDir, { recursive: true });
|
|
103
|
+
expect(result['ZAI_API_KEY']).toBe('zai-existing-456');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('readExistingPiValues returns empty object when auth.json missing', async () => {
|
|
107
|
+
const { readExistingPiValues } = await import('../src/commands/install-pi.js?t=' + Date.now());
|
|
108
|
+
expect(readExistingPiValues('/nonexistent/path')).toEqual({});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('readExistingPiValues extracts DASHSCOPE_API_KEY from models.json when auth.json missing', async () => {
|
|
112
|
+
const { readExistingPiValues } = await import('../src/commands/install-pi.js?t=models' + Date.now());
|
|
113
|
+
const tmpDir = require('node:fs').mkdtempSync(require('node:path').join(require('node:os').tmpdir(), 'pi-test-'));
|
|
114
|
+
require('node:fs').writeFileSync(require('node:path').join(tmpDir, 'models.json'), JSON.stringify({ providers: { dashscope: { apiKey: 'sk-from-models-789' } } }));
|
|
115
|
+
const result = readExistingPiValues(tmpDir);
|
|
116
|
+
require('node:fs').rmSync(tmpDir, { recursive: true });
|
|
117
|
+
expect(result['DASHSCOPE_API_KEY']).toBe('sk-from-models-789');
|
|
118
|
+
});
|
|
87
119
|
});
|