javi-forge 0.1.0 → 1.0.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.
Files changed (159) hide show
  1. package/.releaserc +2 -1
  2. package/README.md +143 -31
  3. package/ai-config/commands/workflows/diagnose.md +70 -0
  4. package/ai-config/commands/workflows/discover.md +86 -0
  5. package/dist/commands/doctor.js +24 -1
  6. package/dist/commands/init.js +48 -1
  7. package/dist/commands/llmstxt.d.ts +9 -0
  8. package/dist/commands/llmstxt.js +93 -0
  9. package/dist/commands/llmstxt.test.d.ts +2 -0
  10. package/dist/commands/plugin.d.ts +24 -0
  11. package/dist/commands/plugin.js +78 -0
  12. package/dist/commands/plugin.test.d.ts +2 -0
  13. package/dist/constants.d.ts +8 -0
  14. package/dist/constants.js +8 -0
  15. package/dist/index.js +33 -4
  16. package/dist/lib/plugin.d.ts +39 -0
  17. package/dist/lib/plugin.js +228 -0
  18. package/dist/lib/plugin.test.d.ts +2 -0
  19. package/dist/types/index.d.ts +42 -0
  20. package/dist/ui/App.d.ts +2 -1
  21. package/dist/ui/App.js +2 -1
  22. package/dist/ui/LlmsTxt.d.ts +8 -0
  23. package/dist/ui/LlmsTxt.js +48 -0
  24. package/dist/ui/Plugin.d.ts +9 -0
  25. package/dist/ui/Plugin.js +96 -0
  26. package/modules/obsidian-brain/README.md +32 -0
  27. package/modules/obsidian-brain/core/templates/braindump.md +15 -0
  28. package/modules/obsidian-brain/core/templates/consolidation.md +42 -0
  29. package/modules/obsidian-brain/core/templates/daily-note.md +18 -0
  30. package/modules/obsidian-brain/core/templates/resource-capture.md +14 -0
  31. package/modules/obsidian-brain/developer/templates/adr.md +40 -0
  32. package/modules/obsidian-brain/developer/templates/coding-session.md +24 -0
  33. package/modules/obsidian-brain/developer/templates/debug-journal.md +22 -0
  34. package/modules/obsidian-brain/developer/templates/sdd-feedback.md +27 -0
  35. package/modules/obsidian-brain/developer/templates/tech-debt.md +20 -0
  36. package/modules/obsidian-brain/pm-lead/templates/daily-brief.md +25 -0
  37. package/modules/obsidian-brain/pm-lead/templates/meeting-notes.md +24 -0
  38. package/modules/obsidian-brain/pm-lead/templates/risk-registry.md +12 -0
  39. package/modules/obsidian-brain/pm-lead/templates/sprint-review.md +27 -0
  40. package/modules/obsidian-brain/pm-lead/templates/stakeholder-update.md +24 -0
  41. package/modules/obsidian-brain/pm-lead/templates/team-intelligence.md +19 -0
  42. package/modules/obsidian-brain/pm-lead/templates/weekly-brief.md +29 -0
  43. package/package.json +1 -1
  44. package/schemas/plugin.schema.json +62 -0
  45. package/ai-config/skills/docs/api-documentation/SKILL.md +0 -293
  46. package/ai-config/skills/docs/docs-spring/SKILL.md +0 -377
  47. package/ai-config/skills/docs/mustache-templates/SKILL.md +0 -190
  48. package/ai-config/skills/docs/technical-docs/SKILL.md +0 -447
  49. package/dist/commands/analyze.d.ts.map +0 -1
  50. package/dist/commands/analyze.js.map +0 -1
  51. package/dist/commands/analyze.test.d.ts.map +0 -1
  52. package/dist/commands/analyze.test.js +0 -145
  53. package/dist/commands/analyze.test.js.map +0 -1
  54. package/dist/commands/doctor.d.ts.map +0 -1
  55. package/dist/commands/doctor.js.map +0 -1
  56. package/dist/commands/doctor.test.d.ts.map +0 -1
  57. package/dist/commands/doctor.test.js +0 -200
  58. package/dist/commands/doctor.test.js.map +0 -1
  59. package/dist/commands/init.d.ts.map +0 -1
  60. package/dist/commands/init.js.map +0 -1
  61. package/dist/commands/init.test.d.ts.map +0 -1
  62. package/dist/commands/init.test.js +0 -271
  63. package/dist/commands/init.test.js.map +0 -1
  64. package/dist/commands/sync.d.ts.map +0 -1
  65. package/dist/commands/sync.js.map +0 -1
  66. package/dist/constants.d.ts.map +0 -1
  67. package/dist/constants.js.map +0 -1
  68. package/dist/e2e/aggressive.e2e.test.d.ts.map +0 -1
  69. package/dist/e2e/aggressive.e2e.test.js +0 -350
  70. package/dist/e2e/aggressive.e2e.test.js.map +0 -1
  71. package/dist/e2e/commands.e2e.test.d.ts.map +0 -1
  72. package/dist/e2e/commands.e2e.test.js +0 -213
  73. package/dist/e2e/commands.e2e.test.js.map +0 -1
  74. package/dist/index.d.ts.map +0 -1
  75. package/dist/index.js.map +0 -1
  76. package/dist/lib/common.d.ts.map +0 -1
  77. package/dist/lib/common.js.map +0 -1
  78. package/dist/lib/common.test.d.ts.map +0 -1
  79. package/dist/lib/common.test.js +0 -316
  80. package/dist/lib/common.test.js.map +0 -1
  81. package/dist/lib/frontmatter.d.ts.map +0 -1
  82. package/dist/lib/frontmatter.js.map +0 -1
  83. package/dist/lib/frontmatter.test.d.ts.map +0 -1
  84. package/dist/lib/frontmatter.test.js +0 -257
  85. package/dist/lib/frontmatter.test.js.map +0 -1
  86. package/dist/lib/template.d.ts.map +0 -1
  87. package/dist/lib/template.js.map +0 -1
  88. package/dist/lib/template.test.d.ts.map +0 -1
  89. package/dist/lib/template.test.js +0 -201
  90. package/dist/lib/template.test.js.map +0 -1
  91. package/dist/types/index.d.ts.map +0 -1
  92. package/dist/types/index.js.map +0 -1
  93. package/dist/ui/AnalyzeUI.d.ts.map +0 -1
  94. package/dist/ui/AnalyzeUI.js.map +0 -1
  95. package/dist/ui/App.d.ts.map +0 -1
  96. package/dist/ui/App.js.map +0 -1
  97. package/dist/ui/CIContext.d.ts.map +0 -1
  98. package/dist/ui/CIContext.js.map +0 -1
  99. package/dist/ui/CISelector.d.ts.map +0 -1
  100. package/dist/ui/CISelector.js.map +0 -1
  101. package/dist/ui/Doctor.d.ts.map +0 -1
  102. package/dist/ui/Doctor.js.map +0 -1
  103. package/dist/ui/Header.d.ts.map +0 -1
  104. package/dist/ui/Header.js.map +0 -1
  105. package/dist/ui/MemorySelector.d.ts.map +0 -1
  106. package/dist/ui/MemorySelector.js.map +0 -1
  107. package/dist/ui/NameInput.d.ts.map +0 -1
  108. package/dist/ui/NameInput.js.map +0 -1
  109. package/dist/ui/OptionSelector.d.ts.map +0 -1
  110. package/dist/ui/OptionSelector.js.map +0 -1
  111. package/dist/ui/Progress.d.ts.map +0 -1
  112. package/dist/ui/Progress.js.map +0 -1
  113. package/dist/ui/StackSelector.d.ts.map +0 -1
  114. package/dist/ui/StackSelector.js.map +0 -1
  115. package/dist/ui/Summary.d.ts.map +0 -1
  116. package/dist/ui/Summary.js.map +0 -1
  117. package/dist/ui/SyncUI.d.ts.map +0 -1
  118. package/dist/ui/SyncUI.js.map +0 -1
  119. package/dist/ui/Welcome.d.ts.map +0 -1
  120. package/dist/ui/Welcome.js.map +0 -1
  121. package/dist/ui/theme.d.ts.map +0 -1
  122. package/dist/ui/theme.js.map +0 -1
  123. package/modules/obsidian-brain/.obsidian/plugins/dataview/data.json +0 -25
  124. package/modules/obsidian-brain/.obsidian/plugins/obsidian-kanban/data.json +0 -29
  125. package/modules/obsidian-brain/.obsidian/plugins/templater-obsidian/data.json +0 -18
  126. package/src/commands/analyze.test.ts +0 -145
  127. package/src/commands/analyze.ts +0 -69
  128. package/src/commands/doctor.test.ts +0 -208
  129. package/src/commands/doctor.ts +0 -163
  130. package/src/commands/init.test.ts +0 -298
  131. package/src/commands/init.ts +0 -285
  132. package/src/constants.ts +0 -69
  133. package/src/e2e/aggressive.e2e.test.ts +0 -557
  134. package/src/e2e/commands.e2e.test.ts +0 -298
  135. package/src/index.tsx +0 -106
  136. package/src/lib/common.test.ts +0 -318
  137. package/src/lib/common.ts +0 -127
  138. package/src/lib/frontmatter.test.ts +0 -291
  139. package/src/lib/frontmatter.ts +0 -77
  140. package/src/lib/template.test.ts +0 -226
  141. package/src/lib/template.ts +0 -99
  142. package/src/types/index.ts +0 -53
  143. package/src/ui/AnalyzeUI.tsx +0 -133
  144. package/src/ui/App.tsx +0 -175
  145. package/src/ui/CIContext.tsx +0 -25
  146. package/src/ui/CISelector.tsx +0 -72
  147. package/src/ui/Doctor.tsx +0 -122
  148. package/src/ui/Header.tsx +0 -48
  149. package/src/ui/MemorySelector.tsx +0 -73
  150. package/src/ui/NameInput.tsx +0 -82
  151. package/src/ui/OptionSelector.tsx +0 -100
  152. package/src/ui/Progress.tsx +0 -88
  153. package/src/ui/StackSelector.tsx +0 -101
  154. package/src/ui/Summary.tsx +0 -134
  155. package/src/ui/Welcome.tsx +0 -54
  156. package/src/ui/theme.ts +0 -10
  157. package/stryker.config.json +0 -19
  158. package/tsconfig.json +0 -19
  159. package/vitest.config.ts +0 -16
