@laitszkin/apollo-toolkit 4.0.11 → 4.1.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.
Files changed (145) hide show
  1. package/AGENTS.md +37 -27
  2. package/CHANGELOG.md +47 -0
  3. package/CLAUDE.md +37 -27
  4. package/README.md +15 -2
  5. package/assets/spec/rg13-1780435029246/test-questions.json +1 -0
  6. package/assets/spec/rg13-1780468345132/test-questions.json +1 -0
  7. package/package.json +3 -3
  8. package/packages/cli/dist/tool-registration.js +1 -0
  9. package/packages/cli/dist/tsconfig.tsbuildinfo +1 -1
  10. package/packages/cli/tool-registration.ts +1 -0
  11. package/packages/tools/architecture/dist/index.js +549 -2
  12. package/packages/tools/architecture/dist/index.test.d.ts +1 -0
  13. package/packages/tools/architecture/dist/index.test.js +229 -0
  14. package/packages/tools/architecture/dist/tsconfig.tsbuildinfo +1 -1
  15. package/packages/tools/architecture/index.test.ts +329 -0
  16. package/packages/tools/architecture/index.ts +613 -5
  17. package/packages/tools/codegraph/dist/index.d.ts +3 -0
  18. package/packages/tools/codegraph/dist/index.js +343 -0
  19. package/packages/tools/codegraph/dist/lib/cg-instance.d.ts +29 -0
  20. package/packages/tools/codegraph/dist/lib/cg-instance.js +59 -0
  21. package/packages/tools/codegraph/dist/lib/cg-instance.test.d.ts +1 -0
  22. package/packages/tools/codegraph/dist/lib/cg-instance.test.js +27 -0
  23. package/packages/tools/codegraph/dist/lib/cmd-explore.d.ts +5 -0
  24. package/packages/tools/codegraph/dist/lib/cmd-explore.js +95 -0
  25. package/packages/tools/codegraph/dist/lib/cmd-explore.test.d.ts +1 -0
  26. package/packages/tools/codegraph/dist/lib/cmd-explore.test.js +133 -0
  27. package/packages/tools/codegraph/dist/lib/cmd-flag-splice.test.d.ts +1 -0
  28. package/packages/tools/codegraph/dist/lib/cmd-flag-splice.test.js +83 -0
  29. package/packages/tools/codegraph/dist/lib/cmd-init.d.ts +5 -0
  30. package/packages/tools/codegraph/dist/lib/cmd-init.js +50 -0
  31. package/packages/tools/codegraph/dist/lib/cmd-init.test.d.ts +1 -0
  32. package/packages/tools/codegraph/dist/lib/cmd-init.test.js +51 -0
  33. package/packages/tools/codegraph/dist/lib/cmd-list-apis.d.ts +5 -0
  34. package/packages/tools/codegraph/dist/lib/cmd-list-apis.js +64 -0
  35. package/packages/tools/codegraph/dist/lib/cmd-list-apis.test.d.ts +1 -0
  36. package/packages/tools/codegraph/dist/lib/cmd-list-apis.test.js +69 -0
  37. package/packages/tools/codegraph/dist/lib/cmd-search.d.ts +5 -0
  38. package/packages/tools/codegraph/dist/lib/cmd-search.js +21 -0
  39. package/packages/tools/codegraph/dist/lib/cmd-search.test.d.ts +1 -0
  40. package/packages/tools/codegraph/dist/lib/cmd-search.test.js +30 -0
  41. package/packages/tools/codegraph/dist/lib/cmd-status.d.ts +4 -0
  42. package/packages/tools/codegraph/dist/lib/cmd-status.js +44 -0
  43. package/packages/tools/codegraph/dist/lib/cmd-status.test.d.ts +1 -0
  44. package/packages/tools/codegraph/dist/lib/cmd-status.test.js +72 -0
  45. package/packages/tools/codegraph/dist/lib/cmd-survey.d.ts +36 -0
  46. package/packages/tools/codegraph/dist/lib/cmd-survey.js +142 -0
  47. package/packages/tools/codegraph/dist/lib/cmd-survey.test.d.ts +1 -0
  48. package/packages/tools/codegraph/dist/lib/cmd-survey.test.js +136 -0
  49. package/packages/tools/codegraph/dist/lib/cmd-sync.d.ts +4 -0
  50. package/packages/tools/codegraph/dist/lib/cmd-sync.js +51 -0
  51. package/packages/tools/codegraph/dist/lib/cmd-sync.test.d.ts +1 -0
  52. package/packages/tools/codegraph/dist/lib/cmd-sync.test.js +30 -0
  53. package/packages/tools/codegraph/dist/lib/cmd-verify.d.ts +4 -0
  54. package/packages/tools/codegraph/dist/lib/cmd-verify.js +134 -0
  55. package/packages/tools/codegraph/dist/lib/cmd-verify.test.d.ts +1 -0
  56. package/packages/tools/codegraph/dist/lib/cmd-verify.test.js +139 -0
  57. package/packages/tools/codegraph/dist/lib/formatter.d.ts +67 -0
  58. package/packages/tools/codegraph/dist/lib/formatter.js +107 -0
  59. package/packages/tools/codegraph/dist/lib/formatter.test.d.ts +1 -0
  60. package/packages/tools/codegraph/dist/lib/formatter.test.js +41 -0
  61. package/packages/tools/codegraph/dist/lib/survey/grouper.d.ts +19 -0
  62. package/packages/tools/codegraph/dist/lib/survey/grouper.js +194 -0
  63. package/packages/tools/codegraph/dist/lib/survey/grouper.test.d.ts +1 -0
  64. package/packages/tools/codegraph/dist/lib/survey/grouper.test.js +62 -0
  65. package/packages/tools/codegraph/dist/lib/survey/scanner.d.ts +31 -0
  66. package/packages/tools/codegraph/dist/lib/survey/scanner.js +50 -0
  67. package/packages/tools/codegraph/dist/lib/verify/checker.d.ts +32 -0
  68. package/packages/tools/codegraph/dist/lib/verify/checker.js +146 -0
  69. package/packages/tools/codegraph/dist/lib/verify/checker.test.d.ts +1 -0
  70. package/packages/tools/codegraph/dist/lib/verify/checker.test.js +128 -0
  71. package/packages/tools/codegraph/dist/tsconfig.tsbuildinfo +1 -0
  72. package/packages/tools/codegraph/env.d.ts +56 -0
  73. package/packages/tools/codegraph/index.ts +362 -0
  74. package/packages/tools/codegraph/lib/cg-instance.test.ts +36 -0
  75. package/packages/tools/codegraph/lib/cg-instance.ts +66 -0
  76. package/packages/tools/codegraph/lib/cmd-explore.test.ts +195 -0
  77. package/packages/tools/codegraph/lib/cmd-explore.ts +129 -0
  78. package/packages/tools/codegraph/lib/cmd-flag-splice.test.ts +94 -0
  79. package/packages/tools/codegraph/lib/cmd-init.test.ts +68 -0
  80. package/packages/tools/codegraph/lib/cmd-init.ts +60 -0
  81. package/packages/tools/codegraph/lib/cmd-list-apis.test.ts +80 -0
  82. package/packages/tools/codegraph/lib/cmd-list-apis.ts +90 -0
  83. package/packages/tools/codegraph/lib/cmd-search.test.ts +37 -0
  84. package/packages/tools/codegraph/lib/cmd-search.ts +32 -0
  85. package/packages/tools/codegraph/lib/cmd-status.test.ts +86 -0
  86. package/packages/tools/codegraph/lib/cmd-status.ts +53 -0
  87. package/packages/tools/codegraph/lib/cmd-survey.test.ts +161 -0
  88. package/packages/tools/codegraph/lib/cmd-survey.ts +199 -0
  89. package/packages/tools/codegraph/lib/cmd-sync.test.ts +41 -0
  90. package/packages/tools/codegraph/lib/cmd-sync.ts +62 -0
  91. package/packages/tools/codegraph/lib/cmd-verify.test.ts +162 -0
  92. package/packages/tools/codegraph/lib/cmd-verify.ts +145 -0
  93. package/packages/tools/codegraph/lib/formatter.test.ts +47 -0
  94. package/packages/tools/codegraph/lib/formatter.ts +130 -0
  95. package/packages/tools/codegraph/lib/survey/grouper.test.ts +72 -0
  96. package/packages/tools/codegraph/lib/survey/grouper.ts +226 -0
  97. package/packages/tools/codegraph/lib/survey/scanner.ts +89 -0
  98. package/packages/tools/codegraph/lib/verify/checker.test.ts +140 -0
  99. package/packages/tools/codegraph/lib/verify/checker.ts +172 -0
  100. package/packages/tools/codegraph/package.json +23 -0
  101. package/packages/tools/codegraph/tsconfig.json +22 -0
  102. package/resources/project-architecture/atlas/atlas.history.log +32 -0
  103. package/resources/project-architecture/atlas/atlas.history.undo.json +356 -28
  104. package/resources/project-architecture/atlas/atlas.history.undo.stack.json +14350 -0
  105. package/resources/project-architecture/atlas/atlas.index.yaml +76 -12
  106. package/resources/project-architecture/atlas/features/codegraph.yaml +95 -0
  107. package/resources/project-architecture/atlas/features/eval-ci-gate.yaml +6 -1
  108. package/resources/project-architecture/atlas/features/eval-cli.yaml +16 -1
  109. package/resources/project-architecture/atlas/features/eval-executor.yaml +12 -2
  110. package/resources/project-architecture/atlas/features/eval-isolation.yaml +6 -1
  111. package/resources/project-architecture/atlas/features/eval-optimizer.yaml +17 -2
  112. package/resources/project-architecture/atlas/features/eval-question.yaml +12 -2
  113. package/resources/project-architecture/atlas/features/eval-reporter.yaml +6 -1
  114. package/resources/project-architecture/atlas/features/eval-scorer.yaml +12 -2
  115. package/resources/project-architecture/features/codegraph/cg-discovery.html +47 -0
  116. package/resources/project-architecture/features/codegraph/cg-lifecycle.html +48 -0
  117. package/resources/project-architecture/features/codegraph/cg-validation.html +47 -0
  118. package/resources/project-architecture/features/codegraph/index.html +58 -0
  119. package/resources/project-architecture/features/eval-ci-gate/workflow-trigger.html +6 -1
  120. package/resources/project-architecture/features/eval-cli/cli-handler.html +8 -1
  121. package/resources/project-architecture/features/eval-executor/exec-api-client.html +6 -1
  122. package/resources/project-architecture/features/eval-executor/trace-recorder.html +6 -1
  123. package/resources/project-architecture/features/eval-isolation/tool-dispatcher.html +6 -1
  124. package/resources/project-architecture/features/eval-optimizer/dedup-engine.html +6 -1
  125. package/resources/project-architecture/features/eval-optimizer/issue-extractor.html +7 -1
  126. package/resources/project-architecture/features/eval-question/question-loader.html +6 -1
  127. package/resources/project-architecture/features/eval-question/variant-generator.html +6 -1
  128. package/resources/project-architecture/features/eval-reporter/report-composer.html +6 -1
  129. package/resources/project-architecture/features/eval-scorer/judge-api-client.html +6 -1
  130. package/resources/project-architecture/features/eval-scorer/judge-prompt-builder.html +6 -1
  131. package/resources/project-architecture/index.html +200 -94
  132. package/scripts/test.sh +39 -0
  133. package/skills/design/SKILL.md +33 -0
  134. package/skills/init-project-html/SKILL.md +66 -56
  135. package/skills/init-project-html/lib/atlas/assets/architecture.css +2 -1
  136. package/skills/init-project-html/lib/atlas/render.js +11 -1
  137. package/skills/init-project-html/lib/atlas/schema.js +44 -7
  138. package/skills/init-project-html/references/TEMPLATE_SPEC.md +20 -0
  139. package/skills/init-project-html/references/architecture.md +35 -35
  140. package/skills/init-project-html/references/definition.md +12 -17
  141. package/skills/update-project-html/README.md +16 -27
  142. package/skills/update-project-html/SKILL.md +54 -41
  143. package/skills/update-project-html/references/architecture.md +35 -35
  144. package/skills/update-project-html/references/definition.md +12 -17
  145. package/tsconfig.json +1 -0
