xtrm-cli 2.1.14 → 2.1.16
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 +32 -0
- package/dist/index.cjs.map +1 -1
- package/package.json +1 -1
- package/src/commands/install.ts +37 -0
- package/test/hooks.test.ts +118 -0
package/package.json
CHANGED
package/src/commands/install.ts
CHANGED
|
@@ -125,6 +125,15 @@ function isDoltInstalled(): boolean {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
function isGitnexusInstalled(): boolean {
|
|
129
|
+
try {
|
|
130
|
+
execSync('gitnexus --version', { stdio: 'ignore' });
|
|
131
|
+
return true;
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
128
137
|
interface GlobalInstallFlags {
|
|
129
138
|
dryRun: boolean;
|
|
130
139
|
yes: boolean;
|
|
@@ -221,6 +230,34 @@ async function runGlobalInstall(
|
|
|
221
230
|
}
|
|
222
231
|
}
|
|
223
232
|
|
|
233
|
+
// Gitnexus global install (for MCP server + CLI tools)
|
|
234
|
+
console.log(t.bold('\n ⚙ gitnexus (code intelligence)'));
|
|
235
|
+
console.log(t.muted(' GitNexus provides knowledge graph queries for impact analysis, execution flows, and symbol context.'));
|
|
236
|
+
|
|
237
|
+
const gitnexusOk = isGitnexusInstalled();
|
|
238
|
+
if (gitnexusOk) {
|
|
239
|
+
console.log(t.success(' ✓ gitnexus already installed\n'));
|
|
240
|
+
} else {
|
|
241
|
+
let doInstallGitnexus = effectiveYes;
|
|
242
|
+
if (!effectiveYes) {
|
|
243
|
+
const { install } = await prompts({
|
|
244
|
+
type: 'confirm',
|
|
245
|
+
name: 'install',
|
|
246
|
+
message: 'Install gitnexus globally? (recommended for MCP server and CLI tools)',
|
|
247
|
+
initial: true,
|
|
248
|
+
});
|
|
249
|
+
doInstallGitnexus = install;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (doInstallGitnexus) {
|
|
253
|
+
console.log(t.muted('\n Installing gitnexus...'));
|
|
254
|
+
spawnSync('npm', ['install', '-g', 'gitnexus'], { stdio: 'inherit' });
|
|
255
|
+
console.log(t.success(' ✓ gitnexus installed\n'));
|
|
256
|
+
} else {
|
|
257
|
+
console.log(t.muted(' ℹ Skipped. Install later with: npm install -g gitnexus\n'));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
224
261
|
const diffTasks = new Listr<DiffCtx>(
|
|
225
262
|
targets.map(target => ({
|
|
226
263
|
title: formatTargetLabel(target),
|
package/test/hooks.test.ts
CHANGED
|
@@ -586,3 +586,121 @@ describe('gitnexus-impact-reminder.py', () => {
|
|
|
586
586
|
expect(r.stdout.trim()).toBe('');
|
|
587
587
|
});
|
|
588
588
|
});
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
// ── beads-gate-core.mjs — decision functions ──────────────────────────────────
|
|
592
|
+
|
|
593
|
+
describe('beads-gate-core.mjs — decision functions', () => {
|
|
594
|
+
const corePath = path.join(HOOKS_DIR, 'beads-gate-core.mjs');
|
|
595
|
+
|
|
596
|
+
it('exports all required decision functions', () => {
|
|
597
|
+
const r = spawnSync('node', ['--input-type=module'], {
|
|
598
|
+
input: `
|
|
599
|
+
import {
|
|
600
|
+
readHookInput,
|
|
601
|
+
resolveSessionContext,
|
|
602
|
+
resolveClaimAndWorkState,
|
|
603
|
+
decideEditGate,
|
|
604
|
+
decideCommitGate,
|
|
605
|
+
decideStopGate,
|
|
606
|
+
} from '${corePath}';
|
|
607
|
+
const ok = [readHookInput, resolveSessionContext, resolveClaimAndWorkState,
|
|
608
|
+
decideEditGate, decideCommitGate, decideStopGate]
|
|
609
|
+
.every(fn => typeof fn === 'function');
|
|
610
|
+
process.exit(ok ? 0 : 1);
|
|
611
|
+
`,
|
|
612
|
+
encoding: 'utf8',
|
|
613
|
+
});
|
|
614
|
+
expect(r.status).toBe(0);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
describe('decideEditGate', () => {
|
|
618
|
+
it('allows when not a beads project', async () => {
|
|
619
|
+
const { decideEditGate } = await import(corePath);
|
|
620
|
+
const decision = decideEditGate(
|
|
621
|
+
{ isBeadsProject: false },
|
|
622
|
+
{ claimed: false, claimId: null, totalWork: 0, inProgress: null }
|
|
623
|
+
);
|
|
624
|
+
expect(decision.allow).toBe(true);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it('allows when session has a claim', async () => {
|
|
628
|
+
const { decideEditGate } = await import(corePath);
|
|
629
|
+
const decision = decideEditGate(
|
|
630
|
+
{ isBeadsProject: true, sessionId: 'test-session' },
|
|
631
|
+
{ claimed: true, claimId: 'issue-123', totalWork: 5, inProgress: null }
|
|
632
|
+
);
|
|
633
|
+
expect(decision.allow).toBe(true);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it('allows when no trackable work exists', async () => {
|
|
637
|
+
const { decideEditGate } = await import(corePath);
|
|
638
|
+
const decision = decideEditGate(
|
|
639
|
+
{ isBeadsProject: true, sessionId: 'test-session' },
|
|
640
|
+
{ claimed: false, claimId: null, totalWork: 0, inProgress: null }
|
|
641
|
+
);
|
|
642
|
+
expect(decision.allow).toBe(true);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it('blocks when no claim but work exists', async () => {
|
|
646
|
+
const { decideEditGate } = await import(corePath);
|
|
647
|
+
const decision = decideEditGate(
|
|
648
|
+
{ isBeadsProject: true, sessionId: 'test-session' },
|
|
649
|
+
{ claimed: false, claimId: null, totalWork: 3, inProgress: null }
|
|
650
|
+
);
|
|
651
|
+
expect(decision.allow).toBe(false);
|
|
652
|
+
expect(decision.reason).toBe('no_claim_with_work');
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('fails open when bd unavailable (state is null)', async () => {
|
|
656
|
+
const { decideEditGate } = await import(corePath);
|
|
657
|
+
const decision = decideEditGate(
|
|
658
|
+
{ isBeadsProject: true, sessionId: 'test-session' },
|
|
659
|
+
null
|
|
660
|
+
);
|
|
661
|
+
expect(decision.allow).toBe(true);
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
describe('decideCommitGate', () => {
|
|
666
|
+
it('allows when no active claim', async () => {
|
|
667
|
+
const { decideCommitGate } = await import(corePath);
|
|
668
|
+
const decision = decideCommitGate(
|
|
669
|
+
{ isBeadsProject: true, sessionId: 'test-session' },
|
|
670
|
+
{ claimed: false, claimId: null, totalWork: 3, inProgress: { count: 1, summary: 'test' } }
|
|
671
|
+
);
|
|
672
|
+
expect(decision.allow).toBe(true);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it('blocks when session has unclosed claim', async () => {
|
|
676
|
+
const { decideCommitGate } = await import(corePath);
|
|
677
|
+
const decision = decideCommitGate(
|
|
678
|
+
{ isBeadsProject: true, sessionId: 'test-session' },
|
|
679
|
+
{ claimed: true, claimId: 'issue-123', totalWork: 3, inProgress: { count: 1, summary: 'test' } }
|
|
680
|
+
);
|
|
681
|
+
expect(decision.allow).toBe(false);
|
|
682
|
+
expect(decision.reason).toBe('unclosed_claim');
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
describe('decideStopGate', () => {
|
|
687
|
+
it('allows when claim is stale (no in_progress issues)', async () => {
|
|
688
|
+
const { decideStopGate } = await import(corePath);
|
|
689
|
+
const decision = decideStopGate(
|
|
690
|
+
{ isBeadsProject: true, sessionId: 'test-session' },
|
|
691
|
+
{ claimed: true, claimId: 'issue-123', totalWork: 3, inProgress: { count: 0, summary: '' } }
|
|
692
|
+
);
|
|
693
|
+
expect(decision.allow).toBe(true);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it('blocks when claim exists and in_progress issues remain', async () => {
|
|
697
|
+
const { decideStopGate } = await import(corePath);
|
|
698
|
+
const decision = decideStopGate(
|
|
699
|
+
{ isBeadsProject: true, sessionId: 'test-session' },
|
|
700
|
+
{ claimed: true, claimId: 'issue-123', totalWork: 3, inProgress: { count: 2, summary: 'test' } }
|
|
701
|
+
);
|
|
702
|
+
expect(decision.allow).toBe(false);
|
|
703
|
+
expect(decision.reason).toBe('unclosed_claim');
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
});
|