@doriandev/devcc 0.1.2 → 0.3.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/README.md +589 -389
- package/bin/devcc.mjs +50 -23
- package/dist/devcc/agents/agents.d.ts +119 -0
- package/dist/devcc/agents/agents.d.ts.map +1 -0
- package/dist/devcc/agents/agents.js +182 -0
- package/dist/devcc/agents/agents.js.map +1 -0
- package/dist/devcc/app/App.d.ts +23 -0
- package/dist/devcc/app/App.d.ts.map +1 -1
- package/dist/devcc/app/App.js +451 -19
- package/dist/devcc/app/App.js.map +1 -1
- package/dist/devcc/app/controlPlaneCommands.d.ts +14 -0
- package/dist/devcc/app/controlPlaneCommands.d.ts.map +1 -0
- package/dist/devcc/app/controlPlaneCommands.js +204 -0
- package/dist/devcc/app/controlPlaneCommands.js.map +1 -0
- package/dist/devcc/app/keymap.d.ts.map +1 -1
- package/dist/devcc/app/keymap.js +25 -3
- package/dist/devcc/app/keymap.js.map +1 -1
- package/dist/devcc/automation.d.ts +52 -0
- package/dist/devcc/automation.d.ts.map +1 -0
- package/dist/devcc/automation.js +339 -0
- package/dist/devcc/automation.js.map +1 -0
- package/dist/devcc/cli.d.ts.map +1 -1
- package/dist/devcc/cli.js +73 -3
- package/dist/devcc/cli.js.map +1 -1
- package/dist/devcc/components/Sidebar.d.ts.map +1 -1
- package/dist/devcc/components/Sidebar.js +1 -0
- package/dist/devcc/components/Sidebar.js.map +1 -1
- package/dist/devcc/components/primitives.d.ts.map +1 -1
- package/dist/devcc/components/primitives.js +3 -0
- package/dist/devcc/components/primitives.js.map +1 -1
- package/dist/devcc/core/commands.d.ts +33 -0
- package/dist/devcc/core/commands.d.ts.map +1 -1
- package/dist/devcc/core/commands.js.map +1 -1
- package/dist/devcc/core/config.d.ts +71 -3
- package/dist/devcc/core/config.d.ts.map +1 -1
- package/dist/devcc/core/config.js +189 -5
- package/dist/devcc/core/config.js.map +1 -1
- package/dist/devcc/core/state.d.ts +19 -1
- package/dist/devcc/core/state.d.ts.map +1 -1
- package/dist/devcc/core/state.js +6 -0
- package/dist/devcc/core/state.js.map +1 -1
- package/dist/devcc/environment/EnvironmentService.d.ts +58 -0
- package/dist/devcc/environment/EnvironmentService.d.ts.map +1 -0
- package/dist/devcc/environment/EnvironmentService.js +159 -0
- package/dist/devcc/environment/EnvironmentService.js.map +1 -0
- package/dist/devcc/environment/integrations.d.ts +52 -0
- package/dist/devcc/environment/integrations.d.ts.map +1 -0
- package/dist/devcc/environment/integrations.js +94 -0
- package/dist/devcc/environment/integrations.js.map +1 -0
- package/dist/devcc/environment/tools.d.ts +51 -0
- package/dist/devcc/environment/tools.d.ts.map +1 -0
- package/dist/devcc/environment/tools.js +99 -0
- package/dist/devcc/environment/tools.js.map +1 -0
- package/dist/devcc/git/GitService.d.ts +26 -0
- package/dist/devcc/git/GitService.d.ts.map +1 -1
- package/dist/devcc/git/GitService.js +71 -2
- package/dist/devcc/git/GitService.js.map +1 -1
- package/dist/devcc/git/worktrees.d.ts +88 -0
- package/dist/devcc/git/worktrees.d.ts.map +1 -0
- package/dist/devcc/git/worktrees.js +226 -0
- package/dist/devcc/git/worktrees.js.map +1 -0
- package/dist/devcc/index.d.ts +15 -3
- package/dist/devcc/index.d.ts.map +1 -1
- package/dist/devcc/index.js +14 -2
- package/dist/devcc/index.js.map +1 -1
- package/dist/devcc/init/detect.d.ts +55 -0
- package/dist/devcc/init/detect.d.ts.map +1 -0
- package/dist/devcc/init/detect.js +207 -0
- package/dist/devcc/init/detect.js.map +1 -0
- package/dist/devcc/init/prompt.d.ts +25 -0
- package/dist/devcc/init/prompt.d.ts.map +1 -0
- package/dist/devcc/init/prompt.js +156 -0
- package/dist/devcc/init/prompt.js.map +1 -0
- package/dist/devcc/platform/launch.d.ts +30 -0
- package/dist/devcc/platform/launch.d.ts.map +1 -0
- package/dist/devcc/platform/launch.js +93 -0
- package/dist/devcc/platform/launch.js.map +1 -0
- package/dist/devcc/services/ServiceManager.d.ts +10 -2
- package/dist/devcc/services/ServiceManager.d.ts.map +1 -1
- package/dist/devcc/services/ServiceManager.js +18 -4
- package/dist/devcc/services/ServiceManager.js.map +1 -1
- package/dist/devcc/services/node.d.ts +11 -0
- package/dist/devcc/services/node.d.ts.map +1 -1
- package/dist/devcc/services/node.js +20 -18
- package/dist/devcc/services/node.js.map +1 -1
- package/dist/devcc/services/ollama.d.ts +64 -5
- package/dist/devcc/services/ollama.d.ts.map +1 -1
- package/dist/devcc/services/ollama.js +118 -17
- package/dist/devcc/services/ollama.js.map +1 -1
- package/dist/devcc/services/remote.d.ts +90 -0
- package/dist/devcc/services/remote.d.ts.map +1 -0
- package/dist/devcc/services/remote.js +233 -0
- package/dist/devcc/services/remote.js.map +1 -0
- package/dist/devcc/services/types.d.ts +8 -1
- package/dist/devcc/services/types.d.ts.map +1 -1
- package/dist/devcc/services/types.js.map +1 -1
- package/dist/devcc/utils/exec.d.ts +6 -0
- package/dist/devcc/utils/exec.d.ts.map +1 -1
- package/dist/devcc/utils/exec.js +12 -2
- package/dist/devcc/utils/exec.js.map +1 -1
- package/dist/devcc/verify/verify.d.ts +107 -0
- package/dist/devcc/verify/verify.d.ts.map +1 -0
- package/dist/devcc/verify/verify.js +203 -0
- package/dist/devcc/verify/verify.js.map +1 -0
- package/dist/devcc/views/OverviewView.d.ts.map +1 -1
- package/dist/devcc/views/OverviewView.js +58 -2
- package/dist/devcc/views/OverviewView.js.map +1 -1
- package/dist/devcc/views/ServicesView.d.ts +2 -0
- package/dist/devcc/views/ServicesView.d.ts.map +1 -1
- package/dist/devcc/views/ServicesView.js +15 -0
- package/dist/devcc/views/ServicesView.js.map +1 -1
- package/dist/devcc/views/SystemView.d.ts +10 -1
- package/dist/devcc/views/SystemView.d.ts.map +1 -1
- package/dist/devcc/views/SystemView.js +127 -19
- package/dist/devcc/views/SystemView.js.map +1 -1
- package/dist/devcc/views/WorktreesView.d.ts +48 -0
- package/dist/devcc/views/WorktreesView.d.ts.map +1 -0
- package/dist/devcc/views/WorktreesView.js +235 -0
- package/dist/devcc/views/WorktreesView.js.map +1 -0
- package/package.json +1 -1
- package/src/devcc/agents/agents.ts +292 -0
- package/src/devcc/app/App.ts +529 -22
- package/src/devcc/app/controlPlaneCommands.ts +227 -0
- package/src/devcc/app/keymap.ts +25 -3
- package/src/devcc/automation.ts +409 -0
- package/src/devcc/cli.ts +77 -3
- package/src/devcc/components/Sidebar.ts +1 -0
- package/src/devcc/components/primitives.ts +3 -0
- package/src/devcc/core/commands.ts +34 -0
- package/src/devcc/core/config.ts +275 -11
- package/src/devcc/core/state.ts +25 -0
- package/src/devcc/environment/EnvironmentService.ts +215 -0
- package/src/devcc/environment/integrations.ts +133 -0
- package/src/devcc/environment/tools.ts +151 -0
- package/src/devcc/git/GitService.ts +104 -2
- package/src/devcc/git/worktrees.ts +256 -0
- package/src/devcc/index.ts +66 -2
- package/src/devcc/init/detect.ts +268 -0
- package/src/devcc/init/prompt.ts +179 -0
- package/src/devcc/platform/launch.ts +125 -0
- package/src/devcc/services/ServiceManager.ts +15 -3
- package/src/devcc/services/node.ts +17 -14
- package/src/devcc/services/ollama.ts +162 -19
- package/src/devcc/services/remote.ts +306 -0
- package/src/devcc/services/types.ts +9 -1
- package/src/devcc/utils/exec.ts +26 -2
- package/src/devcc/verify/verify.ts +316 -0
- package/src/devcc/views/OverviewView.ts +84 -2
- package/src/devcc/views/ServicesView.ts +17 -0
- package/src/devcc/views/SystemView.ts +144 -22
- package/src/devcc/views/WorktreesView.ts +270 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
import { writeFileSync } from "node:fs"
|
|
3
|
+
import { basename, join } from "node:path"
|
|
4
|
+
import { createAgentAdapters } from "./agents/agents.js"
|
|
5
|
+
import { loadConfig, loadConfigFile, type LoadedConfig } from "./core/config.js"
|
|
6
|
+
import { EnvironmentService, type EnvironmentSnapshot } from "./environment/EnvironmentService.js"
|
|
7
|
+
import { GitService } from "./git/GitService.js"
|
|
8
|
+
import { worktreeLabel } from "./git/worktrees.js"
|
|
9
|
+
import { detectProject, renderStarterConfig, starterConfig } from "./init/detect.js"
|
|
10
|
+
import { buildSetupPrompt } from "./init/prompt.js"
|
|
11
|
+
import { errorMessage } from "./utils/errors.js"
|
|
12
|
+
import { pluralise } from "./utils/format.js"
|
|
13
|
+
import { createLogger } from "./utils/logger.js"
|
|
14
|
+
import { detectPackageManager, readPackageJson, readScripts } from "./utils/packageManager.js"
|
|
15
|
+
import { tildify } from "./utils/paths.js"
|
|
16
|
+
import {
|
|
17
|
+
createSpawnExecutor,
|
|
18
|
+
isVerifyIssue,
|
|
19
|
+
resolveVerifySteps,
|
|
20
|
+
runVerification,
|
|
21
|
+
summariseVerify,
|
|
22
|
+
type StepExecutor,
|
|
23
|
+
type VerifyReport,
|
|
24
|
+
} from "./verify/verify.js"
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Non-interactive commands for scripts and AI agents: `devcc verify`,
|
|
28
|
+
* `devcc worktree list`, `devcc env`. None of them load the TUI, so they run on
|
|
29
|
+
* plain Node without FFI. With `--json`, stdout carries exactly one JSON
|
|
30
|
+
* document and nothing else; diagnostics go to stderr.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export interface AutomationIo {
|
|
34
|
+
stdout: (text: string) => void
|
|
35
|
+
stderr: (text: string) => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AutomationOptions {
|
|
39
|
+
rootDir: string
|
|
40
|
+
json: boolean
|
|
41
|
+
io?: AutomationIo
|
|
42
|
+
/** Injected for tests; defaults to spawning each step. */
|
|
43
|
+
executor?: StepExecutor
|
|
44
|
+
signal?: AbortSignal
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const EXIT_OK = 0
|
|
48
|
+
export const EXIT_FAILED = 1
|
|
49
|
+
/** Misconfiguration or misuse: nothing was verified. */
|
|
50
|
+
export const EXIT_USAGE = 2
|
|
51
|
+
|
|
52
|
+
const processIo: AutomationIo = {
|
|
53
|
+
stdout: (text) => process.stdout.write(text),
|
|
54
|
+
stderr: (text) => process.stderr.write(text),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function printJson(io: AutomationIo, value: unknown): void {
|
|
58
|
+
io.stdout(`${JSON.stringify(value, null, 2)}\n`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// devcc verify
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
export async function verifyCommand(options: AutomationOptions): Promise<number> {
|
|
66
|
+
const io = options.io ?? processIo
|
|
67
|
+
const { rootDir, json } = options
|
|
68
|
+
|
|
69
|
+
const packageJson = readPackageJson(rootDir)
|
|
70
|
+
const packageManager = detectPackageManager({ rootDir, packageJson, userAgent: undefined }).manager
|
|
71
|
+
const loaded = await loadConfig(rootDir)
|
|
72
|
+
const { steps, issues } = resolveVerifySteps(loaded.config.verify, {
|
|
73
|
+
commands: loaded.config.commands,
|
|
74
|
+
scripts: readScripts(packageJson),
|
|
75
|
+
packageManager,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// A dropped or unresolvable verify entry must fail the gate: silently running
|
|
79
|
+
// fewer checks than configured would let a broken project pass.
|
|
80
|
+
const blocking = [
|
|
81
|
+
...loaded.issues.filter((issue) => issue.startsWith("Failed to load") || isVerifyIssue(issue)),
|
|
82
|
+
...issues,
|
|
83
|
+
]
|
|
84
|
+
if (steps.length === 0 && blocking.length === 0) {
|
|
85
|
+
blocking.push(
|
|
86
|
+
loaded.filePath
|
|
87
|
+
? `No verification steps: add verify: ["lint", "test"] to ${tildify(loaded.filePath)}`
|
|
88
|
+
: 'No verification steps: create .devcc.ts with verify: ["lint", "test"]',
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
if (blocking.length > 0) {
|
|
92
|
+
if (json) printJson(io, { ok: false, error: "configuration", issues: blocking })
|
|
93
|
+
else for (const issue of blocking) io.stderr(`devcc verify: ${issue}\n`)
|
|
94
|
+
return EXIT_USAGE
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!json) io.stdout(`devcc verify · ${tildify(rootDir)} · ${steps.length} step(s)\n\n`)
|
|
98
|
+
|
|
99
|
+
const executor = options.executor ?? createSpawnExecutor({ rootDir })
|
|
100
|
+
let printed = 0
|
|
101
|
+
const report = await runVerification(steps, executor, {
|
|
102
|
+
signal: options.signal,
|
|
103
|
+
onProgress: (progress) => {
|
|
104
|
+
if (json) return
|
|
105
|
+
for (; printed < progress.length; printed += 1) {
|
|
106
|
+
const step = progress[printed]
|
|
107
|
+
if (!step || step.status === "pending") break
|
|
108
|
+
if (step.status === "running") {
|
|
109
|
+
io.stdout(`→ ${step.name}: ${[step.command, ...step.args].join(" ")}\n`)
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
io.stdout(stepLine(step))
|
|
113
|
+
if (step.status === "failed") io.stdout(failureOutput(step))
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
if (json) {
|
|
119
|
+
printJson(io, { ...report, rootDir, summary: summariseVerify(report) })
|
|
120
|
+
} else {
|
|
121
|
+
for (const step of report.steps.slice(printed)) if (step.status === "skipped") io.stdout(stepLine(step))
|
|
122
|
+
io.stdout(`\n${report.ok ? "✓" : "✗"} ${summariseVerify(report)} in ${seconds(report.durationMs)}\n`)
|
|
123
|
+
}
|
|
124
|
+
return report.ok ? EXIT_OK : EXIT_FAILED
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function stepLine(step: VerifyReport["steps"][number]): string {
|
|
128
|
+
const mark = step.status === "passed" ? "✓" : step.status === "skipped" ? "-" : "✗"
|
|
129
|
+
const detail =
|
|
130
|
+
step.status === "skipped"
|
|
131
|
+
? "skipped"
|
|
132
|
+
: [
|
|
133
|
+
seconds(step.durationMs),
|
|
134
|
+
step.exitCode !== null && step.exitCode !== 0 ? `exit ${step.exitCode}` : undefined,
|
|
135
|
+
step.error,
|
|
136
|
+
]
|
|
137
|
+
.filter(Boolean)
|
|
138
|
+
.join(" · ")
|
|
139
|
+
return `${mark} ${step.name} (${detail})\n`
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The tail of a failed step's output, indented under it. */
|
|
143
|
+
function failureOutput(step: VerifyReport["steps"][number], maxLines = 60): string {
|
|
144
|
+
const text = [step.stdout, step.stderr]
|
|
145
|
+
.filter((part) => part.trim() !== "")
|
|
146
|
+
.join("\n")
|
|
147
|
+
.trimEnd()
|
|
148
|
+
if (text === "") return ""
|
|
149
|
+
const lines = text.split("\n")
|
|
150
|
+
const shown = lines.slice(-maxLines)
|
|
151
|
+
const header = lines.length > maxLines ? [`… ${lines.length - maxLines} earlier line(s) omitted`] : []
|
|
152
|
+
return `${[...header, ...shown].map((line) => ` ${line}`).join("\n")}\n`
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function seconds(ms: number): string {
|
|
156
|
+
return `${(ms / 1000).toFixed(1)}s`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// devcc worktree list
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
export async function worktreeListCommand(options: AutomationOptions): Promise<number> {
|
|
164
|
+
const io = options.io ?? processIo
|
|
165
|
+
const git = new GitService({ rootDir: options.rootDir, logger: createLogger(options.rootDir).child("git") })
|
|
166
|
+
try {
|
|
167
|
+
const worktrees = await git.listWorktrees()
|
|
168
|
+
if (options.json) {
|
|
169
|
+
printJson(io, { rootDir: options.rootDir, worktrees })
|
|
170
|
+
return EXIT_OK
|
|
171
|
+
}
|
|
172
|
+
const width = Math.max(...worktrees.map((worktree) => worktreeLabel(worktree).length), 6)
|
|
173
|
+
for (const worktree of worktrees) {
|
|
174
|
+
const flags = [
|
|
175
|
+
worktree.isMain ? "main" : undefined,
|
|
176
|
+
worktree.locked ? "locked" : undefined,
|
|
177
|
+
worktree.prunable ? "prunable" : undefined,
|
|
178
|
+
].filter(Boolean)
|
|
179
|
+
io.stdout(
|
|
180
|
+
`${worktree.isCurrent ? "*" : " "} ${worktreeLabel(worktree).padEnd(width)} ${tildify(worktree.path)}${
|
|
181
|
+
flags.length > 0 ? ` [${flags.join(", ")}]` : ""
|
|
182
|
+
}\n`,
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
return EXIT_OK
|
|
186
|
+
} catch (error) {
|
|
187
|
+
const message = errorMessage(error)
|
|
188
|
+
if (options.json) printJson(io, { error: message, rootDir: options.rootDir, worktrees: [] })
|
|
189
|
+
else io.stderr(`devcc worktree list: ${message}\n`)
|
|
190
|
+
return EXIT_FAILED
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// devcc env
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
export async function envCommand(
|
|
199
|
+
options: AutomationOptions & { environment?: EnvironmentService },
|
|
200
|
+
): Promise<number> {
|
|
201
|
+
const io = options.io ?? processIo
|
|
202
|
+
const environment = options.environment ?? new EnvironmentService({ rootDir: options.rootDir })
|
|
203
|
+
const snapshot = await environment.refresh()
|
|
204
|
+
if (options.json) {
|
|
205
|
+
const { checking: _checking, ...rest } = snapshot
|
|
206
|
+
printJson(io, { rootDir: options.rootDir, ...rest })
|
|
207
|
+
} else {
|
|
208
|
+
io.stdout(formatEnvironment(snapshot))
|
|
209
|
+
}
|
|
210
|
+
return EXIT_OK
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function formatEnvironment(snapshot: EnvironmentSnapshot): string {
|
|
214
|
+
const lines: string[] = ["TOOLS"]
|
|
215
|
+
const nameWidth = Math.max(...snapshot.tools.map((tool) => tool.name.length), 4) + 2
|
|
216
|
+
for (const tool of snapshot.tools) {
|
|
217
|
+
const mark = !tool.installed ? "·" : tool.status === "ready" ? "✓" : tool.status === "warning" ? "⚠" : "✗"
|
|
218
|
+
const value = tool.installed ? (tool.version ?? "installed") : "not installed"
|
|
219
|
+
const note = tool.installed && tool.message ? ` ${tool.message}` : ""
|
|
220
|
+
lines.push(` ${mark} ${tool.name.padEnd(nameWidth)}${value}${note}`)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (snapshot.mise.installed) {
|
|
224
|
+
lines.push("", "RUNTIMES (mise)")
|
|
225
|
+
if (snapshot.mise.error) lines.push(` ⚠ ${snapshot.mise.error}`)
|
|
226
|
+
else if (snapshot.mise.runtimes.length === 0) lines.push(" no tools pinned for this project")
|
|
227
|
+
for (const runtime of snapshot.mise.runtimes) {
|
|
228
|
+
lines.push(
|
|
229
|
+
` ${runtime.tool.padEnd(nameWidth + 2)}${runtime.version}${runtime.installed ? "" : " (not installed)"}`,
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const { direnv } = snapshot
|
|
235
|
+
if (direnv.installed || direnv.rcPath) {
|
|
236
|
+
lines.push("", "DIRENV")
|
|
237
|
+
if (direnv.error) lines.push(` ⚠ ${direnv.error}`)
|
|
238
|
+
if (direnv.rcPath) {
|
|
239
|
+
lines.push(` .envrc ${tildify(direnv.rcPath)}`)
|
|
240
|
+
if (direnv.installed) {
|
|
241
|
+
lines.push(` approval ${direnv.approval}`)
|
|
242
|
+
lines.push(` loaded ${direnv.loaded ? "yes" : "no"}`)
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
lines.push(" no .envrc for this project")
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return `${lines.join("\n")}\n`
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// devcc config check
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
/** Everything wrong with a loaded config, including verify names that resolve to nothing. */
|
|
256
|
+
export function configProblems(rootDir: string, loaded: LoadedConfig): string[] {
|
|
257
|
+
const packageJson = readPackageJson(rootDir)
|
|
258
|
+
const { issues } = resolveVerifySteps(loaded.config.verify, {
|
|
259
|
+
commands: loaded.config.commands,
|
|
260
|
+
scripts: readScripts(packageJson),
|
|
261
|
+
packageManager: detectPackageManager({ rootDir, packageJson }).manager,
|
|
262
|
+
})
|
|
263
|
+
return [...loaded.issues, ...issues]
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function configSummary(loaded: LoadedConfig) {
|
|
267
|
+
const { config } = loaded
|
|
268
|
+
return {
|
|
269
|
+
processes: config.processes.length,
|
|
270
|
+
commands: config.commands.length,
|
|
271
|
+
verify: config.verify.length,
|
|
272
|
+
services: config.services.length,
|
|
273
|
+
remoteMachines: config.remoteMachines.length,
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function configCheckCommand(options: AutomationOptions): Promise<number> {
|
|
278
|
+
const io = options.io ?? processIo
|
|
279
|
+
const loaded = await loadConfig(options.rootDir)
|
|
280
|
+
if (!loaded.filePath) {
|
|
281
|
+
const issue = "No devcc config found (run `devcc init` to create .devcc.ts)"
|
|
282
|
+
if (options.json) printJson(io, { ok: false, file: null, issues: [issue] })
|
|
283
|
+
else io.stderr(`devcc config check: ${issue}\n`)
|
|
284
|
+
return EXIT_FAILED
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const problems = configProblems(options.rootDir, loaded)
|
|
288
|
+
const summary = configSummary(loaded)
|
|
289
|
+
if (options.json) {
|
|
290
|
+
printJson(io, { ok: problems.length === 0, file: loaded.filePath, issues: problems, summary })
|
|
291
|
+
} else if (problems.length > 0) {
|
|
292
|
+
io.stderr(`✗ ${basename(loaded.filePath)} has ${problems.length} issue(s):\n`)
|
|
293
|
+
for (const problem of problems) io.stderr(` - ${problem}\n`)
|
|
294
|
+
} else {
|
|
295
|
+
const labels: Record<keyof typeof summary, [string, string]> = {
|
|
296
|
+
processes: ["process", "processes"],
|
|
297
|
+
commands: ["command", "commands"],
|
|
298
|
+
verify: ["verify step", "verify steps"],
|
|
299
|
+
services: ["service", "services"],
|
|
300
|
+
remoteMachines: ["remote machine", "remote machines"],
|
|
301
|
+
}
|
|
302
|
+
const parts = (Object.entries(summary) as [keyof typeof summary, number][])
|
|
303
|
+
.filter(([, count]) => count > 0)
|
|
304
|
+
.map(([field, count]) => pluralise(count, ...labels[field]))
|
|
305
|
+
io.stdout(`✓ ${basename(loaded.filePath)} is valid${parts.length > 0 ? `: ${parts.join(", ")}` : ""}\n`)
|
|
306
|
+
}
|
|
307
|
+
return problems.length === 0 ? EXIT_OK : EXIT_FAILED
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
// devcc init
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
export interface InitOptions extends AutomationOptions {
|
|
315
|
+
/** Replace an existing .devcc.ts with the starter. */
|
|
316
|
+
force?: boolean
|
|
317
|
+
/** Print the agent setup prompt instead of writing anything. */
|
|
318
|
+
printPrompt?: boolean
|
|
319
|
+
/** Start this agent interactively with the setup prompt. */
|
|
320
|
+
agent?: string
|
|
321
|
+
/** Injected for tests. */
|
|
322
|
+
isTTY?: boolean
|
|
323
|
+
runInteractive?: (command: string, args: string[], cwd: string) => Promise<number>
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function runInteractive(command: string, args: string[], cwd: string): Promise<number> {
|
|
327
|
+
return new Promise((resolve) => {
|
|
328
|
+
const child = spawn(command, args, { cwd, stdio: "inherit" })
|
|
329
|
+
child.once("error", (error) => {
|
|
330
|
+
process.stderr.write(`devcc init: could not start ${command}: ${error.message}\n`)
|
|
331
|
+
resolve(EXIT_FAILED)
|
|
332
|
+
})
|
|
333
|
+
child.once("exit", (code) => resolve(code ?? EXIT_FAILED))
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* `devcc init`: write a starter .devcc.ts from what can be detected.
|
|
339
|
+
* `--prompt` prints the agent setup prompt; `--agent <id>` opens that agent
|
|
340
|
+
* interactively with it, so the user watches and approves its edits.
|
|
341
|
+
*/
|
|
342
|
+
export async function initCommand(options: InitOptions): Promise<number> {
|
|
343
|
+
const io = options.io ?? processIo
|
|
344
|
+
const { rootDir } = options
|
|
345
|
+
const facts = detectProject(rootDir)
|
|
346
|
+
|
|
347
|
+
if (options.printPrompt) {
|
|
348
|
+
io.stdout(buildSetupPrompt(facts, "write"))
|
|
349
|
+
return EXIT_OK
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (options.agent) {
|
|
353
|
+
const loaded = await loadConfig(rootDir)
|
|
354
|
+
const adapters = createAgentAdapters(loaded.config.agents)
|
|
355
|
+
const adapter = adapters.find((candidate) => candidate.id === options.agent)
|
|
356
|
+
if (!adapter?.interactiveArgs) {
|
|
357
|
+
const available = adapters
|
|
358
|
+
.filter((candidate) => candidate.interactiveArgs)
|
|
359
|
+
.map((candidate) => candidate.id)
|
|
360
|
+
io.stderr(`devcc init: --agent must be one of: ${available.join(", ")}\n`)
|
|
361
|
+
return EXIT_USAGE
|
|
362
|
+
}
|
|
363
|
+
if (!(options.isTTY ?? process.stdout.isTTY)) {
|
|
364
|
+
io.stderr(
|
|
365
|
+
"devcc init --agent needs an interactive terminal. Use `devcc init --prompt` to get the prompt instead.\n",
|
|
366
|
+
)
|
|
367
|
+
return EXIT_USAGE
|
|
368
|
+
}
|
|
369
|
+
if (!(await adapter.detect())) {
|
|
370
|
+
io.stderr(`devcc init: ${adapter.name} is not installed (\`${adapter.command}\` was not found)\n`)
|
|
371
|
+
return EXIT_FAILED
|
|
372
|
+
}
|
|
373
|
+
io.stdout(`Starting ${adapter.name} to set up devcc for ${facts.name}…\n`)
|
|
374
|
+
const run = options.runInteractive ?? runInteractive
|
|
375
|
+
return run(adapter.command, adapter.interactiveArgs(buildSetupPrompt(facts, "write")), rootDir)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (facts.configPath && !options.force) {
|
|
379
|
+
io.stderr(
|
|
380
|
+
`devcc init: ${basename(facts.configPath)} already exists.\n` +
|
|
381
|
+
" Improve it with an agent: devcc init --agent claude (or codex)\n" +
|
|
382
|
+
" Replace it with a starter: devcc init --force\n",
|
|
383
|
+
)
|
|
384
|
+
return EXIT_USAGE
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const target = join(rootDir, ".devcc.ts")
|
|
388
|
+
writeFileSync(target, renderStarterConfig(facts), "utf8")
|
|
389
|
+
const loaded = await loadConfigFile(target)
|
|
390
|
+
const problems = configProblems(rootDir, loaded)
|
|
391
|
+
const starter = starterConfig(facts)
|
|
392
|
+
|
|
393
|
+
io.stdout(`Created ${tildify(target)}\n`)
|
|
394
|
+
if (facts.configPath && facts.configPath !== target) {
|
|
395
|
+
io.stdout(` note: .devcc.ts takes precedence over ${basename(facts.configPath)}, which is now ignored\n`)
|
|
396
|
+
}
|
|
397
|
+
io.stdout(
|
|
398
|
+
` processes: ${starter.processes.map((entry) => entry.name).join(", ") || "none detected"}\n` +
|
|
399
|
+
` verify: ${starter.verify.map((entry) => (typeof entry === "string" ? entry : entry.id)).join(", ") || "none detected"}\n`,
|
|
400
|
+
)
|
|
401
|
+
for (const problem of problems) io.stderr(` ⚠ ${problem}\n`)
|
|
402
|
+
io.stdout(
|
|
403
|
+
"\nNext:\n" +
|
|
404
|
+
" devcc init --agent claude let an agent read the project and refine it (or codex)\n" +
|
|
405
|
+
" devcc init --prompt print that prompt for any other agent\n" +
|
|
406
|
+
" devcc config check validate after editing\n",
|
|
407
|
+
)
|
|
408
|
+
return problems.length === 0 ? EXIT_OK : EXIT_FAILED
|
|
409
|
+
}
|
package/src/devcc/cli.ts
CHANGED
|
@@ -8,8 +8,16 @@
|
|
|
8
8
|
* bypassed it.
|
|
9
9
|
*/
|
|
10
10
|
import { createRequire } from "node:module"
|
|
11
|
+
import { resolve } from "node:path"
|
|
11
12
|
import { pathToFileURL } from "node:url"
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
configCheckCommand,
|
|
15
|
+
envCommand,
|
|
16
|
+
EXIT_USAGE,
|
|
17
|
+
initCommand,
|
|
18
|
+
verifyCommand,
|
|
19
|
+
worktreeListCommand,
|
|
20
|
+
} from "./automation.js"
|
|
13
21
|
import { errorMessage } from "./utils/errors.js"
|
|
14
22
|
import { findProjectRoot } from "./utils/paths.js"
|
|
15
23
|
|
|
@@ -17,6 +25,12 @@ interface CliArgs {
|
|
|
17
25
|
cwd?: string
|
|
18
26
|
help: boolean
|
|
19
27
|
version: boolean
|
|
28
|
+
json: boolean
|
|
29
|
+
force: boolean
|
|
30
|
+
prompt: boolean
|
|
31
|
+
agent?: string
|
|
32
|
+
/** Subcommand and its arguments, e.g. `["worktree", "list"]`. */
|
|
33
|
+
positionals: string[]
|
|
20
34
|
}
|
|
21
35
|
|
|
22
36
|
/**
|
|
@@ -37,11 +51,25 @@ function readVersion(): string {
|
|
|
37
51
|
const VERSION = readVersion()
|
|
38
52
|
|
|
39
53
|
function parseArgs(argv: readonly string[]): CliArgs {
|
|
40
|
-
const args: CliArgs = {
|
|
54
|
+
const args: CliArgs = {
|
|
55
|
+
help: false,
|
|
56
|
+
version: false,
|
|
57
|
+
json: false,
|
|
58
|
+
force: false,
|
|
59
|
+
prompt: false,
|
|
60
|
+
positionals: [],
|
|
61
|
+
}
|
|
41
62
|
for (let index = 0; index < argv.length; index += 1) {
|
|
42
63
|
const arg = argv[index]
|
|
43
64
|
if (arg === "--help" || arg === "-h") args.help = true
|
|
44
65
|
else if (arg === "--version" || arg === "-v") args.version = true
|
|
66
|
+
else if (arg === "--json") args.json = true
|
|
67
|
+
else if (arg === "--force") args.force = true
|
|
68
|
+
else if (arg === "--prompt") args.prompt = true
|
|
69
|
+
else if (arg === "--agent") {
|
|
70
|
+
args.agent = argv[index + 1]
|
|
71
|
+
index += 1
|
|
72
|
+
} else if (arg?.startsWith("--agent=")) args.agent = arg.slice("--agent=".length)
|
|
45
73
|
else if (arg === "--cwd" || arg === "-C") {
|
|
46
74
|
const value = argv[index + 1]
|
|
47
75
|
if (value) {
|
|
@@ -50,6 +78,8 @@ function parseArgs(argv: readonly string[]): CliArgs {
|
|
|
50
78
|
}
|
|
51
79
|
} else if (arg?.startsWith("--cwd=")) {
|
|
52
80
|
args.cwd = arg.slice("--cwd=".length)
|
|
81
|
+
} else if (arg !== undefined && !arg.startsWith("-")) {
|
|
82
|
+
args.positionals.push(arg)
|
|
53
83
|
}
|
|
54
84
|
}
|
|
55
85
|
return args
|
|
@@ -58,13 +88,25 @@ function parseArgs(argv: readonly string[]): CliArgs {
|
|
|
58
88
|
const USAGE = `dev command center ${VERSION}
|
|
59
89
|
|
|
60
90
|
Usage:
|
|
61
|
-
devcc [options]
|
|
91
|
+
devcc [options] Open the dashboard
|
|
92
|
+
devcc verify [--json] Run the project's verify steps; exit 0 only if all pass
|
|
93
|
+
devcc worktree list [--json] List git worktrees
|
|
94
|
+
devcc env [--json] Report tools, mise runtimes and direnv status
|
|
95
|
+
devcc init Write a starter .devcc.ts from what is detected
|
|
96
|
+
devcc init --agent <claude|codex> Let an agent inspect the project and set up .devcc.ts
|
|
97
|
+
devcc init --prompt Print that setup prompt for any other agent
|
|
98
|
+
devcc config check [--json] Validate the project's devcc config
|
|
62
99
|
|
|
63
100
|
Options:
|
|
64
101
|
-C, --cwd <dir> Project directory (default: nearest project root)
|
|
102
|
+
--json Machine-readable output (verify, worktree list, env, config check)
|
|
103
|
+
--force init: replace an existing .devcc.ts
|
|
65
104
|
-h, --help Show this help
|
|
66
105
|
-v, --version Show the version
|
|
67
106
|
|
|
107
|
+
Exit codes (verify):
|
|
108
|
+
0 every step passed 1 a step failed 2 nothing to verify / bad config
|
|
109
|
+
|
|
68
110
|
Environment:
|
|
69
111
|
DEVCC_LOG_LEVEL debug | info | warn | error | silent (default: info)
|
|
70
112
|
|
|
@@ -84,11 +126,28 @@ export async function main(argv: readonly string[] = process.argv.slice(2)): Pro
|
|
|
84
126
|
return 0
|
|
85
127
|
}
|
|
86
128
|
|
|
129
|
+
const [command, ...rest] = args.positionals
|
|
130
|
+
if (command !== undefined) {
|
|
131
|
+
const rootDir = args.cwd ? resolve(args.cwd) : findProjectRoot()
|
|
132
|
+
const options = { rootDir, json: args.json }
|
|
133
|
+
if (command === "verify" && rest.length === 0) return runVerifyCli(options)
|
|
134
|
+
if (command === "worktree" && rest.length === 1 && rest[0] === "list") return worktreeListCommand(options)
|
|
135
|
+
if (command === "env" && rest.length === 0) return envCommand(options)
|
|
136
|
+
if (command === "init" && rest.length === 0) {
|
|
137
|
+
return initCommand({ ...options, force: args.force, printPrompt: args.prompt, agent: args.agent })
|
|
138
|
+
}
|
|
139
|
+
if (command === "config" && rest.length === 1 && rest[0] === "check") return configCheckCommand(options)
|
|
140
|
+
process.stderr.write(`devcc: unknown command "${args.positionals.join(" ")}"\n\n${USAGE}`)
|
|
141
|
+
return EXIT_USAGE
|
|
142
|
+
}
|
|
143
|
+
|
|
87
144
|
if (!process.stdout.isTTY) {
|
|
88
145
|
process.stderr.write("devcc needs an interactive terminal (stdout is not a TTY).\n")
|
|
89
146
|
return 1
|
|
90
147
|
}
|
|
91
148
|
|
|
149
|
+
// Loaded lazily: OpenTUI needs FFI, which the automation commands above do not.
|
|
150
|
+
const { App } = await import("./app/App.js")
|
|
92
151
|
const app = new App({ cwd: args.cwd ? args.cwd : findProjectRoot() })
|
|
93
152
|
|
|
94
153
|
try {
|
|
@@ -107,6 +166,21 @@ export async function main(argv: readonly string[] = process.argv.slice(2)): Pro
|
|
|
107
166
|
}
|
|
108
167
|
}
|
|
109
168
|
|
|
169
|
+
/** Ctrl+C stops the running step instead of leaving it orphaned. */
|
|
170
|
+
async function runVerifyCli(options: { rootDir: string; json: boolean }): Promise<number> {
|
|
171
|
+
const controller = new AbortController()
|
|
172
|
+
const onSignal = () => controller.abort()
|
|
173
|
+
process.once("SIGINT", onSignal)
|
|
174
|
+
process.once("SIGTERM", onSignal)
|
|
175
|
+
try {
|
|
176
|
+
const code = await verifyCommand({ ...options, signal: controller.signal })
|
|
177
|
+
return controller.signal.aborted ? 130 : code
|
|
178
|
+
} finally {
|
|
179
|
+
process.off("SIGINT", onSignal)
|
|
180
|
+
process.off("SIGTERM", onSignal)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
110
184
|
const entry = process.argv[1]
|
|
111
185
|
const isDirectRun = entry !== undefined && import.meta.url === pathToFileURL(entry).href
|
|
112
186
|
|
|
@@ -16,6 +16,7 @@ export const SIDEBAR_ITEMS: readonly SidebarItem[] = [
|
|
|
16
16
|
{ id: "services", label: "Services" },
|
|
17
17
|
{ id: "scripts", label: "Scripts" },
|
|
18
18
|
{ id: "git", label: "Git" },
|
|
19
|
+
{ id: "worktrees", label: "Worktrees" },
|
|
19
20
|
{ id: "logs", label: "Logs" },
|
|
20
21
|
{ id: "dependencies", label: "Dependencies" },
|
|
21
22
|
{ id: "system", label: "System" },
|
|
@@ -98,6 +98,9 @@ export class RowList {
|
|
|
98
98
|
})
|
|
99
99
|
this.box.onSizeChange = () => {
|
|
100
100
|
const visible = this.visibleCount
|
|
101
|
+
// The first setRows can happen before layout, when the viewport is one
|
|
102
|
+
// row tall; re-derive the scroll position now that the real height is known.
|
|
103
|
+
this.ensureVisible()
|
|
101
104
|
this.paint()
|
|
102
105
|
if (visible !== this.lastVisibleCount) {
|
|
103
106
|
this.lastVisibleCount = visible
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import type { AgentManager } from "../agents/agents.js"
|
|
2
|
+
import type { RemoteMachineConfig } from "./config.js"
|
|
1
3
|
import type { DependencyService } from "../deps/DependencyService.js"
|
|
4
|
+
import type { EnvironmentService } from "../environment/EnvironmentService.js"
|
|
2
5
|
import type { GitService } from "../git/GitService.js"
|
|
6
|
+
import type { GitWorktree } from "../git/worktrees.js"
|
|
3
7
|
import type { LogStore } from "../logs/LogBuffer.js"
|
|
4
8
|
import type { ProcessManager } from "../processes/ProcessManager.js"
|
|
5
9
|
import type { ServiceManager } from "../services/ServiceManager.js"
|
|
@@ -23,6 +27,8 @@ export interface CommandContext {
|
|
|
23
27
|
logs: LogStore
|
|
24
28
|
system: SystemMonitor
|
|
25
29
|
deps: DependencyService
|
|
30
|
+
agents: AgentManager
|
|
31
|
+
environment: EnvironmentService
|
|
26
32
|
|
|
27
33
|
navigate: (view: ViewId) => void
|
|
28
34
|
notify: (level: NotificationLevel, message: string, detail?: string) => void
|
|
@@ -43,6 +49,34 @@ export interface CommandContext {
|
|
|
43
49
|
switchProject: (path: string) => Promise<void>
|
|
44
50
|
/** Close an open project, stopping what devcc started there. Defaults to the current one. */
|
|
45
51
|
closeProject: (path?: string) => Promise<void>
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The worktree commands act on: the one selected in the Worktrees view, or
|
|
55
|
+
* the worktree devcc is running in. Undefined outside a git repository.
|
|
56
|
+
*/
|
|
57
|
+
selectedWorktree: () => GitWorktree | undefined
|
|
58
|
+
/** Prompt for branch, base and path, then `git worktree add`. */
|
|
59
|
+
createWorktree: () => Promise<void>
|
|
60
|
+
/** Confirmed, never forced `git worktree remove`. */
|
|
61
|
+
removeWorktree: (worktree: GitWorktree) => Promise<void>
|
|
62
|
+
/** Prompt for a task and start the agent in the worktree (default: the selected one). */
|
|
63
|
+
launchAgent: (agentId: string, worktree?: GitWorktree) => Promise<void>
|
|
64
|
+
openInEditor: (path: string) => Promise<void>
|
|
65
|
+
openInTerminal: (path: string) => Promise<void>
|
|
66
|
+
/** Run the project's verification steps; output lands in Logs. */
|
|
67
|
+
runVerify: () => Promise<void>
|
|
68
|
+
/** Remote machines declared in config. */
|
|
69
|
+
remoteMachines: () => RemoteMachineConfig[]
|
|
70
|
+
/** Open an interactive SSH session in a terminal. */
|
|
71
|
+
sshTo: (machineId: string) => Promise<void>
|
|
72
|
+
/**
|
|
73
|
+
* Set up `.devcc.ts`: `"starter"` writes the detected starter; an agent id
|
|
74
|
+
* asks that agent to inspect the project and propose one, which devcc
|
|
75
|
+
* validates and writes only after confirmation.
|
|
76
|
+
*/
|
|
77
|
+
setUpConfig: (how: "starter" | (string & {})) => Promise<void>
|
|
78
|
+
/** Re-read the config, rebuilding the project session (confirms if processes run). */
|
|
79
|
+
reloadConfig: () => Promise<void>
|
|
46
80
|
quit: () => void
|
|
47
81
|
}
|
|
48
82
|
|