@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.
Files changed (63) hide show
  1. package/README.md +39 -9
  2. package/bin/devcc.mjs +2 -2
  3. package/dist/devcc/agents/agents.d.ts +7 -0
  4. package/dist/devcc/agents/agents.d.ts.map +1 -1
  5. package/dist/devcc/agents/agents.js +3 -0
  6. package/dist/devcc/agents/agents.js.map +1 -1
  7. package/dist/devcc/app/App.d.ts +8 -0
  8. package/dist/devcc/app/App.d.ts.map +1 -1
  9. package/dist/devcc/app/App.js +125 -2
  10. package/dist/devcc/app/App.js.map +1 -1
  11. package/dist/devcc/app/controlPlaneCommands.d.ts.map +1 -1
  12. package/dist/devcc/app/controlPlaneCommands.js +25 -0
  13. package/dist/devcc/app/controlPlaneCommands.js.map +1 -1
  14. package/dist/devcc/app/keymap.d.ts.map +1 -1
  15. package/dist/devcc/app/keymap.js +1 -0
  16. package/dist/devcc/app/keymap.js.map +1 -1
  17. package/dist/devcc/automation.d.ts +21 -0
  18. package/dist/devcc/automation.d.ts.map +1 -1
  19. package/dist/devcc/automation.js +138 -1
  20. package/dist/devcc/automation.js.map +1 -1
  21. package/dist/devcc/cli.d.ts.map +1 -1
  22. package/dist/devcc/cli.js +30 -3
  23. package/dist/devcc/cli.js.map +1 -1
  24. package/dist/devcc/core/commands.d.ts +8 -0
  25. package/dist/devcc/core/commands.d.ts.map +1 -1
  26. package/dist/devcc/core/commands.js.map +1 -1
  27. package/dist/devcc/core/config.d.ts +2 -0
  28. package/dist/devcc/core/config.d.ts.map +1 -1
  29. package/dist/devcc/core/config.js +4 -0
  30. package/dist/devcc/core/config.js.map +1 -1
  31. package/dist/devcc/index.d.ts +4 -2
  32. package/dist/devcc/index.d.ts.map +1 -1
  33. package/dist/devcc/index.js +4 -2
  34. package/dist/devcc/index.js.map +1 -1
  35. package/dist/devcc/init/detect.d.ts +55 -0
  36. package/dist/devcc/init/detect.d.ts.map +1 -0
  37. package/dist/devcc/init/detect.js +207 -0
  38. package/dist/devcc/init/detect.js.map +1 -0
  39. package/dist/devcc/init/prompt.d.ts +25 -0
  40. package/dist/devcc/init/prompt.d.ts.map +1 -0
  41. package/dist/devcc/init/prompt.js +156 -0
  42. package/dist/devcc/init/prompt.js.map +1 -0
  43. package/dist/devcc/services/node.d.ts +11 -0
  44. package/dist/devcc/services/node.d.ts.map +1 -1
  45. package/dist/devcc/services/node.js +20 -18
  46. package/dist/devcc/services/node.js.map +1 -1
  47. package/dist/devcc/views/OverviewView.d.ts.map +1 -1
  48. package/dist/devcc/views/OverviewView.js +3 -1
  49. package/dist/devcc/views/OverviewView.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/devcc/agents/agents.ts +10 -0
  52. package/src/devcc/app/App.ts +153 -2
  53. package/src/devcc/app/controlPlaneCommands.ts +30 -0
  54. package/src/devcc/app/keymap.ts +1 -0
  55. package/src/devcc/automation.ts +168 -1
  56. package/src/devcc/cli.ts +35 -3
  57. package/src/devcc/core/commands.ts +8 -0
  58. package/src/devcc/core/config.ts +4 -0
  59. package/src/devcc/index.ts +10 -1
  60. package/src/devcc/init/detect.ts +268 -0
  61. package/src/devcc/init/prompt.ts +179 -0
  62. package/src/devcc/services/node.ts +17 -14
  63. package/src/devcc/views/OverviewView.ts +8 -1