@@ -0,0 +1,133 @@
1
+ import { describe, it, mock, before } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createRequire } from 'node:module';
4
+ const mockSearchResults = [];
5
+ // ---------------------------------------------------------------------------
6
+ // Pre-load @colbymchenry/codegraph so we can mock its methods before
7
+ // cmd-explore.js performs its own require().
8
+ // ---------------------------------------------------------------------------
9
+ const require = createRequire(import.meta.url);
10
+ const { CodeGraph } = require('@colbymchenry/codegraph');
11
+ let mockInstance;
12
+ before(() => {
13
+ mockInstance = {
14
+ searchNodes: () => mockSearchResults.map(r => ({ node: r.node, score: r.score })),
15
+ getCallers: () => [],
16
+ getCallees: () => [],
17
+ getCode: async () => null,
18
+ close: () => { },
19
+ };
20
+ mock.method(CodeGraph, 'open', async () => mockInstance);
21
+ });
22
+ // ---------------------------------------------------------------------------
23
+ // Import the module under test (same cached CodeGraph is used internally)
24
+ // ---------------------------------------------------------------------------
25
+ let handleExplore;
26
+ before(async () => {
27
+ const mod = await import('./cmd-explore.js');
28
+ handleExplore = mod.handleExplore;
29
+ });
30
+ // =========================================================================
31
+ // REGTEST-7: Explore output should group symbols by file
32
+ // =========================================================================
33
+ describe('REGTEST-7: Explore grouping by file', () => {
34
+ it('should group symbols under a single file header', async () => {
35
+ // Arrange: two symbols in the same file
36
+ mockSearchResults.length = 0;
37
+ mockSearchResults.push({
38
+ node: {
39
+ id: '1',
40
+ name: 'addUser',
41
+ kind: 'function',
42
+ filePath: 'src/utils.ts',
43
+ startLine: 10,
44
+ endLine: 25,
45
+ qualifiedName: 'utils.addUser',
46
+ signature: '(name: string, age: number): User',
47
+ },
48
+ score: 0.95,
49
+ }, {
50
+ node: {
51
+ id: '2',
52
+ name: 'deleteUser',
53
+ kind: 'function',
54
+ filePath: 'src/utils.ts',
55
+ startLine: 30,
56
+ endLine: 40,
57
+ qualifiedName: 'utils.deleteUser',
58
+ signature: '(id: number): void',
59
+ },
60
+ score: 0.85,
61
+ });
62
+ // Capture stdout
63
+ const stdoutChunks = [];
64
+ const origWrite = process.stdout.write;
65
+ process.stdout.write = ((chunk) => {
66
+ stdoutChunks.push(String(chunk));
67
+ return true;
68
+ });
69
+ try {
70
+ // Act
71
+ const exitCode = await handleExplore('/fake/project', 'utils', {});
72
+ // Assert
73
+ assert.strictEqual(exitCode, 0);
74
+ const output = stdoutChunks.join('');
75
+ // Exactly one file header for the shared filePath
76
+ const headerMatches = output.match(/=== src\/utils\.ts ===/g);
77
+ assert.strictEqual(headerMatches?.length, 1, 'Expected exactly one file header for src/utils.ts, ' +
78
+ `got ${headerMatches?.length ?? 0}`);
79
+ // Both symbols appear in the output
80
+ assert.ok(output.includes('addUser'), 'Output should contain addUser');
81
+ assert.ok(output.includes('deleteUser'), 'Output should contain deleteUser');
82
+ // File header precedes both symbols (not duplicated)
83
+ const headerIdx = output.indexOf('=== src/utils.ts ===');
84
+ assert.ok(headerIdx < output.indexOf('addUser'), 'File header should appear before addUser');
85
+ assert.ok(headerIdx < output.indexOf('deleteUser'), 'File header should appear before deleteUser');
86
+ }
87
+ finally {
88
+ process.stdout.write = origWrite;
89
+ }
90
+ });
91
+ });
92
+ // =========================================================================
93
+ // REGTEST-8: Explore --feature acceptance
94
+ // =========================================================================
95
+ describe('REGTEST-8: Explore --feature acceptance', () => {
96
+ it('should accept feature parameter without error', async () => {
97
+ // Arrange: at least one result so the feature line is emitted
98
+ mockSearchResults.length = 0;
99
+ mockSearchResults.push({
100
+ node: {
101
+ id: '3',
102
+ name: 'authLogin',
103
+ kind: 'function',
104
+ filePath: 'src/auth.ts',
105
+ startLine: 5,
106
+ endLine: 20,
107
+ qualifiedName: 'auth.login',
108
+ signature: '(credentials: Record<string, unknown>): Session',
109
+ },
110
+ score: 0.9,
111
+ });
112
+ const stdoutChunks = [];
113
+ const origWrite = process.stdout.write;
114
+ process.stdout.write = ((chunk) => {
115
+ stdoutChunks.push(String(chunk));
116
+ return true;
117
+ });
118
+ try {
119
+ // Act — pass feature without json, expect no error
120
+ const exitCode = await handleExplore('/fake/project', 'authLogin', {
121
+ feature: 'auth',
122
+ json: false,
123
+ });
124
+ // Assert
125
+ assert.strictEqual(exitCode, 0, 'Should return exit code 0');
126
+ const output = stdoutChunks.join('');
127
+ assert.ok(output.includes('Feature: auth'), 'Output should include "Feature: auth" header');
128
+ }
129
+ finally {
130
+ process.stdout.write = origWrite;
131
+ }
132
+ });
133
+ });
@@ -0,0 +1,83 @@
1
+ import { describe, it, mock } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createRequire } from 'node:module';
4
+ const require = createRequire(import.meta.url);
5
+ describe('REGTEST-R2-01: handleListApis --all flag splice', () => {
6
+ it('should include non-exported symbols with all=true and filter them with all=false', async (ctx) => {
7
+ // Arrange: three nodes — two exported, one non-exported
8
+ const nodes = [
9
+ {
10
+ id: 'n1',
11
+ name: 'funcA',
12
+ kind: 'function',
13
+ filePath: 'src/feature/a.ts',
14
+ startLine: 10,
15
+ endLine: 30,
16
+ qualifiedName: 'funcA',
17
+ signature: '(x: string): void',
18
+ isExported: true,
19
+ },
20
+ {
21
+ id: 'n2',
22
+ name: 'funcB',
23
+ kind: 'function',
24
+ filePath: 'src/lib/b.ts',
25
+ startLine: 5,
26
+ endLine: 25,
27
+ qualifiedName: 'funcB',
28
+ signature: '(y: number): string',
29
+ isExported: false,
30
+ },
31
+ {
32
+ id: 'n3',
33
+ name: 'funcC',
34
+ kind: 'function',
35
+ filePath: 'src/lib/c.ts',
36
+ startLine: 1,
37
+ endLine: 20,
38
+ qualifiedName: 'funcC',
39
+ signature: '(z: boolean): void',
40
+ isExported: true,
41
+ },
42
+ ];
43
+ // Mock CodeGraph.open before importing the module under test
44
+ const { CodeGraph } = require('@colbymchenry/codegraph');
45
+ const openMock = mock.method(CodeGraph, 'open', async () => ({
46
+ getNodesByKind: (_kind) => nodes,
47
+ getCallers: (_id) => [],
48
+ close: () => { },
49
+ }));
50
+ const { handleListApis } = await import('./cmd-list-apis.js');
51
+ // Test 1: all=true — all symbols appear, grouped by directory
52
+ {
53
+ const chunks = [];
54
+ ctx.mock.method(process.stdout, 'write', (chunk) => {
55
+ chunks.push(String(chunk));
56
+ });
57
+ await handleListApis('/fake/root', undefined, { all: true });
58
+ const output = chunks.join('');
59
+ assert.ok(output.includes('funcA'), 'all=true: should include exported funcA');
60
+ assert.ok(output.includes('funcB'), 'all=true: should include non-exported funcB');
61
+ assert.ok(output.includes('funcC'), 'all=true: should include exported funcC');
62
+ assert.ok(output.includes('=== src/feature/ ==='), 'all=true: should group src/feature/');
63
+ assert.ok(output.includes('=== src/lib/ ==='), 'all=true: should group src/lib/');
64
+ ctx.mock.reset();
65
+ }
66
+ // Test 2: all=false — only exported symbols, ungrouped
67
+ {
68
+ const chunks = [];
69
+ ctx.mock.method(process.stdout, 'write', (chunk) => {
70
+ chunks.push(String(chunk));
71
+ });
72
+ await handleListApis('/fake/root', undefined, { all: false });
73
+ const output = chunks.join('');
74
+ assert.ok(output.includes('funcA'), 'all=false: should include exported funcA');
75
+ assert.ok(!output.includes('funcB'), 'all=false: should NOT include non-exported funcB');
76
+ assert.ok(output.includes('funcC'), 'all=false: should include exported funcC');
77
+ assert.ok(!output.includes('=== src/feature/ ==='), 'all=false: should not group');
78
+ assert.ok(!output.includes('=== src/lib/ ==='), 'all=false: should not group');
79
+ }
80
+ // Clean up global mocks (CodeGraph.open)
81
+ mock.reset();
82
+ });
83
+ });
@@ -0,0 +1,5 @@
1
+ export interface InitOptions {
2
+ index?: boolean;
3
+ json?: boolean;
4
+ }
5
+ export declare function handleInit(projectRoot: string, options?: InitOptions): Promise<number>;
@@ -0,0 +1,50 @@
1
+ import { createOrOpenIndex, closeIndex } from './cg-instance.js';
2
+ import { formatSummary, formatOutput } from './formatter.js';
3
+ export async function handleInit(projectRoot, options = {}) {
4
+ const progressEvents = [];
5
+ const start = Date.now();
6
+ const cg = await createOrOpenIndex(projectRoot, {
7
+ index: options.index,
8
+ onProgress: (p) => {
9
+ progressEvents.push({ phase: p.phase, current: p.current, total: p.total });
10
+ if (process.stdout.isTTY) {
11
+ process.stdout.write(`\r Indexing: ${p.phase} ${p.current}/${p.total}${p.currentFile ? ` ${p.currentFile}` : ''}`);
12
+ }
13
+ },
14
+ });
15
+ if (options.index && process.stdout.isTTY) {
16
+ process.stdout.write('\n');
17
+ }
18
+ const stats = cg.getStats();
19
+ closeIndex(cg);
20
+ const durationMs = Date.now() - start;
21
+ const result = {
22
+ projectRoot,
23
+ initialized: true,
24
+ indexed: !!options.index,
25
+ durationMs,
26
+ stats: {
27
+ fileCount: stats.fileCount,
28
+ nodeCount: stats.nodeCount,
29
+ edgeCount: stats.edgeCount,
30
+ },
31
+ progress: progressEvents.length > 0 ? progressEvents : undefined,
32
+ };
33
+ if (options.json) {
34
+ process.stdout.write(formatOutput(result, { json: true }) + '\n');
35
+ }
36
+ else {
37
+ const summary = [
38
+ ['Project:', projectRoot],
39
+ ['Status:', 'Initialized'],
40
+ ];
41
+ if (options.index) {
42
+ summary.push(['Files:', stats.fileCount]);
43
+ summary.push(['Nodes:', stats.nodeCount]);
44
+ summary.push(['Edges:', stats.edgeCount]);
45
+ summary.push(['Duration:', `${durationMs}ms`]);
46
+ }
47
+ process.stdout.write(formatSummary(summary) + '\n');
48
+ }
49
+ return 0;
50
+ }
@@ -0,0 +1,51 @@
1
+ import { describe, it, mock } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ // Mock cg-instance before importing cmd-init so the mock takes effect
4
+ // before module resolution caches anything.
5
+ mock.module('./cg-instance.js', {
6
+ namedExports: {
7
+ createOrOpenIndex: async (_projectRoot, _options) => ({
8
+ getStats: () => ({ fileCount: 42, nodeCount: 1280, edgeCount: 5600 }),
9
+ close: () => { },
10
+ }),
11
+ closeIndex: () => { },
12
+ },
13
+ });
14
+ describe('handleInit', () => {
15
+ it('should include Duration: in TTY summary output when --index is used', async () => {
16
+ const writes = [];
17
+ const origWrite = process.stdout.write.bind(process.stdout);
18
+ process.stdout.write = (chunk, ..._rest) => {
19
+ writes.push(String(chunk));
20
+ return true;
21
+ };
22
+ try {
23
+ const { handleInit } = await import('./cmd-init.js');
24
+ const code = await handleInit('/tmp/test-project', { index: true, json: false });
25
+ assert.strictEqual(code, 0);
26
+ const output = writes.join('');
27
+ assert.ok(output.includes('Duration:'), `Expected summary output to contain "Duration:", got:\n${output}`);
28
+ }
29
+ finally {
30
+ process.stdout.write = origWrite;
31
+ }
32
+ });
33
+ it('should include durationMs in JSON output when --index is used', async () => {
34
+ const writes = [];
35
+ const origWrite = process.stdout.write.bind(process.stdout);
36
+ process.stdout.write = (chunk, ..._rest) => {
37
+ writes.push(String(chunk));
38
+ return true;
39
+ };
40
+ try {
41
+ const { handleInit } = await import('./cmd-init.js');
42
+ const code = await handleInit('/tmp/test-project', { index: true, json: true });
43
+ assert.strictEqual(code, 0);
44
+ const output = writes.join('');
45
+ assert.ok(output.includes('durationMs'), `Expected JSON output to contain "durationMs", got:\n${output}`);
46
+ }
47
+ finally {
48
+ process.stdout.write = origWrite;
49
+ }
50
+ });
51
+ });
@@ -0,0 +1,5 @@
1
+ export interface ListApisOptions {
2
+ json?: boolean;
3
+ all?: boolean;
4
+ }
5
+ export declare function handleListApis(projectRoot: string, feature?: string, options?: ListApisOptions): Promise<number>;
@@ -0,0 +1,64 @@
1
+ import { createRequire } from 'node:module';
2
+ const require = createRequire(import.meta.url);
3
+ import { closeIndex } from './cg-instance.js';
4
+ import { formatApiList, formatApiListGrouped, formatOutput } from './formatter.js';
5
+ export async function handleListApis(projectRoot, feature, options = {}) {
6
+ const { CodeGraph } = require('@colbymchenry/codegraph');
7
+ const cg = await CodeGraph.open(projectRoot, { sync: false, readOnly: true });
8
+ let nodes = cg.getNodesByKind('function');
9
+ // Filter by feature directory if specified
10
+ if (feature) {
11
+ const featurePath = feature.replace(/^\/?/, '').replace(/\/?$/, '/');
12
+ nodes = nodes.filter((n) => n.filePath.startsWith(featurePath));
13
+ }
14
+ // Only exported functions unless --all is specified
15
+ if (!options.all) {
16
+ nodes = nodes.filter((n) => n.isExported);
17
+ }
18
+ const apis = [];
19
+ for (const node of nodes) {
20
+ const callers = cg.getCallers(node.id).map((c) => ({
21
+ name: c.node.name,
22
+ filePath: c.node.filePath,
23
+ startLine: c.node.startLine,
24
+ }));
25
+ apis.push({
26
+ name: node.name,
27
+ kind: node.kind,
28
+ filePath: node.filePath,
29
+ startLine: node.startLine,
30
+ endLine: node.endLine,
31
+ qualifiedName: node.qualifiedName,
32
+ signature: node.signature,
33
+ isExported: !!node.isExported,
34
+ callerCount: callers.length,
35
+ callers,
36
+ });
37
+ }
38
+ closeIndex(cg);
39
+ if (options.json) {
40
+ // For JSON with --all, group by directory
41
+ if (options.all) {
42
+ const grouped = {};
43
+ for (const api of apis) {
44
+ const dir = api.filePath.substring(0, api.filePath.lastIndexOf('/'));
45
+ if (!grouped[dir])
46
+ grouped[dir] = [];
47
+ grouped[dir].push(api);
48
+ }
49
+ process.stdout.write(formatOutput(grouped, { json: true }) + '\n');
50
+ }
51
+ else {
52
+ process.stdout.write(formatOutput(apis, { json: true }) + '\n');
53
+ }
54
+ }
55
+ else {
56
+ if (options.all) {
57
+ process.stdout.write(formatApiListGrouped(apis) + '\n');
58
+ }
59
+ else {
60
+ process.stdout.write(formatApiList(apis) + '\n');
61
+ }
62
+ }
63
+ return 0;
64
+ }
@@ -0,0 +1,69 @@
1
+ import { describe, it, mock } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createRequire } from 'node:module';
4
+ const require = createRequire(import.meta.url);
5
+ describe('handleListApis', () => {
6
+ it('should group APIs by directory when --all is used and show ungrouped output without --all', async (ctx) => {
7
+ const nodes = [
8
+ {
9
+ id: 'n1',
10
+ name: 'funcA',
11
+ kind: 'function',
12
+ filePath: 'src/feature/a.ts',
13
+ startLine: 10,
14
+ endLine: 30,
15
+ qualifiedName: 'funcA',
16
+ signature: '(x: string): void',
17
+ isExported: true,
18
+ },
19
+ {
20
+ id: 'n2',
21
+ name: 'funcB',
22
+ kind: 'function',
23
+ filePath: 'src/lib/b.ts',
24
+ startLine: 5,
25
+ endLine: 25,
26
+ qualifiedName: 'funcB',
27
+ signature: '(y: number): string',
28
+ isExported: true,
29
+ },
30
+ ];
31
+ // Use manual require + mock.method instead of ctx.mock.module,
32
+ // because lazy CJS require() inside handlers bypasses mock.module interception.
33
+ const { CodeGraph } = require('@colbymchenry/codegraph');
34
+ const openMock = mock.method(CodeGraph, 'open', async () => ({
35
+ getNodesByKind: (_kind) => nodes,
36
+ getCallers: (_id) => [],
37
+ close: () => { },
38
+ }));
39
+ const { handleListApis } = await import('./cmd-list-apis.js');
40
+ // Test 1: --all produces directory-grouped output
41
+ {
42
+ const chunks = [];
43
+ ctx.mock.method(process.stdout, 'write', (chunk) => {
44
+ chunks.push(String(chunk));
45
+ });
46
+ await handleListApis('/fake/root', undefined, { all: true });
47
+ const output = chunks.join('');
48
+ assert.ok(output.includes('=== src/feature/ ==='), 'should group under src/feature/');
49
+ assert.ok(output.includes('=== src/lib/ ==='), 'should group under src/lib/');
50
+ assert.ok(output.includes('funcA'), 'should list funcA in output');
51
+ assert.ok(output.includes('funcB'), 'should list funcB in output');
52
+ ctx.mock.reset();
53
+ }
54
+ // Test 2: without --all, output is not grouped
55
+ {
56
+ const chunks = [];
57
+ ctx.mock.method(process.stdout, 'write', (chunk) => {
58
+ chunks.push(String(chunk));
59
+ });
60
+ await handleListApis('/fake/root', undefined, { all: false });
61
+ const output = chunks.join('');
62
+ assert.ok(!output.includes('=== src/feature/ ==='), 'should not group under src/feature/');
63
+ assert.ok(!output.includes('=== src/lib/ ==='), 'should not group under src/lib/');
64
+ assert.ok(output.includes('APIs'), 'should start with APIs count');
65
+ }
66
+ // Clean up all mocks (including CodeGraph.open)
67
+ mock.reset();
68
+ });
69
+ });
@@ -0,0 +1,5 @@
1
+ export interface SearchOptions {
2
+ limit?: number;
3
+ json?: boolean;
4
+ }
5
+ export declare function handleSearch(projectRoot: string, query: string, options?: SearchOptions): Promise<number>;
@@ -0,0 +1,21 @@
1
+ import { createRequire } from 'node:module';
2
+ const require = createRequire(import.meta.url);
3
+ import { closeIndex } from './cg-instance.js';
4
+ import { formatSearchResults, formatOutput } from './formatter.js';
5
+ export async function handleSearch(projectRoot, query, options = {}) {
6
+ const { CodeGraph } = require('@colbymchenry/codegraph');
7
+ if (!CodeGraph.isInitialized(projectRoot)) {
8
+ process.stderr.write('CodeGraph is not initialized. Run `apltk codegraph init` first.\n');
9
+ return 1;
10
+ }
11
+ const cg = await CodeGraph.open(projectRoot, { sync: false, readOnly: true });
12
+ const results = cg.searchNodes(query, { limit: options.limit ?? 20 });
13
+ closeIndex(cg);
14
+ if (options.json) {
15
+ process.stdout.write(formatOutput(results, { json: true }) + '\n');
16
+ }
17
+ else {
18
+ process.stdout.write(formatSearchResults(results) + '\n');
19
+ }
20
+ return 0;
21
+ }
@@ -0,0 +1,30 @@
1
+ import { describe, it, mock } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createRequire } from 'node:module';
4
+ const require = createRequire(import.meta.url);
5
+ describe('REGTEST-R2-03: handleSearch — CodeGraph init check', () => {
6
+ it('should return exit code 1 when CodeGraph is not initialized', async () => {
7
+ // Mock CodeGraph.isInitialized to return false before importing the module under test
8
+ const { CodeGraph } = require('@colbymchenry/codegraph');
9
+ const initMock = mock.method(CodeGraph, 'isInitialized', () => false);
10
+ // Import after mock is in place
11
+ const { handleSearch } = await import('./cmd-search.js');
12
+ // Capture stderr writes
13
+ const stderrChunks = [];
14
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
15
+ process.stderr.write = (chunk) => {
16
+ stderrChunks.push(String(chunk));
17
+ return true;
18
+ };
19
+ try {
20
+ const exitCode = await handleSearch('/test/project', 'testQuery', {});
21
+ assert.strictEqual(exitCode, 1);
22
+ const stderrOutput = stderrChunks.join('');
23
+ assert.ok(stderrOutput.includes('CodeGraph is not initialized'), 'stderr should contain "CodeGraph is not initialized" message');
24
+ }
25
+ finally {
26
+ process.stderr.write = originalStderrWrite;
27
+ initMock.mock.restore();
28
+ }
29
+ });
30
+ });
@@ -0,0 +1,4 @@
1
+ export interface StatusOptions {
2
+ json?: boolean;
3
+ }
4
+ export declare function handleStatus(projectRoot: string, options?: StatusOptions): Promise<number>;
@@ -0,0 +1,44 @@
1
+ import { createRequire } from 'node:module';
2
+ const require = createRequire(import.meta.url);
3
+ import { closeIndex, getCodeGraphModule } from './cg-instance.js';
4
+ import { formatSummary, formatOutput } from './formatter.js';
5
+ export async function handleStatus(projectRoot, options = {}) {
6
+ if (!getCodeGraphModule().CodeGraph.isInitialized(projectRoot)) {
7
+ process.stderr.write('CodeGraph is not initialized. Run `apltk codegraph init` first.\n');
8
+ return 1;
9
+ }
10
+ const cg = await getCodeGraphModule().CodeGraph.open(projectRoot, { sync: false, readOnly: true });
11
+ const stats = cg.getStats();
12
+ closeIndex(cg);
13
+ if (options.json) {
14
+ process.stdout.write(formatOutput(stats, { json: true }) + '\n');
15
+ }
16
+ else {
17
+ // Summarize nodes by kind (skip zero entries)
18
+ const kindEntries = Object.entries(stats.nodesByKind).filter(([, v]) => v > 0);
19
+ const nodeKindSummary = kindEntries.map(([kind, count]) => ` ${kind.padEnd(14)} ${count}`).join('\n');
20
+ const edgeKindEntries = Object.entries(stats.edgesByKind).filter(([, v]) => v > 0);
21
+ const edgeKindSummary = edgeKindEntries.map(([kind, count]) => ` ${kind.padEnd(14)} ${count}`).join('\n');
22
+ const langEntries = Object.entries(stats.filesByLanguage || {}).filter(([, v]) => v > 0);
23
+ const langSummary = langEntries.length > 0
24
+ ? langEntries.map(([lang, count]) => ` ${lang.padEnd(14)} ${count}`).join('\n')
25
+ : ' (no files indexed)';
26
+ const summary = [
27
+ ['Project:', projectRoot],
28
+ ['Files:', stats.fileCount],
29
+ ['Nodes:', stats.nodeCount],
30
+ ['Edges:', stats.edgeCount],
31
+ ['DB size:', `${(stats.dbSizeBytes / 1024).toFixed(1)} KB`],
32
+ ['Last updated:', new Date(stats.lastUpdated).toISOString()],
33
+ ['', ''],
34
+ ['Nodes by kind:', ''],
35
+ ];
36
+ process.stdout.write(formatSummary(summary) + '\n');
37
+ process.stdout.write(nodeKindSummary + '\n');
38
+ process.stdout.write('\nEdges by kind:\n');
39
+ process.stdout.write(edgeKindSummary + '\n');
40
+ process.stdout.write('\nLanguages:\n');
41
+ process.stdout.write(langSummary + '\n');
42
+ }
43
+ return 0;
44
+ }