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.
- package/.releaserc +2 -1
- package/README.md +143 -31
- package/ai-config/commands/workflows/diagnose.md +70 -0
- package/ai-config/commands/workflows/discover.md +86 -0
- package/dist/commands/doctor.js +24 -1
- package/dist/commands/init.js +48 -1
- package/dist/commands/llmstxt.d.ts +9 -0
- package/dist/commands/llmstxt.js +93 -0
- package/dist/commands/llmstxt.test.d.ts +2 -0
- package/dist/commands/plugin.d.ts +24 -0
- package/dist/commands/plugin.js +78 -0
- package/dist/commands/plugin.test.d.ts +2 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.js +8 -0
- package/dist/index.js +33 -4
- package/dist/lib/plugin.d.ts +39 -0
- package/dist/lib/plugin.js +228 -0
- package/dist/lib/plugin.test.d.ts +2 -0
- package/dist/types/index.d.ts +42 -0
- package/dist/ui/App.d.ts +2 -1
- package/dist/ui/App.js +2 -1
- package/dist/ui/LlmsTxt.d.ts +8 -0
- package/dist/ui/LlmsTxt.js +48 -0
- package/dist/ui/Plugin.d.ts +9 -0
- package/dist/ui/Plugin.js +96 -0
- package/modules/obsidian-brain/README.md +32 -0
- package/modules/obsidian-brain/core/templates/braindump.md +15 -0
- package/modules/obsidian-brain/core/templates/consolidation.md +42 -0
- package/modules/obsidian-brain/core/templates/daily-note.md +18 -0
- package/modules/obsidian-brain/core/templates/resource-capture.md +14 -0
- package/modules/obsidian-brain/developer/templates/adr.md +40 -0
- package/modules/obsidian-brain/developer/templates/coding-session.md +24 -0
- package/modules/obsidian-brain/developer/templates/debug-journal.md +22 -0
- package/modules/obsidian-brain/developer/templates/sdd-feedback.md +27 -0
- package/modules/obsidian-brain/developer/templates/tech-debt.md +20 -0
- package/modules/obsidian-brain/pm-lead/templates/daily-brief.md +25 -0
- package/modules/obsidian-brain/pm-lead/templates/meeting-notes.md +24 -0
- package/modules/obsidian-brain/pm-lead/templates/risk-registry.md +12 -0
- package/modules/obsidian-brain/pm-lead/templates/sprint-review.md +27 -0
- package/modules/obsidian-brain/pm-lead/templates/stakeholder-update.md +24 -0
- package/modules/obsidian-brain/pm-lead/templates/team-intelligence.md +19 -0
- package/modules/obsidian-brain/pm-lead/templates/weekly-brief.md +29 -0
- package/package.json +1 -1
- package/schemas/plugin.schema.json +62 -0
- package/ai-config/skills/docs/api-documentation/SKILL.md +0 -293
- package/ai-config/skills/docs/docs-spring/SKILL.md +0 -377
- package/ai-config/skills/docs/mustache-templates/SKILL.md +0 -190
- package/ai-config/skills/docs/technical-docs/SKILL.md +0 -447
- package/dist/commands/analyze.d.ts.map +0 -1
- package/dist/commands/analyze.js.map +0 -1
- package/dist/commands/analyze.test.d.ts.map +0 -1
- package/dist/commands/analyze.test.js +0 -145
- package/dist/commands/analyze.test.js.map +0 -1
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/doctor.test.d.ts.map +0 -1
- package/dist/commands/doctor.test.js +0 -200
- package/dist/commands/doctor.test.js.map +0 -1
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/init.test.d.ts.map +0 -1
- package/dist/commands/init.test.js +0 -271
- package/dist/commands/init.test.js.map +0 -1
- package/dist/commands/sync.d.ts.map +0 -1
- package/dist/commands/sync.js.map +0 -1
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js.map +0 -1
- package/dist/e2e/aggressive.e2e.test.d.ts.map +0 -1
- package/dist/e2e/aggressive.e2e.test.js +0 -350
- package/dist/e2e/aggressive.e2e.test.js.map +0 -1
- package/dist/e2e/commands.e2e.test.d.ts.map +0 -1
- package/dist/e2e/commands.e2e.test.js +0 -213
- package/dist/e2e/commands.e2e.test.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/common.d.ts.map +0 -1
- package/dist/lib/common.js.map +0 -1
- package/dist/lib/common.test.d.ts.map +0 -1
- package/dist/lib/common.test.js +0 -316
- package/dist/lib/common.test.js.map +0 -1
- package/dist/lib/frontmatter.d.ts.map +0 -1
- package/dist/lib/frontmatter.js.map +0 -1
- package/dist/lib/frontmatter.test.d.ts.map +0 -1
- package/dist/lib/frontmatter.test.js +0 -257
- package/dist/lib/frontmatter.test.js.map +0 -1
- package/dist/lib/template.d.ts.map +0 -1
- package/dist/lib/template.js.map +0 -1
- package/dist/lib/template.test.d.ts.map +0 -1
- package/dist/lib/template.test.js +0 -201
- package/dist/lib/template.test.js.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/ui/AnalyzeUI.d.ts.map +0 -1
- package/dist/ui/AnalyzeUI.js.map +0 -1
- package/dist/ui/App.d.ts.map +0 -1
- package/dist/ui/App.js.map +0 -1
- package/dist/ui/CIContext.d.ts.map +0 -1
- package/dist/ui/CIContext.js.map +0 -1
- package/dist/ui/CISelector.d.ts.map +0 -1
- package/dist/ui/CISelector.js.map +0 -1
- package/dist/ui/Doctor.d.ts.map +0 -1
- package/dist/ui/Doctor.js.map +0 -1
- package/dist/ui/Header.d.ts.map +0 -1
- package/dist/ui/Header.js.map +0 -1
- package/dist/ui/MemorySelector.d.ts.map +0 -1
- package/dist/ui/MemorySelector.js.map +0 -1
- package/dist/ui/NameInput.d.ts.map +0 -1
- package/dist/ui/NameInput.js.map +0 -1
- package/dist/ui/OptionSelector.d.ts.map +0 -1
- package/dist/ui/OptionSelector.js.map +0 -1
- package/dist/ui/Progress.d.ts.map +0 -1
- package/dist/ui/Progress.js.map +0 -1
- package/dist/ui/StackSelector.d.ts.map +0 -1
- package/dist/ui/StackSelector.js.map +0 -1
- package/dist/ui/Summary.d.ts.map +0 -1
- package/dist/ui/Summary.js.map +0 -1
- package/dist/ui/SyncUI.d.ts.map +0 -1
- package/dist/ui/SyncUI.js.map +0 -1
- package/dist/ui/Welcome.d.ts.map +0 -1
- package/dist/ui/Welcome.js.map +0 -1
- package/dist/ui/theme.d.ts.map +0 -1
- package/dist/ui/theme.js.map +0 -1
- package/modules/obsidian-brain/.obsidian/plugins/dataview/data.json +0 -25
- package/modules/obsidian-brain/.obsidian/plugins/obsidian-kanban/data.json +0 -29
- package/modules/obsidian-brain/.obsidian/plugins/templater-obsidian/data.json +0 -18
- package/src/commands/analyze.test.ts +0 -145
- package/src/commands/analyze.ts +0 -69
- package/src/commands/doctor.test.ts +0 -208
- package/src/commands/doctor.ts +0 -163
- package/src/commands/init.test.ts +0 -298
- package/src/commands/init.ts +0 -285
- package/src/constants.ts +0 -69
- package/src/e2e/aggressive.e2e.test.ts +0 -557
- package/src/e2e/commands.e2e.test.ts +0 -298
- package/src/index.tsx +0 -106
- package/src/lib/common.test.ts +0 -318
- package/src/lib/common.ts +0 -127
- package/src/lib/frontmatter.test.ts +0 -291
- package/src/lib/frontmatter.ts +0 -77
- package/src/lib/template.test.ts +0 -226
- package/src/lib/template.ts +0 -99
- package/src/types/index.ts +0 -53
- package/src/ui/AnalyzeUI.tsx +0 -133
- package/src/ui/App.tsx +0 -175
- package/src/ui/CIContext.tsx +0 -25
- package/src/ui/CISelector.tsx +0 -72
- package/src/ui/Doctor.tsx +0 -122
- package/src/ui/Header.tsx +0 -48
- package/src/ui/MemorySelector.tsx +0 -73
- package/src/ui/NameInput.tsx +0 -82
- package/src/ui/OptionSelector.tsx +0 -100
- package/src/ui/Progress.tsx +0 -88
- package/src/ui/StackSelector.tsx +0 -101
- package/src/ui/Summary.tsx +0 -134
- package/src/ui/Welcome.tsx +0 -54
- package/src/ui/theme.ts +0 -10
- package/stryker.config.json +0 -19
- package/tsconfig.json +0 -19
- 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
|
-
})
|
package/src/commands/analyze.ts
DELETED
|
@@ -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
|
-
})
|
package/src/commands/doctor.ts
DELETED
|
@@ -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
|
-
}
|