@@ -0,0 +1,268 @@
1
+ import { existsSync, readFileSync } from "node:fs"
2
+ import { basename, join } from "node:path"
3
+ import { findConfigFile } from "../core/config.js"
4
+ import type { ProcessDefinition } from "../processes/ManagedProcess.js"
5
+ import { detectFrameworks } from "../services/node.js"
6
+ import {
7
+ detectPackageManager,
8
+ readPackageJson,
9
+ readScripts,
10
+ runScriptArgs,
11
+ type PackageManager,
12
+ type PackageManagerDetection,
13
+ type PackageScript,
14
+ } from "../utils/packageManager.js"
15
+ import type { VerifyEntry } from "../verify/verify.js"
16
+
17
+ /**
18
+ * Project detection for `devcc init` and the agent setup prompt.
19
+ *
20
+ * Only facts that can be read from the repository are collected - nothing is
21
+ * executed and nothing is guessed. The starter config built from them is
22
+ * deliberately conservative: a script is only used when it exists.
23
+ */
24
+
25
+ export interface ProjectFacts {
26
+ rootDir: string
27
+ name: string
28
+ /** Existing config file, if any. */
29
+ configPath: string | null
30
+ hasGit: boolean
31
+ /** Null when there is no package.json. */
32
+ packageManager: PackageManagerDetection | null
33
+ scripts: PackageScript[]
34
+ frameworks: { name: string; dependency: string; port: number }[]
35
+ /** Script chosen to run the app in development. */
36
+ devScript?: string
37
+ /** Scripts suitable for `verify`, in the order they should run. */
38
+ verifyScripts: string[]
39
+ /** Scripts that look like verification but were left out, with the reason. */
40
+ skippedScripts: { name: string; reason: string }[]
41
+ monorepo: string | null
42
+ languages: ("node" | "rust" | "go" | "python")[]
43
+ /** Notable files present at the root (compose file, .envrc, mise.toml, Makefile, ...). */
44
+ files: string[]
45
+ }
46
+
47
+ type Exists = (path: string) => boolean
48
+
49
+ const DEV_SCRIPTS = ["dev", "start:dev", "develop", "serve"] as const
50
+
51
+ /** Verify categories in run order; the first matching script name in each wins. */
52
+ const VERIFY_CATEGORIES: readonly (readonly string[])[] = [
53
+ ["format:check", "format-check", "fmt:check", "prettier:check", "check:format", "lint:format"],
54
+ ["lint"],
55
+ ["typecheck", "type-check", "check-types", "types:check", "tsc"],
56
+ ["test:ci", "test:run", "test:unit", "test"],
57
+ ["build"],
58
+ ]
59
+
60
+ /** Aggregate scripts used only when no individual checks exist. */
61
+ const AGGREGATE_SCRIPTS = ["verify", "ci", "check", "validate"] as const
62
+
63
+ const NOTABLE_FILES = [
64
+ "compose.yaml",
65
+ "compose.yml",
66
+ "docker-compose.yml",
67
+ "docker-compose.yaml",
68
+ ".envrc",
69
+ "mise.toml",
70
+ ".mise.toml",
71
+ ".tool-versions",
72
+ ".env.example",
73
+ "Makefile",
74
+ "turbo.json",
75
+ "nx.json",
76
+ "pnpm-workspace.yaml",
77
+ "Cargo.toml",
78
+ "go.mod",
79
+ "pyproject.toml",
80
+ "AGENTS.md",
81
+ "CLAUDE.md",
82
+ ] as const
83
+
84
+ /** Why a script cannot be a verify step, or null when it can. */
85
+ export function unsuitableForVerify(command: string): string | null {
86
+ if (/--watch\b|\bwatch\b/u.test(command)) return "runs in watch mode"
87
+ if (/no test specified/u.test(command)) return "is npm's placeholder"
88
+ if (/--fix\b|--write\b/u.test(command)) return "modifies files"
89
+ return null
90
+ }
91
+
92
+ export function detectProject(rootDir: string, exists: Exists = existsSync): ProjectFacts {
93
+ const packageJson = readPackageJson(rootDir)
94
+ const scripts = readScripts(packageJson)
95
+ const byName = new Map(scripts.map((script) => [script.name, script.command]))
96
+
97
+ const verifyScripts: string[] = []
98
+ const skippedScripts: { name: string; reason: string }[] = []
99
+ for (const category of VERIFY_CATEGORIES) {
100
+ for (const name of category) {
101
+ const command = byName.get(name)
102
+ if (command === undefined) continue
103
+ const reason = unsuitableForVerify(command)
104
+ if (reason) {
105
+ skippedScripts.push({ name, reason })
106
+ continue
107
+ }
108
+ verifyScripts.push(name)
109
+ break
110
+ }
111
+ }
112
+ if (verifyScripts.length === 0) {
113
+ const aggregate = AGGREGATE_SCRIPTS.find((name) => {
114
+ const command = byName.get(name)
115
+ return command !== undefined && unsuitableForVerify(command) === null
116
+ })
117
+ if (aggregate) verifyScripts.push(aggregate)
118
+ }
119
+
120
+ const files = NOTABLE_FILES.filter((file) => exists(join(rootDir, file)))
121
+ const languages: ProjectFacts["languages"] = []
122
+ if (packageJson) languages.push("node")
123
+ if (files.includes("Cargo.toml")) languages.push("rust")
124
+ if (files.includes("go.mod")) languages.push("go")
125
+ if (files.includes("pyproject.toml")) languages.push("python")
126
+
127
+ const monorepo = files.includes("pnpm-workspace.yaml")
128
+ ? "pnpm workspaces"
129
+ : files.includes("turbo.json")
130
+ ? "Turborepo"
131
+ : files.includes("nx.json")
132
+ ? "Nx"
133
+ : packageJson?.workspaces
134
+ ? "package.json workspaces"
135
+ : null
136
+
137
+ return {
138
+ rootDir,
139
+ name: typeof packageJson?.name === "string" ? packageJson.name : basename(rootDir),
140
+ configPath: findConfigFile(rootDir, exists),
141
+ hasGit: exists(join(rootDir, ".git")),
142
+ packageManager: packageJson ? detectPackageManager({ rootDir, packageJson, fileExists: exists }) : null,
143
+ scripts,
144
+ frameworks: detectFrameworks(packageJson).map((hint) => ({
145
+ name: hint.name,
146
+ dependency: hint.dependency,
147
+ port: hint.port,
148
+ })),
149
+ devScript: DEV_SCRIPTS.find((name) => byName.has(name)),
150
+ verifyScripts,
151
+ skippedScripts,
152
+ monorepo,
153
+ languages,
154
+ files,
155
+ }
156
+ }
157
+
158
+ /** Inline verify steps for non-Node projects, using each toolchain's standard commands. */
159
+ function toolchainVerify(facts: ProjectFacts): VerifyEntry[] {
160
+ if (facts.verifyScripts.length > 0) return facts.verifyScripts
161
+ if (facts.languages.includes("rust")) {
162
+ return [
163
+ { id: "fmt", name: "cargo fmt", command: "cargo", args: ["fmt", "--check"] },
164
+ { id: "clippy", name: "cargo clippy", command: "cargo", args: ["clippy", "--all-targets"] },
165
+ { id: "test", name: "cargo test", command: "cargo", args: ["test"] },
166
+ ]
167
+ }
168
+ if (facts.languages.includes("go")) {
169
+ return [
170
+ { id: "vet", name: "go vet", command: "go", args: ["vet", "./..."] },
171
+ { id: "test", name: "go test", command: "go", args: ["test", "./..."] },
172
+ { id: "build", name: "go build", command: "go", args: ["build", "./..."] },
173
+ ]
174
+ }
175
+ return []
176
+ }
177
+
178
+ export interface StarterConfig {
179
+ processes: ProcessDefinition[]
180
+ verify: VerifyEntry[]
181
+ }
182
+
183
+ export function starterConfig(facts: ProjectFacts): StarterConfig {
184
+ const processes: ProcessDefinition[] = []
185
+ const manager: PackageManager | undefined = facts.packageManager?.manager
186
+ if (manager && facts.devScript) {
187
+ const framework = facts.frameworks[0]
188
+ processes.push({
189
+ id: "dev",
190
+ name: framework?.name ?? `${manager} ${facts.devScript}`,
191
+ command: manager,
192
+ args: runScriptArgs(manager, facts.devScript),
193
+ ...(framework ? { url: `localhost:${framework.port}`, readiness: { port: framework.port } } : {}),
194
+ })
195
+ }
196
+ return { processes, verify: toolchainVerify(facts) }
197
+ }
198
+
199
+ /** Render the starter as a `.devcc.ts` file. Plain object: no dependency on devcc is required. */
200
+ export function renderStarterConfig(facts: ProjectFacts, date = new Date()): string {
201
+ const starter = starterConfig(facts)
202
+ const lines = [
203
+ `/**`,
204
+ ` * devcc configuration for ${facts.name}, generated by \`devcc init\` on ${date.toISOString().slice(0, 10)}.`,
205
+ ` *`,
206
+ ` * Everything here is optional. Check it with \`devcc config check\`; refine it with`,
207
+ ` * \`devcc init --agent claude\` (or codex). Reference: https://www.npmjs.com/package/@doriandev/devcc`,
208
+ ` */`,
209
+ `export default {`,
210
+ ]
211
+
212
+ if (starter.processes.length > 0) {
213
+ lines.push(` processes: ${literal(starter.processes, 2)},`, ``)
214
+ } else {
215
+ lines.push(
216
+ ` // processes: [{ id: "web", command: "pnpm", args: ["dev"], readiness: { port: 3000 } }],`,
217
+ ``,
218
+ )
219
+ }
220
+
221
+ if (starter.verify.length > 0) {
222
+ lines.push(` // \`devcc verify\` runs these in order and exits 0 only if all pass.`)
223
+ lines.push(` verify: ${literal(starter.verify, 2)},`, ``)
224
+ } else {
225
+ lines.push(` // verify: ["lint", "test"],`, ``)
226
+ }
227
+
228
+ lines.push(
229
+ ` // editor: { command: "code" },`,
230
+ ` // agents: { claude: { args: ["--permission-mode", "acceptEdits"] } },`,
231
+ `}`,
232
+ ``,
233
+ )
234
+ return lines.join("\n")
235
+ }
236
+
237
+ /** A TypeScript object literal: identifier keys unquoted, values JSON-encoded. */
238
+ export function literal(value: unknown, indent = 0): string {
239
+ const pad = " ".repeat(indent)
240
+ const inner = " ".repeat(indent + 2)
241
+ if (Array.isArray(value)) {
242
+ if (value.every((item) => typeof item !== "object" || item === null)) {
243
+ return `[${value.map((item) => literal(item)).join(", ")}]`
244
+ }
245
+ return `[\n${value.map((item) => `${inner}${literal(item, indent + 2)},`).join("\n")}\n${pad}]`
246
+ }
247
+ if (typeof value === "object" && value !== null) {
248
+ const entries = Object.entries(value).filter(([, entry]) => entry !== undefined)
249
+ const rendered = entries.map(
250
+ ([key, entry]) =>
251
+ `${/^[A-Za-z_$][\w$]*$/u.test(key) ? key : JSON.stringify(key)}: ${literal(entry, indent + 2)}`,
252
+ )
253
+ const oneLine = `{ ${rendered.join(", ")} }`
254
+ if (oneLine.length + indent <= 100 && !oneLine.includes("\n")) return oneLine
255
+ return `{\n${rendered.map((entry) => `${inner}${entry},`).join("\n")}\n${pad}}`
256
+ }
257
+ return JSON.stringify(value)
258
+ }
259
+
260
+ /** Short file contents for the prompt, so the agent starts from real data. */
261
+ export function readSnippet(path: string, maxLength = 2_000): string | null {
262
+ try {
263
+ const text = readFileSync(path, "utf8")
264
+ return text.length > maxLength ? `${text.slice(0, maxLength)}\n…` : text
265
+ } catch {
266
+ return null
267
+ }
268
+ }
@@ -0,0 +1,179 @@
1
+ import { basename } from "node:path"
2
+ import { readSnippet, renderStarterConfig, type ProjectFacts } from "./detect.js"
3
+
4
+ /**
5
+ * The prompt that lets a coding agent set up devcc for a project.
6
+ *
7
+ * It carries what devcc already detected (so the agent verifies rather than
8
+ * rediscovers), a draft to start from, the config reference, and firm rules:
9
+ * never invent commands, never guess hosts, keep verify non-interactive.
10
+ *
11
+ * Two delivery modes:
12
+ * - `write`: an interactive agent edits `.devcc.ts` itself and checks its work
13
+ * with `devcc config check` (used by `devcc init --agent`).
14
+ * - `print`: a headless agent prints the file between markers; devcc validates
15
+ * it and asks before writing (used from the dashboard, where the agent has no
16
+ * permission to edit files).
17
+ */
18
+
19
+ export type SetupPromptMode = "write" | "print"
20
+
21
+ export const CONFIG_BEGIN_MARKER = "----- BEGIN .devcc.ts -----"
22
+ export const CONFIG_END_MARKER = "----- END .devcc.ts -----"
23
+
24
+ const REFERENCE = `
25
+ ## .devcc.ts reference (all fields optional)
26
+
27
+ export default {
28
+ name?: string, // header label
29
+ processes?: [{ // long-running things devcc starts/stops
30
+ id, name?, command, args?: string[], cwd?, env?: Record<string, string>,
31
+ kind?: "service" | "task", autoStart?: boolean, url?: string, port?: number,
32
+ dependsOn?: string[], // ids that must be ready first
33
+ readiness?: { port?: number, host?: string, logPattern?: string, timeoutMs?: number },
34
+ restartOnCrash?: boolean,
35
+ }],
36
+ commands?: [{ // palette actions
37
+ id, title, category?, command, args?, cwd?, keywords?: string[],
38
+ confirm?: true | { title, message }, // REQUIRED for anything destructive
39
+ }],
40
+ verify?: (string | { id, name?, command, args?, cwd? })[],
41
+ // strings = command ids, then package.json script names
42
+ services?: [ // extra things to watch
43
+ { id, name?, port, host?, healthPath? } |
44
+ { id, name?, type: "ollama", baseUrl: "http://host:11434" },
45
+ ],
46
+ agents?: { claude?: { command?, args? }, codex?: { command?, args? } },
47
+ editor?: { command: "code" | "zed" | "cursor" | "webstorm" } | { application: "WebStorm" },
48
+ terminal?: { application: "iTerm" } | { command, args?, execArgs? },
49
+ worktrees?: { directory?: string }, // relative to the main worktree, default ".."
50
+ remoteMachines?: [{ id, name?, host, transport: "ssh", port?, user?, services?: string[] }],
51
+ refresh?: { system?, services?, git?, clock? },
52
+ }
53
+
54
+ Everything is spawned as command + args with no shell: never put pipes, &&, env assignments or
55
+ quotes inside a single string. \`command: "pnpm", args: ["run", "dev"]\` is right;
56
+ \`command: "pnpm run dev"\` is wrong.`
57
+
58
+ function factsSection(facts: ProjectFacts): string {
59
+ const lines = [
60
+ `- root: ${facts.rootDir}`,
61
+ `- name: ${facts.name}`,
62
+ `- git repository: ${facts.hasGit ? "yes" : "no"}`,
63
+ `- languages: ${facts.languages.join(", ") || "none detected"}`,
64
+ `- package manager: ${facts.packageManager ? `${facts.packageManager.manager} (from ${facts.packageManager.source})` : "none (no package.json)"}`,
65
+ `- monorepo: ${facts.monorepo ?? "no"}`,
66
+ `- frameworks (by dependency): ${facts.frameworks.map((framework) => `${framework.name} (default port ${framework.port})`).join(", ") || "none recognised"}`,
67
+ `- notable files: ${facts.files.join(", ") || "none"}`,
68
+ `- existing devcc config: ${facts.configPath ? basename(facts.configPath) : "none"}`,
69
+ ]
70
+ if (facts.scripts.length > 0) {
71
+ lines.push(`- package.json scripts:`)
72
+ for (const script of facts.scripts) lines.push(` - ${script.name}: ${script.command}`)
73
+ }
74
+ if (facts.skippedScripts.length > 0) {
75
+ lines.push(
76
+ `- left out of verify: ${facts.skippedScripts.map((script) => `${script.name} (${script.reason})`).join(", ")}`,
77
+ )
78
+ }
79
+ return lines.join("\n")
80
+ }
81
+
82
+ export function buildSetupPrompt(facts: ProjectFacts, mode: SetupPromptMode): string {
83
+ const existing = facts.configPath ? readSnippet(facts.configPath, 6_000) : null
84
+ const draft = renderStarterConfig(facts)
85
+
86
+ const output =
87
+ mode === "write"
88
+ ? `## Deliver
89
+
90
+ 1. ${facts.configPath ? `Update \`${basename(facts.configPath)}\` in place, keeping anything the user wrote that is still correct.` : "Create `.devcc.ts` in the project root."}
91
+ 2. Run \`devcc config check\` and fix every issue it reports. Do not stop until it exits 0.
92
+ 3. If you added \`verify\`, run \`devcc verify\`. A step that fails because the project itself is broken
93
+ stays in the list - report it; do not delete checks to make the gate pass.
94
+ 4. Finish with a short summary: what you configured, what you deliberately left out, and why.`
95
+ : `## Deliver
96
+
97
+ You cannot edit files in this session. Print the complete file, and nothing else between the markers,
98
+ exactly like this:
99
+
100
+ ${CONFIG_BEGIN_MARKER}
101
+ export default {
102
+ ...
103
+ }
104
+ ${CONFIG_END_MARKER}
105
+
106
+ devcc validates it and asks the user before writing it${facts.configPath ? ` over \`${basename(facts.configPath)}\`` : ""}. After the markers, add
107
+ a short summary: what you configured, what you left out, and why.`
108
+
109
+ return `Set up devcc for this project: produce a solid .devcc.ts.
110
+
111
+ devcc (@doriandev/devcc) is a keyboard-driven terminal dashboard for local development. Its config
112
+ tells it which dev processes it may start, which checks \`devcc verify\` runs as the quality gate, and
113
+ which commands appear in its palette. You are configuring it for the repository in your working
114
+ directory.
115
+
116
+ ## What devcc already detected
117
+
118
+ ${factsSection(facts)}
119
+
120
+ ## Draft from \`devcc init\`
121
+
122
+ Start from this, but verify every line against the repository - it only knows script names.
123
+
124
+ \`\`\`ts
125
+ ${draft.trimEnd()}
126
+ \`\`\`
127
+ ${existing ? `\n## Current config (${basename(facts.configPath ?? "")})\n\n\`\`\`ts\n${existing.trimEnd()}\n\`\`\`\n` : ""}
128
+ ## Investigate before writing
129
+
130
+ - Read package.json (every workspace package in a monorepo), the README, CONTRIBUTING, AGENTS.md or
131
+ CLAUDE.md, CI workflows (.github/workflows), Makefile/justfile, docker compose files, framework
132
+ configs (next.config.*, vite.config.*, etc.) and .env.example.
133
+ - Find the real dev servers and their ports. Ports come from scripts, framework config or
134
+ .env.example - not from defaults when the project overrides them.
135
+ - CI shows what the team treats as the quality gate; mirror it in \`verify\` where it can run locally.
136
+
137
+ ## Rules
138
+
139
+ - Only use commands that exist in this repository or are the standard CLI of a toolchain it uses. Never
140
+ invent scripts, binaries or ports.
141
+ - \`processes\`: long-running dev servers, watchers and local infra the developer starts by hand. Use
142
+ \`readiness\` with the real port (or a \`logPattern\` from the server's actual output) and \`dependsOn\`
143
+ when one service needs another. In a monorepo, one process per app with \`cwd\`. Do not set
144
+ \`autoStart\` unless the README says the project is always run that way.
145
+ - \`verify\`: fast to slow (format check, lint, typecheck, tests, build). Every step must be
146
+ non-interactive, exit on its own, and not modify files (no --watch, --fix or --write). Prefer
147
+ package.json script names over inline steps.
148
+ - \`commands\`: useful project tasks that are not already scripts devcc shows (migrations, seeding,
149
+ codegen, docker compose profiles). Anything that deletes, resets, migrates or deploys needs
150
+ \`confirm: true\`.
151
+ - Never add secrets, tokens, credentials or values from .env files. Never add \`remoteMachines\`,
152
+ hostnames or IP addresses unless the repository documents them for local development.
153
+ - Leave out \`editor\`, \`terminal\` and \`agents\` unless the repository states a preference; they are
154
+ personal. Leave out anything you are unsure of rather than guessing - a small correct config beats a
155
+ large wrong one.
156
+ - Use a plain \`export default { ... }\` object. Import \`defineConfig\` from "@doriandev/devcc" only if that
157
+ package is already a dependency of this project.
158
+ ${REFERENCE}
159
+
160
+ ${output}
161
+ `
162
+ }
163
+
164
+ /**
165
+ * Pull the config out of a headless agent's output: the last block between the
166
+ * markers, with a surrounding code fence removed. Null when there is none.
167
+ */
168
+ export function extractConfigFromOutput(output: string): string | null {
169
+ const end = output.lastIndexOf(CONFIG_END_MARKER)
170
+ if (end === -1) return null
171
+ const begin = output.lastIndexOf(CONFIG_BEGIN_MARKER, end)
172
+ if (begin === -1) return null
173
+ const body = output
174
+ .slice(begin + CONFIG_BEGIN_MARKER.length, end)
175
+ .replace(/^\s*```[a-z]*\s*\n/u, "")
176
+ .replace(/\n\s*```\s*$/u, "")
177
+ .trim()
178
+ return body === "" ? null : `${body}\n`
179
+ }
@@ -73,7 +73,7 @@ export class PortService implements ServiceAdapter {
73
73
  }