@@ -1,145 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest'
2
- import type { InitStep } from '../types/index.js'
3
-
4
- // ── Mock child_process ───────────────────────────────────────────────────────
5
- vi.mock('child_process', () => ({
6
- execFile: vi.fn((_cmd: string, _args: string[], _opts: unknown, cb: unknown) => {
7
- if (typeof cb === 'function') cb(null, { stdout: '', stderr: '' })
8
- return undefined as any
9
- }),
10
- }))
11
-
12
- import { execFile } from 'child_process'
13
- import { runAnalyze } from './analyze.js'
14
-
15
- const mockedExecFile = vi.mocked(execFile)
16
-
17
- beforeEach(() => {
18
- vi.resetAllMocks()
19
- })
20
-
21
- function collectSteps(projectDir: string, dryRun: boolean): Promise<InitStep[]> {
22
- const steps: InitStep[] = []
23
- return runAnalyze(projectDir, dryRun, (step) => steps.push(step)).then(() => steps)
24
- }
25
-
26
- describe('runAnalyze', () => {
27
- it('reports error when repoforge not found', async () => {
28
- // which fails → repoforge not found
29
- mockedExecFile.mockImplementation((_cmd: unknown, _args: unknown, _opts: unknown, cb: unknown) => {
30
- const cmd = String(_cmd)
31
- // The 'which' call is execFile('which', ['repoforge']) with 2 args (no opts)
32
- // promisify(execFile) passes (cmd, args) → node adds callback
33
- if (cmd === 'which') {
34
- const callback = typeof _opts === 'function' ? _opts : cb
35
- if (typeof callback === 'function') (callback as Function)(new Error('not found'))
36
- }
37
- return undefined as any
38
- })
39
-
40
- const steps = await collectSteps('/project', false)
41
- const errorStep = steps.find(s => s.status === 'error')
42
- expect(errorStep).toBeDefined()
43
- expect(errorStep!.detail).toContain('repoforge not found')
44
- expect(errorStep!.detail).toContain('pip install repoforge')
45
- })
46
-
47
- it('reports done when repoforge succeeds', async () => {
48
- mockedExecFile.mockImplementation((_cmd: unknown, _args: unknown, _opts: unknown, cb: unknown) => {
49
- const cmd = String(_cmd)
50
- if (cmd === 'which') {
51
- const callback = typeof _opts === 'function' ? _opts : cb
52
- if (typeof callback === 'function') (callback as Function)(null, { stdout: '/usr/bin/repoforge', stderr: '' })
53
- } else if (cmd === 'repoforge') {
54
- const callback = typeof _opts === 'function' ? _opts : cb
55
- if (typeof callback === 'function') (callback as Function)(null, { stdout: 'Analysis complete\nGenerated 5 skills', stderr: '' })
56
- }
57
- return undefined as any
58
- })
59
-
60
- const steps = await collectSteps('/project', false)
61
- const doneStep = steps.find(s => s.id === 'analyze-repoforge' && s.status === 'done')
62
- expect(doneStep).toBeDefined()
63
- expect(doneStep!.detail).toContain('Generated 5 skills')
64
- })
65
-
66
- it('passes --dry-run when dryRun is true', async () => {
67
- let capturedArgs: string[] = []
68
- mockedExecFile.mockImplementation((_cmd: unknown, _args: unknown, _opts: unknown, cb: unknown) => {
69
- const cmd = String(_cmd)
70
- if (cmd === 'which') {
71
- const callback = typeof _opts === 'function' ? _opts : cb
72
- if (typeof callback === 'function') (callback as Function)(null, { stdout: '/usr/bin/repoforge', stderr: '' })
73
- } else if (cmd === 'repoforge') {
74
- capturedArgs = (_args as string[]) ?? []
75
- const callback = typeof _opts === 'function' ? _opts : cb
76
- if (typeof callback === 'function') (callback as Function)(null, { stdout: '', stderr: '' })
77
- }
78
- return undefined as any
79
- })
80
-
81
- await collectSteps('/project', true)
82
- expect(capturedArgs).toContain('--dry-run')
83
- })
84
-
85
- it('reports error when repoforge execution fails', async () => {
86
- mockedExecFile.mockImplementation((_cmd: unknown, _args: unknown, _opts: unknown, cb: unknown) => {
87
- const cmd = String(_cmd)
88
- if (cmd === 'which') {
89
- const callback = typeof _opts === 'function' ? _opts : cb
90
- if (typeof callback === 'function') (callback as Function)(null, { stdout: '/usr/bin/repoforge', stderr: '' })
91
- } else if (cmd === 'repoforge') {
92
- const callback = typeof _opts === 'function' ? _opts : cb
93
- if (typeof callback === 'function') (callback as Function)(new Error('Analysis crashed'))
94
- }
95
- return undefined as any
96
- })
97
-
98
- const steps = await collectSteps('/project', false)
99
- const errorStep = steps.find(s => s.id === 'analyze-repoforge' && s.status === 'error')
100
- expect(errorStep).toBeDefined()
101
- expect(errorStep!.detail).toContain('Analysis crashed')
102
- })
103
-
104
- it('reports done with dry-run message when no stdout and dryRun', async () => {
105
- mockedExecFile.mockImplementation((_cmd: unknown, _args: unknown, _opts: unknown, cb: unknown) => {
106
- const cmd = String(_cmd)
107
- if (cmd === 'which') {
108
- const callback = typeof _opts === 'function' ? _opts : cb
109
- if (typeof callback === 'function') (callback as Function)(null, { stdout: '/usr/bin/repoforge', stderr: '' })
110
- } else if (cmd === 'repoforge') {
111
- const callback = typeof _opts === 'function' ? _opts : cb
112
- if (typeof callback === 'function') (callback as Function)(null, { stdout: '', stderr: '' })
113
- }
114
- return undefined as any
115
- })
116
-
117
- const steps = await collectSteps('/project', true)
118
- const doneStep = steps.find(s => s.id === 'analyze-repoforge' && s.status === 'done')
119
- expect(doneStep).toBeDefined()
120
- expect(doneStep!.detail).toContain('dry-run')
121
- })
122
-
123
- it('reports warning when stderr has output', async () => {
124
- mockedExecFile.mockImplementation((_cmd: unknown, _args: unknown, _opts: unknown, cb: unknown) => {
125
- const cmd = String(_cmd)
126
- if (cmd === 'which') {
127
- const callback = typeof _opts === 'function' ? _opts : cb
128
- if (typeof callback === 'function') (callback as Function)(null, { stdout: '/usr/bin/repoforge', stderr: '' })
129
- } else if (cmd === 'repoforge') {
130
- const callback = typeof _opts === 'function' ? _opts : cb
131
- if (typeof callback === 'function') (callback as Function)(null, {
132
- stdout: 'Analysis done',
133
- stderr: 'Warning: deprecated API\nWarning: slow scan',
134
- })
135
- }
136
- return undefined as any
137
- })
138
-
139
- const steps = await collectSteps('/project', false)
140
- const warnStep = steps.find(s => s.id === 'analyze-warnings')
141
- expect(warnStep).toBeDefined()
142
- expect(warnStep!.status).toBe('skipped')
143
- expect(warnStep!.detail).toContain('Warning: slow scan')
144
- })
145
- })
@@ -1,69 +0,0 @@
1
- import { execFile } from 'child_process'
2
- import { promisify } from 'util'
3
- import type { InitStep } from '../types/index.js'
4
-
5
- const execFileAsync = promisify(execFile)
6
-
7
- type StepCallback = (step: InitStep) => void
8
-
9
- function report(onStep: StepCallback, id: string, label: string, status: InitStep['status'], detail?: string) {
10
- onStep({ id, label, status, detail })
11
- }
12
-
13
- /** Check if a binary is available in PATH */
14
- async function which(bin: string): Promise<boolean> {
15
- try {
16
- await execFileAsync('which', [bin])
17
- return true
18
- } catch {
19
- return false
20
- }
21
- }
22
-
23
- /**
24
- * Run repoforge skills analysis on a project directory.
25
- * This is a thin wrapper that delegates to the repoforge CLI.
26
- */
27
- export async function runAnalyze(
28
- projectDir: string,
29
- dryRun: boolean,
30
- onStep: StepCallback
31
- ): Promise<void> {
32
- const stepId = 'analyze-repoforge'
33
- report(onStep, stepId, 'Run repoforge skills analysis', 'running')
34
-
35
- try {
36
- // Check if repoforge is installed
37
- const hasRepoforge = await which('repoforge')
38
-
39
- if (!hasRepoforge) {
40
- report(onStep, stepId, 'Run repoforge skills analysis', 'error',
41
- 'repoforge not found. Install with: pip install repoforge')
42
- return
43
- }
44
-
45
- const args = ['skills', '-w', projectDir]
46
- if (dryRun) {
47
- args.push('--dry-run')
48
- }
49
-
50
- const { stdout, stderr } = await execFileAsync('repoforge', args, {
51
- cwd: projectDir,
52
- timeout: 300_000, // 5 min — analysis can take a while on large repos
53
- })
54
-
55
- if (stdout) {
56
- report(onStep, stepId, 'Run repoforge skills analysis', 'done', stdout.trim().split('\n').pop() ?? 'complete')
57
- } else {
58
- report(onStep, stepId, 'Run repoforge skills analysis', 'done', dryRun ? 'dry-run complete' : 'complete')
59
- }
60
-
61
- if (stderr) {
62
- const warnId = 'analyze-warnings'
63
- report(onStep, warnId, 'Analysis warnings', 'skipped', stderr.trim().split('\n').pop() ?? '')
64
- }
65
- } catch (e: unknown) {
66
- const msg = e instanceof Error ? e.message : String(e)
67
- report(onStep, stepId, 'Run repoforge skills analysis', 'error', msg)
68
- }
69
- }
@@ -1,208 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest'
2
-
3
- // ── Mock fs-extra ────────────────────────────────────────────────────────────
4
- vi.mock('fs-extra', () => {
5
- const mockFs = {
6
- pathExists: vi.fn(),
7
- readFile: vi.fn(),
8
- readJson: vi.fn(),
9
- readdir: vi.fn(),
10
- copy: vi.fn(),
11
- ensureDir: vi.fn(),
12
- }
13
- return { default: mockFs, ...mockFs }
14
- })
15
-
16
- // ── Mock child_process ───────────────────────────────────────────────────────
17
- vi.mock('child_process', () => ({
18
- execFile: vi.fn((_cmd: string, _args: string[], cb: unknown) => {
19
- if (typeof cb === 'function') cb(null, { stdout: '', stderr: '' })
20
- return undefined as any
21
- }),
22
- }))
23
-
24
- // ── Mock common module ───────────────────────────────────────────────────────
25
- vi.mock('../lib/common.js', () => ({
26
- detectStack: vi.fn(),
27
- backupIfExists: vi.fn().mockResolvedValue(false),
28
- ensureDirExists: vi.fn().mockResolvedValue(undefined),
29
- STACK_LABELS: {
30
- 'node': 'Node.js / TypeScript',
31
- 'python': 'Python',
32
- 'go': 'Go',
33
- 'rust': 'Rust',
34
- 'java-gradle': 'Java (Gradle)',
35
- 'java-maven': 'Java (Maven)',
36
- 'elixir': 'Elixir',
37
- },
38
- }))
39
-
40
- import fs from 'fs-extra'
41
- import { execFile } from 'child_process'
42
- import { runDoctor } from './doctor.js'
43
- import { detectStack } from '../lib/common.js'
44
-
45
- const mockedFs = vi.mocked(fs)
46
- const mockedExecFile = vi.mocked(execFile)
47
- const mockedDetectStack = vi.mocked(detectStack)
48
-
49
- beforeEach(() => {
50
- vi.resetAllMocks()
51
-
52
- // Default: all paths exist, readdir returns items
53
- mockedFs.pathExists.mockResolvedValue(true as never)
54
- mockedFs.readdir.mockResolvedValue(['file1', 'file2', '.hidden'] as never)
55
-
56
- // Default: which + version succeed
57
- mockedExecFile.mockImplementation((_cmd: unknown, _args: unknown, cb: unknown) => {
58
- const cmd = String(_cmd)
59
- if (cmd === 'which') {
60
- if (typeof cb === 'function') cb(null, { stdout: '/usr/bin/tool', stderr: '' })
61
- } else {
62
- if (typeof cb === 'function') cb(null, { stdout: 'v1.0.0', stderr: '' })
63
- }
64
- return undefined as any
65
- })
66
-
67
- // Default: no stack detected
68
- mockedDetectStack.mockResolvedValue(null)
69
- })
70
-
71
- describe('runDoctor', () => {
72
- it('reports all tools as ok when present', async () => {
73
- const result = await runDoctor('/test/project')
74
- const toolSection = result.sections.find(s => s.title === 'System Tools')
75
- expect(toolSection).toBeDefined()
76
- const allOk = toolSection!.checks.every(c => c.status === 'ok')
77
- expect(allOk).toBe(true)
78
- })
79
-
80
- it('reports fail for missing required tool', async () => {
81
- mockedExecFile.mockImplementation((_cmd: unknown, _args: unknown, cb: unknown) => {
82
- const cmd = String(_cmd)
83
- const args = _args as string[]
84
- if (cmd === 'which' && args?.[0] === 'git') {
85
- if (typeof cb === 'function') cb(new Error('not found'), { stdout: '', stderr: '' })
86
- } else if (cmd === 'which') {
87
- if (typeof cb === 'function') cb(null, { stdout: '/usr/bin/tool', stderr: '' })
88
- } else {
89
- if (typeof cb === 'function') cb(null, { stdout: 'v1.0.0', stderr: '' })
90
- }
91
- return undefined as any
92
- })
93
-
94
- const result = await runDoctor('/test/project')
95
- const toolSection = result.sections.find(s => s.title === 'System Tools')!
96
- const gitCheck = toolSection.checks.find(c => c.label === 'Git')
97
- expect(gitCheck!.status).toBe('fail')
98
- })
99
-
100
- it('reports skip for missing optional tool (docker)', async () => {
101
- mockedExecFile.mockImplementation((_cmd: unknown, _args: unknown, cb: unknown) => {
102
- const cmd = String(_cmd)
103
- const args = _args as string[]
104
- if (cmd === 'which' && args?.[0] === 'docker') {
105
- if (typeof cb === 'function') cb(new Error('not found'), { stdout: '', stderr: '' })
106
- } else if (cmd === 'which') {
107
- if (typeof cb === 'function') cb(null, { stdout: '/usr/bin/tool', stderr: '' })
108
- } else {
109
- if (typeof cb === 'function') cb(null, { stdout: 'v1.0.0', stderr: '' })
110
- }
111
- return undefined as any
112
- })
113
-
114
- const result = await runDoctor('/test/project')
115
- const toolSection = result.sections.find(s => s.title === 'System Tools')!
116
- const dockerCheck = toolSection.checks.find(c => c.label === 'Docker')
117
- expect(dockerCheck!.status).toBe('skip')
118
- })
119
-
120
- it('shows skip when no manifest found', async () => {
121
- mockedFs.pathExists.mockImplementation(async (p: unknown) => {
122
- const s = String(p)
123
- if (s.includes('manifest.json')) return false as never
124
- return true as never
125
- })
126
-
127
- const result = await runDoctor('/test/project')
128
- const manifestSection = result.sections.find(s => s.title === 'Project Manifest')!
129
- const manifestCheck = manifestSection.checks.find(c => c.label === 'Forge manifest')
130
- expect(manifestCheck!.status).toBe('skip')
131
- expect(manifestCheck!.detail).toContain('not a forge-managed project')
132
- })
133
-
134
- it('shows manifest details when found', async () => {
135
- mockedFs.readJson.mockResolvedValue({
136
- version: '0.1.0',
137
- projectName: 'test-project',
138
- stack: 'node',
139
- ciProvider: 'github',
140
- memory: 'engram',
141
- createdAt: '2025-01-15T10:00:00Z',
142
- updatedAt: '2025-01-15T10:00:00Z',
143
- modules: ['engram', 'ghagga'],
144
- } as never)
145
-
146
- const result = await runDoctor('/test/project')
147
- const manifestSection = result.sections.find(s => s.title === 'Project Manifest')!
148
- const manifestCheck = manifestSection.checks.find(c => c.label === 'Forge manifest')
149
- expect(manifestCheck!.status).toBe('ok')
150
- expect(manifestCheck!.detail).toContain('test-project')
151
-
152
- const modulesCheck = manifestSection.checks.find(c => c.label === 'Modules')
153
- expect(modulesCheck!.status).toBe('ok')
154
- expect(modulesCheck!.detail).toContain('engram')
155
- })
156
-
157
- it('reports ok for existing framework dirs', async () => {
158
- mockedFs.pathExists.mockResolvedValue(true as never)
159
- mockedFs.readdir.mockResolvedValue(['a', 'b', '.dotfile'] as never)
160
-
161
- const result = await runDoctor('/test/project')
162
- const structSection = result.sections.find(s => s.title === 'Framework Structure')!
163
- expect(structSection.checks.every(c => c.status === 'ok')).toBe(true)
164
- // countDir should filter dotfiles → "2 entries"
165
- expect(structSection.checks[0].detail).toBe('2 entries')
166
- })
167
-
168
- it('reports fail for missing framework dirs', async () => {
169
- mockedFs.pathExists.mockImplementation(async (p: unknown) => {
170
- const s = String(p)
171
- if (s.includes('templates')) return false as never
172
- if (s.includes('manifest.json')) return false as never
173
- // Modules directory for installed modules
174
- if (s.includes('.javi-forge')) return false as never
175
- return true as never
176
- })
177
- mockedFs.readdir.mockResolvedValue(['a'] as never)
178
-
179
- const result = await runDoctor('/test/project')
180
- const structSection = result.sections.find(s => s.title === 'Framework Structure')!
181
- const templatesCheck = structSection.checks.find(c => c.label === 'templates/')
182
- expect(templatesCheck!.status).toBe('fail')
183
- })
184
-
185
- it('shows stack when detected', async () => {
186
- mockedDetectStack.mockResolvedValue({
187
- stackType: 'node',
188
- buildTool: 'pnpm',
189
- })
190
-
191
- const result = await runDoctor('/test/project')
192
- const stackSection = result.sections.find(s => s.title === 'Stack Detection')!
193
- const stackCheck = stackSection.checks[0]
194
- expect(stackCheck.status).toBe('ok')
195
- expect(stackCheck.detail).toContain('node')
196
- expect(stackCheck.detail).toContain('pnpm')
197
- })
198
-
199
- it('countDir filters dotfiles', async () => {
200
- mockedFs.readdir.mockResolvedValue(['.hidden', 'file1', '.git', 'file2'] as never)
201
- mockedFs.pathExists.mockResolvedValue(true as never)
202
-
203
- const result = await runDoctor('/test/project')
204
- const structSection = result.sections.find(s => s.title === 'Framework Structure')!
205
- // Filtered: file1, file2 → 2 entries
206
- expect(structSection.checks[0].detail).toBe('2 entries')
207
- })
208
- })
@@ -1,163 +0,0 @@
1
- import fs from 'fs-extra'
2
- import path from 'path'
3
- import { execFile } from 'child_process'
4
- import { promisify } from 'util'
5
- import { detectStack } from '../lib/common.js'
6
- import { FORGE_ROOT, TEMPLATES_DIR, MODULES_DIR, AI_CONFIG_DIR } from '../constants.js'
7
- import type { DoctorResult, DoctorSection, DoctorCheck, ForgeManifest } from '../types/index.js'
8
-
9
- const execFileAsync = promisify(execFile)
10
-
11
- export type CheckStatus = 'ok' | 'fail' | 'skip'
12
-
13
- /** Resolve a binary name to its full path, returns null if not found */
14
- async function which(bin: string): Promise<string | null> {
15
- try {
16
- const { stdout } = await execFileAsync('which', [bin])
17
- return stdout.trim() || null
18
- } catch {
19
- return null
20
- }
21
- }
22
-
23
- /** Read the forge manifest from a project directory */
24
- async function readManifest(projectDir: string): Promise<ForgeManifest | null> {
25
- const manifestPath = path.join(projectDir, '.javi-forge', 'manifest.json')
26
- if (!await fs.pathExists(manifestPath)) return null
27
- try {
28
- return await fs.readJson(manifestPath) as ForgeManifest
29
- } catch {
30
- return null
31
- }
32
- }
33
-
34
- /** Count entries in a directory */
35
- async function countDir(dir: string): Promise<number> {
36
- if (!await fs.pathExists(dir)) return 0
37
- const entries = await fs.readdir(dir)
38
- return entries.filter(e => !e.startsWith('.')).length
39
- }
40
-
41
- /**
42
- * Run comprehensive health checks for the project and framework.
43
- */
44
- export async function runDoctor(projectDir?: string): Promise<DoctorResult> {
45
- const cwd = projectDir ?? process.cwd()
46
- const sections: DoctorSection[] = []
47
-
48
- // ── 1. System Tools ────────────────────────────────────────────────────────
49
- const toolChecks: DoctorCheck[] = []
50
- const tools = [
51
- { name: 'git', label: 'Git' },
52
- { name: 'docker', label: 'Docker' },
53
- { name: 'semgrep', label: 'Semgrep' },
54
- { name: 'node', label: 'Node.js' },
55
- { name: 'pnpm', label: 'pnpm' },
56
- ]
57
-
58
- for (const tool of tools) {
59
- const bin = await which(tool.name)
60
- if (bin) {
61
- // Try to get version
62
- let version = ''
63
- try {
64
- const { stdout } = await execFileAsync(tool.name, ['--version'])
65
- version = stdout.trim().split('\n')[0] ?? ''
66
- } catch { /* ignore */ }
67
- toolChecks.push({
68
- label: tool.label,
69
- status: 'ok',
70
- detail: version ? `${version}` : `found at ${bin}`,
71
- })
72
- } else {
73
- toolChecks.push({
74
- label: tool.label,
75
- status: tool.name === 'docker' || tool.name === 'semgrep' ? 'skip' : 'fail',
76
- detail: 'not found in PATH',
77
- })
78
- }
79
- }
80
- sections.push({ title: 'System Tools', checks: toolChecks })
81
-
82
- // ── 2. Framework Structure ─────────────────────────────────────────────────
83
- const structureChecks: DoctorCheck[] = []
84
- const expectedDirs = [
85
- { path: TEMPLATES_DIR, label: 'templates/' },
86
- { path: MODULES_DIR, label: 'modules/' },
87
- { path: AI_CONFIG_DIR, label: 'ai-config/' },
88
- { path: path.join(FORGE_ROOT, 'workflows'), label: 'workflows/' },
89
- { path: path.join(FORGE_ROOT, 'schemas'), label: 'schemas/' },
90
- { path: path.join(FORGE_ROOT, 'ci-local'), label: 'ci-local/' },
91
- ]
92
-
93
- for (const dir of expectedDirs) {
94
- if (await fs.pathExists(dir.path)) {
95
- const count = await countDir(dir.path)
96
- structureChecks.push({ label: dir.label, status: 'ok', detail: `${count} entries` })
97
- } else {
98
- structureChecks.push({ label: dir.label, status: 'fail', detail: 'missing' })
99
- }
100
- }
101
- sections.push({ title: 'Framework Structure', checks: structureChecks })
102
-
103
- // ── 3. Stack Detection ─────────────────────────────────────────────────────
104
- const stackChecks: DoctorCheck[] = []
105
- const detection = await detectStack(cwd)
106
- if (detection) {
107
- stackChecks.push({
108
- label: 'Detected stack',
109
- status: 'ok',
110
- detail: `${detection.stackType} (${detection.buildTool})${detection.javaVersion ? ` Java ${detection.javaVersion}` : ''}`,
111
- })
112
- } else {
113
- stackChecks.push({
114
- label: 'Detected stack',
115
- status: 'skip',
116
- detail: 'no recognizable project files in current directory',
117
- })
118
- }
119
- sections.push({ title: 'Stack Detection', checks: stackChecks })
120
-
121
- // ── 4. Project Manifest ────────────────────────────────────────────────────
122
- const manifestChecks: DoctorCheck[] = []
123
- const manifest = await readManifest(cwd)
124
- if (manifest) {
125
- manifestChecks.push({
126
- label: 'Forge manifest',
127
- status: 'ok',
128
- detail: `project: ${manifest.projectName}, stack: ${manifest.stack}`,
129
- })
130
- manifestChecks.push({
131
- label: 'Created',
132
- status: 'ok',
133
- detail: manifest.createdAt.split('T')[0],
134
- })
135
- manifestChecks.push({
136
- label: 'Modules',
137
- status: manifest.modules.length > 0 ? 'ok' : 'skip',
138
- detail: manifest.modules.length > 0 ? manifest.modules.join(', ') : 'none installed',
139
- })
140
- } else {
141
- manifestChecks.push({
142
- label: 'Forge manifest',
143
- status: 'skip',
144
- detail: 'not a forge-managed project (run javi-forge init)',
145
- })
146
- }
147
- sections.push({ title: 'Project Manifest', checks: manifestChecks })
148
-
149
- // ── 5. Installed Modules ───────────────────────────────────────────────────
150
- const moduleChecks: DoctorCheck[] = []
151
- const moduleNames = ['engram', 'obsidian-brain', 'memory-simple', 'ghagga']
152
- for (const mod of moduleNames) {
153
- const modPath = path.join(cwd, '.javi-forge', 'modules', mod)
154
- if (await fs.pathExists(modPath)) {
155
- moduleChecks.push({ label: mod, status: 'ok', detail: 'installed' })
156
- } else {
157
- moduleChecks.push({ label: mod, status: 'skip', detail: 'not installed' })
158
- }
159
- }
160
- sections.push({ title: 'Installed Modules', checks: moduleChecks })
161
-
162
- return { sections }
163
- }