@doriandev/devcc 0.2.0 → 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 +39 -9
- package/bin/devcc.mjs +2 -2
- package/dist/devcc/agents/agents.d.ts +7 -0
- package/dist/devcc/agents/agents.d.ts.map +1 -1
- package/dist/devcc/agents/agents.js +3 -0
- package/dist/devcc/agents/agents.js.map +1 -1
- package/dist/devcc/app/App.d.ts +8 -0
- package/dist/devcc/app/App.d.ts.map +1 -1
- package/dist/devcc/app/App.js +125 -2
- package/dist/devcc/app/App.js.map +1 -1
- package/dist/devcc/app/controlPlaneCommands.d.ts.map +1 -1
- package/dist/devcc/app/controlPlaneCommands.js +25 -0
- package/dist/devcc/app/controlPlaneCommands.js.map +1 -1
- package/dist/devcc/app/keymap.d.ts.map +1 -1
- package/dist/devcc/app/keymap.js +1 -0
- package/dist/devcc/app/keymap.js.map +1 -1
- package/dist/devcc/automation.d.ts +21 -0
- package/dist/devcc/automation.d.ts.map +1 -1
- package/dist/devcc/automation.js +138 -1
- package/dist/devcc/automation.js.map +1 -1
- package/dist/devcc/cli.d.ts.map +1 -1
- package/dist/devcc/cli.js +30 -3
- package/dist/devcc/cli.js.map +1 -1
- package/dist/devcc/core/commands.d.ts +8 -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 +2 -0
- package/dist/devcc/core/config.d.ts.map +1 -1
- package/dist/devcc/core/config.js +4 -0
- package/dist/devcc/core/config.js.map +1 -1
- package/dist/devcc/index.d.ts +4 -2
- package/dist/devcc/index.d.ts.map +1 -1
- package/dist/devcc/index.js +4 -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/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/views/OverviewView.d.ts.map +1 -1
- package/dist/devcc/views/OverviewView.js +3 -1
- package/dist/devcc/views/OverviewView.js.map +1 -1
- package/package.json +1 -1
- package/src/devcc/agents/agents.ts +10 -0
- package/src/devcc/app/App.ts +153 -2
- package/src/devcc/app/controlPlaneCommands.ts +30 -0
- package/src/devcc/app/keymap.ts +1 -0
- package/src/devcc/automation.ts +168 -1
- package/src/devcc/cli.ts +35 -3
- package/src/devcc/core/commands.ts +8 -0
- package/src/devcc/core/config.ts +4 -0
- package/src/devcc/index.ts +10 -1
- package/src/devcc/init/detect.ts +268 -0
- package/src/devcc/init/prompt.ts +179 -0
- package/src/devcc/services/node.ts +17 -14
- package/src/devcc/views/OverviewView.ts +8 -1
package/src/devcc/app/App.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { BoxRenderable, createCliRenderer, type CliRenderer, type KeyEvent } from "@opentui/core"
|
|
2
|
-
import { existsSync } from "node:fs"
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
3
3
|
import { basename, join, resolve } from "node:path"
|
|
4
4
|
import { AgentManager, createAgentAdapters } from "../agents/agents.js"
|
|
5
|
+
import { configProblems } from "../automation.js"
|
|
6
|
+
import { detectProject, renderStarterConfig } from "../init/detect.js"
|
|
7
|
+
import { buildSetupPrompt, extractConfigFromOutput } from "../init/prompt.js"
|
|
5
8
|
import { ConfirmDialog, PromptDialog } from "../components/ConfirmDialog.js"
|
|
6
9
|
import { Footer, type KeyHint } from "../components/Footer.js"
|
|
7
10
|
import { Header } from "../components/Header.js"
|
|
@@ -11,7 +14,7 @@ import { Sidebar } from "../components/Sidebar.js"
|
|
|
11
14
|
import { CommandPalette } from "../components/CommandPalette.js"
|
|
12
15
|
import { ProjectPicker } from "../components/ProjectPicker.js"
|
|
13
16
|
import { CommandRegistry, type CommandContext } from "../core/commands.js"
|
|
14
|
-
import { loadConfig, type CommandConfigEntry, type ResolvedConfig } from "../core/config.js"
|
|
17
|
+
import { loadConfig, loadConfigFile, type CommandConfigEntry, type ResolvedConfig } from "../core/config.js"
|
|
15
18
|
import { EnvironmentService, type EnvironmentSnapshot } from "../environment/EnvironmentService.js"
|
|
16
19
|
import { Subscriptions } from "../core/events.js"
|
|
17
20
|
import {
|
|
@@ -132,6 +135,8 @@ interface ProjectSession {
|
|
|
132
135
|
/** Config problems that make verification untrustworthy; running is refused while any exist. */
|
|
133
136
|
verifyIssues: string[]
|
|
134
137
|
verifyAbort: AbortController | null
|
|
138
|
+
/** The agent session proposing a .devcc.ts, if one is running. */
|
|
139
|
+
setupAgentSessionId: string | null
|
|
135
140
|
scripts: { name: string; command: string }[]
|
|
136
141
|
/** Script runs belong to the project they ran in, not to the store. */
|
|
137
142
|
scriptRuns: Record<string, ScriptRun>
|
|
@@ -361,6 +366,7 @@ export class App {
|
|
|
361
366
|
verify: { configured: resolvedVerify.steps.length, running: false, steps: [] },
|
|
362
367
|
verifyIssues: [...loaded.issues.filter(isVerifyIssue), ...resolvedVerify.issues],
|
|
363
368
|
verifyAbort: null,
|
|
369
|
+
setupAgentSessionId: null,
|
|
364
370
|
scripts,
|
|
365
371
|
scriptRuns: {},
|
|
366
372
|
wiring: new Subscriptions(),
|
|
@@ -388,6 +394,9 @@ export class App {
|
|
|
388
394
|
for (const issue of this.session.project.configIssues) {
|
|
389
395
|
this.pushNotification("warn", `Config: ${issue}`)
|
|
390
396
|
}
|
|
397
|
+
if (!this.session.project.configPath) {
|
|
398
|
+
this.pushNotification("info", "No .devcc.ts yet: press / and run “Project: set up .devcc.ts”")
|
|
399
|
+
}
|
|
391
400
|
}
|
|
392
401
|
|
|
393
402
|
// ---------------------------------------------------------------------
|
|
@@ -519,6 +528,7 @@ export class App {
|
|
|
519
528
|
processes.on("process:exit", (process) => {
|
|
520
529
|
this.finishScriptRun(session, process.id, process.exitCode ?? 0)
|
|
521
530
|
syncProcesses()
|
|
531
|
+
if (process.id === session.setupAgentSessionId) void this.finishConfigSetup(session, process)
|
|
522
532
|
})
|
|
523
533
|
processes.on("process:error", (error) => notify("error", error.message, error.detail))
|
|
524
534
|
processes.on("process:retry", (managed, delayMs) => {
|
|
@@ -1148,6 +1158,8 @@ export class App {
|
|
|
1148
1158
|
runVerify: () => this.runVerify(),
|
|
1149
1159
|
remoteMachines: () => this.config.remoteMachines,
|
|
1150
1160
|
sshTo: (machineId) => this.sshTo(machineId),
|
|
1161
|
+
setUpConfig: (how) => this.setUpConfig(how),
|
|
1162
|
+
reloadConfig: () => this.reloadConfig(),
|
|
1151
1163
|
quit: () => void this.quit(),
|
|
1152
1164
|
}
|
|
1153
1165
|
|
|
@@ -1690,6 +1702,145 @@ export class App {
|
|
|
1690
1702
|
}
|
|
1691
1703
|
}
|
|
1692
1704
|
|
|
1705
|
+
// ---------------------------------------------------------------------
|
|
1706
|
+
// Project setup (.devcc.ts)
|
|
1707
|
+
// ---------------------------------------------------------------------
|
|
1708
|
+
|
|
1709
|
+
private async setUpConfig(how: string): Promise<void> {
|
|
1710
|
+
const session = this.session
|
|
1711
|
+
const facts = detectProject(session.rootDir)
|
|
1712
|
+
|
|
1713
|
+
if (how === "starter") {
|
|
1714
|
+
if (facts.configPath) {
|
|
1715
|
+
const replace = await this.askConfirm(
|
|
1716
|
+
"Replace the existing config with a starter?",
|
|
1717
|
+
`${tildify(facts.configPath)} will be overwritten with what devcc detects. Consider setting it up with an agent instead, which keeps what is still right.`,
|
|
1718
|
+
)
|
|
1719
|
+
if (!replace) return
|
|
1720
|
+
}
|
|
1721
|
+
const target = join(session.rootDir, ".devcc.ts")
|
|
1722
|
+
writeFileSync(target, renderStarterConfig(facts), "utf8")
|
|
1723
|
+
this.pushNotification("success", `Created ${tildify(target)}`)
|
|
1724
|
+
await this.reloadConfig()
|
|
1725
|
+
return
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
const adapter = this.agents.get(how)
|
|
1729
|
+
if (!adapter) {
|
|
1730
|
+
this.pushNotification("warn", `Unknown agent "${how}"`)
|
|
1731
|
+
return
|
|
1732
|
+
}
|
|
1733
|
+
if (session.setupAgentSessionId && session.processes.isRunning(session.setupAgentSessionId)) {
|
|
1734
|
+
this.pushNotification("warn", "An agent is already setting up .devcc.ts")
|
|
1735
|
+
return
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
try {
|
|
1739
|
+
const agentSession = await this.agents.launch(how, {
|
|
1740
|
+
cwd: session.rootDir,
|
|
1741
|
+
prompt: buildSetupPrompt(facts, "print"),
|
|
1742
|
+
})
|
|
1743
|
+
session.setupAgentSessionId = agentSession.id
|
|
1744
|
+
updateUi(this.store, { logSource: agentSession.id })
|
|
1745
|
+
this.pushNotification(
|
|
1746
|
+
"info",
|
|
1747
|
+
`${adapter.name} is reading the project; devcc will offer its .devcc.ts when it finishes`,
|
|
1748
|
+
)
|
|
1749
|
+
} catch (error) {
|
|
1750
|
+
const devccError = toDevccError(error, "process")
|
|
1751
|
+
this.pushNotification("error", devccError.message, devccError.detail)
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/**
|
|
1756
|
+
* The setup agent exited: take the config it printed, validate it from a
|
|
1757
|
+
* scratch file under .devcc/, and write it only if the user agrees.
|
|
1758
|
+
*/
|
|
1759
|
+
private async finishConfigSetup(session: ProjectSession, agent: ManagedProcess): Promise<void> {
|
|
1760
|
+
session.setupAgentSessionId = null
|
|
1761
|
+
const name = session.project.name
|
|
1762
|
+
if (agent.exitCode !== 0) {
|
|
1763
|
+
this.pushNotification(
|
|
1764
|
+
"error",
|
|
1765
|
+
`[${name}] Config setup agent exited with code ${agent.exitCode ?? "?"}; see its logs`,
|
|
1766
|
+
)
|
|
1767
|
+
return
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
const output = (session.logs.get(agent.id)?.toArray() ?? [])
|
|
1771
|
+
.filter((entry) => entry.stream !== "system")
|
|
1772
|
+
.map((entry) => entry.text)
|
|
1773
|
+
.join("\n")
|
|
1774
|
+
const proposal = extractConfigFromOutput(output)
|
|
1775
|
+
if (!proposal) {
|
|
1776
|
+
this.pushNotification("warn", `[${name}] The agent did not print a .devcc.ts; see its logs`)
|
|
1777
|
+
return
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
const scratchDir = join(session.rootDir, ".devcc")
|
|
1781
|
+
const proposedPath = join(scratchDir, "proposed.devcc.ts")
|
|
1782
|
+
mkdirSync(scratchDir, { recursive: true })
|
|
1783
|
+
writeFileSync(proposedPath, proposal, "utf8")
|
|
1784
|
+
const loaded = await loadConfigFile(proposedPath)
|
|
1785
|
+
const problems = configProblems(session.rootDir, loaded)
|
|
1786
|
+
const { config } = loaded
|
|
1787
|
+
|
|
1788
|
+
const current = session.project.configPath
|
|
1789
|
+
const target = current && !current.endsWith(".json") ? current : join(session.rootDir, ".devcc.ts")
|
|
1790
|
+
const summary = [
|
|
1791
|
+
`${config.processes.length} process(es), ${config.commands.length} command(s), verify: ${
|
|
1792
|
+
config.verify.map((entry) => (typeof entry === "string" ? entry : entry.id)).join(", ") || "none"
|
|
1793
|
+
}.`,
|
|
1794
|
+
problems.length > 0 ? `${problems.length} issue(s): ${problems[0] ?? ""}` : "It validates cleanly.",
|
|
1795
|
+
`Review it first in ${tildify(proposedPath)}.`,
|
|
1796
|
+
].join(" ")
|
|
1797
|
+
|
|
1798
|
+
const write = await this.askConfirm(
|
|
1799
|
+
`Write ${current ? "the updated" : "the proposed"} ${tildify(target)}?`,
|
|
1800
|
+
summary,
|
|
1801
|
+
)
|
|
1802
|
+
if (!write) {
|
|
1803
|
+
this.pushNotification("info", `Kept the proposal at ${tildify(proposedPath)}`)
|
|
1804
|
+
return
|
|
1805
|
+
}
|
|
1806
|
+
writeFileSync(target, proposal, "utf8")
|
|
1807
|
+
this.pushNotification("success", `Wrote ${tildify(target)}`)
|
|
1808
|
+
if (session === this.session) await this.reloadConfig()
|
|
1809
|
+
else this.pushNotification("info", `Switch to ${name} and reload its config to use it`)
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
/** Rebuild the active project's session so a changed config takes effect. */
|
|
1813
|
+
private async reloadConfig(): Promise<void> {
|
|
1814
|
+
const previous = this.session
|
|
1815
|
+
const running = previous.processes.list().filter((entry) => isActive(entry.status))
|
|
1816
|
+
if (running.length > 0) {
|
|
1817
|
+
const confirmed = await this.askConfirm(
|
|
1818
|
+
`Reload the config and stop ${running.length} running process(es)?`,
|
|
1819
|
+
`${running.map((entry) => entry.name).join(", ")} will be stopped; start them again afterwards.`,
|
|
1820
|
+
)
|
|
1821
|
+
if (!confirmed) return
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
this.stopSessionBackground(previous)
|
|
1825
|
+
previous.verifyAbort?.abort()
|
|
1826
|
+
await previous.processes.stopAll()
|
|
1827
|
+
previous.wiring.dispose()
|
|
1828
|
+
|
|
1829
|
+
try {
|
|
1830
|
+
const next = await this.createSession(previous.rootDir)
|
|
1831
|
+
this.sessions.set(next.rootDir, next)
|
|
1832
|
+
this.wireSession(next)
|
|
1833
|
+
this.activateSession(next)
|
|
1834
|
+
this.reportConfigIssues()
|
|
1835
|
+
this.pushNotification(
|
|
1836
|
+
"success",
|
|
1837
|
+
`Reloaded ${next.project.configPath ? basename(next.project.configPath) : "config"}`,
|
|
1838
|
+
)
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
this.pushNotification("error", `Could not reload the config: ${errorMessage(error)}`)
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1693
1844
|
// ---------------------------------------------------------------------
|
|
1694
1845
|
// Dependencies
|
|
1695
1846
|
// ---------------------------------------------------------------------
|
|
@@ -168,6 +168,36 @@ export function createControlPlaneCommands(options: ControlPlaneCommandOptions):
|
|
|
168
168
|
},
|
|
169
169
|
]
|
|
170
170
|
|
|
171
|
+
// --- Project setup -------------------------------------------------------
|
|
172
|
+
const hasConfig = (context: CommandContext) => context.store.getState().project.configPath !== null
|
|
173
|
+
commands.push(
|
|
174
|
+
{
|
|
175
|
+
id: "config.starter",
|
|
176
|
+
title: "Project: set up .devcc.ts from detected scripts",
|
|
177
|
+
category: "Project",
|
|
178
|
+
keywords: ["init", "config", "setup", "starter", "generate"],
|
|
179
|
+
when: (context) => !hasConfig(context),
|
|
180
|
+
execute: (context) => context.setUpConfig("starter"),
|
|
181
|
+
},
|
|
182
|
+
...options.agents
|
|
183
|
+
.filter((agent) => agent.separatesPrompt)
|
|
184
|
+
.map((agent): CommandDefinition => ({
|
|
185
|
+
id: `config.agent.${agent.id}`,
|
|
186
|
+
title: `Project: set up .devcc.ts with ${agent.name}`,
|
|
187
|
+
category: "Project",
|
|
188
|
+
keywords: ["init", "config", "setup", "improve", "agent", agent.id],
|
|
189
|
+
execute: (context) => context.setUpConfig(agent.id),
|
|
190
|
+
})),
|
|
191
|
+
{
|
|
192
|
+
id: "config.reload",
|
|
193
|
+
title: "Project: reload .devcc.ts",
|
|
194
|
+
category: "Project",
|
|
195
|
+
keywords: ["config", "refresh", "reread"],
|
|
196
|
+
when: hasConfig,
|
|
197
|
+
execute: (context) => context.reloadConfig(),
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
|
|
171
201
|
// --- Remote machines -----------------------------------------------------
|
|
172
202
|
for (const machine of options.machines) {
|
|
173
203
|
commands.push({
|
package/src/devcc/app/keymap.ts
CHANGED
|
@@ -64,6 +64,7 @@ export const HELP_SECTIONS: HelpSection[] = [
|
|
|
64
64
|
title: "Getting started",
|
|
65
65
|
entries: [
|
|
66
66
|
{ keys: ".devcc.ts", description: "declare processes devcc can control (see Config below)" },
|
|
67
|
+
{ keys: "/ set up", description: "“Project: set up .devcc.ts” writes a starter or asks an agent" },
|
|
67
68
|
{ keys: "ctrl+p", description: "open another project; the current one keeps running" },
|
|
68
69
|
{ keys: "/", description: "the palette lists everything the app can do" },
|
|
69
70
|
{ keys: ".devcc/devcc.log", description: "internal diagnostics, if something looks wrong" },
|
package/src/devcc/automation.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import {
|
|
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"
|
|
2
6
|
import { EnvironmentService, type EnvironmentSnapshot } from "./environment/EnvironmentService.js"
|
|
3
7
|
import { GitService } from "./git/GitService.js"
|
|
4
8
|
import { worktreeLabel } from "./git/worktrees.js"
|
|
9
|
+
import { detectProject, renderStarterConfig, starterConfig } from "./init/detect.js"
|
|
10
|
+
import { buildSetupPrompt } from "./init/prompt.js"
|
|
5
11
|
import { errorMessage } from "./utils/errors.js"
|
|
12
|
+
import { pluralise } from "./utils/format.js"
|
|
6
13
|
import { createLogger } from "./utils/logger.js"
|
|
7
14
|
import { detectPackageManager, readPackageJson, readScripts } from "./utils/packageManager.js"
|
|
8
15
|
import { tildify } from "./utils/paths.js"
|
|
@@ -240,3 +247,163 @@ export function formatEnvironment(snapshot: EnvironmentSnapshot): string {
|
|
|
240
247
|
}
|
|
241
248
|
return `${lines.join("\n")}\n`
|
|
242
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
|
@@ -10,7 +10,14 @@
|
|
|
10
10
|
import { createRequire } from "node:module"
|
|
11
11
|
import { resolve } from "node:path"
|
|
12
12
|
import { pathToFileURL } from "node:url"
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
configCheckCommand,
|
|
15
|
+
envCommand,
|
|
16
|
+
EXIT_USAGE,
|
|
17
|
+
initCommand,
|
|
18
|
+
verifyCommand,
|
|
19
|
+
worktreeListCommand,
|
|
20
|
+
} from "./automation.js"
|
|
14
21
|
import { errorMessage } from "./utils/errors.js"
|
|
15
22
|
import { findProjectRoot } from "./utils/paths.js"
|
|
16
23
|
|
|
@@ -19,6 +26,9 @@ interface CliArgs {
|
|
|
19
26
|
help: boolean
|
|
20
27
|
version: boolean
|
|
21
28
|
json: boolean
|
|
29
|
+
force: boolean
|
|
30
|
+
prompt: boolean
|
|
31
|
+
agent?: string
|
|
22
32
|
/** Subcommand and its arguments, e.g. `["worktree", "list"]`. */
|
|
23
33
|
positionals: string[]
|
|
24
34
|
}
|
|
@@ -41,12 +51,25 @@ function readVersion(): string {
|
|
|
41
51
|
const VERSION = readVersion()
|
|
42
52
|
|
|
43
53
|
function parseArgs(argv: readonly string[]): CliArgs {
|
|
44
|
-
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
|
+
}
|
|
45
62
|
for (let index = 0; index < argv.length; index += 1) {
|
|
46
63
|
const arg = argv[index]
|
|
47
64
|
if (arg === "--help" || arg === "-h") args.help = true
|
|
48
65
|
else if (arg === "--version" || arg === "-v") args.version = true
|
|
49
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)
|
|
50
73
|
else if (arg === "--cwd" || arg === "-C") {
|
|
51
74
|
const value = argv[index + 1]
|
|
52
75
|
if (value) {
|
|
@@ -69,10 +92,15 @@ Usage:
|
|
|
69
92
|
devcc verify [--json] Run the project's verify steps; exit 0 only if all pass
|
|
70
93
|
devcc worktree list [--json] List git worktrees
|
|
71
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
|
|
72
99
|
|
|
73
100
|
Options:
|
|
74
101
|
-C, --cwd <dir> Project directory (default: nearest project root)
|
|
75
|
-
--json Machine-readable output (verify, worktree list, env)
|
|
102
|
+
--json Machine-readable output (verify, worktree list, env, config check)
|
|
103
|
+
--force init: replace an existing .devcc.ts
|
|
76
104
|
-h, --help Show this help
|
|
77
105
|
-v, --version Show the version
|
|
78
106
|
|
|
@@ -105,6 +133,10 @@ export async function main(argv: readonly string[] = process.argv.slice(2)): Pro
|
|
|
105
133
|
if (command === "verify" && rest.length === 0) return runVerifyCli(options)
|
|
106
134
|
if (command === "worktree" && rest.length === 1 && rest[0] === "list") return worktreeListCommand(options)
|
|
107
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)
|
|
108
140
|
process.stderr.write(`devcc: unknown command "${args.positionals.join(" ")}"\n\n${USAGE}`)
|
|
109
141
|
return EXIT_USAGE
|
|
110
142
|
}
|
|
@@ -69,6 +69,14 @@ export interface CommandContext {
|
|
|
69
69
|
remoteMachines: () => RemoteMachineConfig[]
|
|
70
70
|
/** Open an interactive SSH session in a terminal. */
|
|
71
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>
|
|
72
80
|
quit: () => void
|
|
73
81
|
}
|
|
74
82
|
|
package/src/devcc/core/config.ts
CHANGED
|
@@ -411,7 +411,11 @@ export interface LoadedConfig extends ValidationResult {
|
|
|
411
411
|
export async function loadConfig(rootDir: string): Promise<LoadedConfig> {
|
|
412
412
|
const filePath = findConfigFile(rootDir)
|
|
413
413
|
if (!filePath) return { filePath: null, config: { ...EMPTY_CONFIG }, issues: [] }
|
|
414
|
+
return loadConfigFile(filePath)
|
|
415
|
+
}
|
|
414
416
|
|
|
417
|
+
/** Load and validate one specific config file (e.g. a proposed config before it is written). */
|
|
418
|
+
export async function loadConfigFile(filePath: string): Promise<LoadedConfig> {
|
|
415
419
|
try {
|
|
416
420
|
const raw = await importConfigModule(filePath)
|
|
417
421
|
const result = validateConfig(raw)
|
package/src/devcc/index.ts
CHANGED
|
@@ -17,7 +17,15 @@ export {
|
|
|
17
17
|
type AgentLaunchOptions,
|
|
18
18
|
type AgentSession,
|
|
19
19
|
} from "./agents/agents.js"
|
|
20
|
-
export {
|
|
20
|
+
export {
|
|
21
|
+
configCheckCommand,
|
|
22
|
+
envCommand,
|
|
23
|
+
initCommand,
|
|
24
|
+
verifyCommand,
|
|
25
|
+
worktreeListCommand,
|
|
26
|
+
} from "./automation.js"
|
|
27
|
+
export { detectProject, renderStarterConfig, starterConfig, type ProjectFacts } from "./init/detect.js"
|
|
28
|
+
export { buildSetupPrompt, extractConfigFromOutput, type SetupPromptMode } from "./init/prompt.js"
|
|
21
29
|
export { createBuiltinCommands, createConfigCommands, createScriptCommands } from "./app/builtinCommands.js"
|
|
22
30
|
export { HELP_SECTIONS, VIEW_SHORTCUTS } from "./app/keymap.js"
|
|
23
31
|
export { theme, glyphs, processStatusStyle, serviceStateStyle } from "./app/theme.js"
|
|
@@ -28,6 +36,7 @@ export {
|
|
|
28
36
|
loadConfig,
|
|
29
37
|
validateConfig,
|
|
30
38
|
findConfigFile,
|
|
39
|
+
loadConfigFile,
|
|
31
40
|
CONFIG_FILENAMES,
|
|
32
41
|
DEFAULT_REFRESH,
|
|
33
42
|
type CommandConfigEntry,
|