74
74
  }
75
75
 
76
- interface FrameworkHint {
76
+ export interface FrameworkHint {
77
77
  /** Dependency name to look for in package.json. */
78
78
  dependency: string
79
79
  id: string
@@ -108,20 +108,26 @@ function dependencyNames(packageJson: Record<string, unknown> | null): Set<strin
108
108
  return names
109
109
  }
110
110
 
111
+ /** Frameworks this project depends on, in hint order, one per port. */
112
+ export function detectFrameworks(packageJson: Record<string, unknown> | null): FrameworkHint[] {
113
+ const dependencies = dependencyNames(packageJson)
114
+ const seenPorts = new Set<number>()
115
+ const found: FrameworkHint[] = []
116
+ for (const hint of FRAMEWORK_HINTS) {
117
+ if (!dependencies.has(hint.dependency) || seenPorts.has(hint.port)) continue
118
+ seenPorts.add(hint.port)
119
+ found.push(hint)
120
+ }
121
+ return found
122
+ }
123
+
111
124
  /**
112
125
  * Build port adapters for frameworks this project actually depends on.
113
126
  * Nothing is assumed: a project with none of these gets no adapters.
114
127
  */
115
128
  export function detectNodeServices(packageJson: Record<string, unknown> | null): PortService[] {
116
- const dependencies = dependencyNames(packageJson)
117
- const seenPorts = new Set<number>()
118
- const adapters: PortService[] = []
119
-
120
- for (const hint of FRAMEWORK_HINTS) {
121
- if (!dependencies.has(hint.dependency)) continue
122
- if (seenPorts.has(hint.port)) continue
123
- seenPorts.add(hint.port)
124
- adapters.push(
129
+ return detectFrameworks(packageJson).map(
130
+ (hint) =>
125
131
  new PortService({
126
132
  id: hint.id,
127
133
  name: hint.name,
@@ -130,8 +136,5 @@ export function detectNodeServices(packageJson: Record<string, unknown> | null):
130
136
  healthPath: hint.healthPath,
131
137
  onlyWhenListening: true,
132
138
  }),
133
- )
134
- }
135
-
136
- return adapters
139
+ )
137
140
  }
@@ -95,7 +95,14 @@ export class OverviewView extends View {
95
95
  rows.push(blank())
96
96
  rows.push(section("Development"))
97
97
  if (state.processes.length === 0) {
98
- rows.push(info("", "no managed processes - add some in .devcc.ts"))
98
+ rows.push(
99
+ info(
100
+ "",
101
+ state.project.configPath
102
+ ? "no managed processes - add some in .devcc.ts"
103
+ : "no .devcc.ts yet - press / and run “Project: set up .devcc.ts”",
104
+ ),
105
+ )
99
106
  } else {
100
107
  for (const process of state.processes.slice(0, compact ? 4 : 8)) {
101
108
  const detail = process.url ?? (process.pid ? `pid ${process.pid}` : "")