phoebe-agent 0.0.0 → 0.1.1
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 +77 -8
- package/bootstrap/bin.mjs +42 -0
- package/bootstrap/boot.ts +431 -0
- package/bootstrap/cli.ts +29 -0
- package/bootstrap/crash-loop.ts +391 -0
- package/bootstrap/define-config.ts +20 -0
- package/bootstrap/engine-source.ts +83 -0
- package/bootstrap/github-engine.ts +169 -0
- package/bootstrap/index.mjs +9 -0
- package/bootstrap/index.ts +24 -0
- package/bootstrap/materialize.mjs +53 -0
- package/bootstrap/reconcile.ts +314 -0
- package/bootstrap/spawn-engine.mjs +78 -0
- package/package.json +26 -24
- package/prompts/checks-prompt.md +21 -6
- package/prompts/conflict-prompt.md +12 -1
- package/prompts/research-prompt.md +61 -0
- package/src/agent-env.ts +32 -0
- package/src/branded.ts +19 -0
- package/src/cli.ts +187 -0
- package/src/config-schema.ts +278 -0
- package/src/drain.ts +74 -0
- package/src/execution-gate.ts +27 -0
- package/src/git-model.ts +145 -0
- package/src/init.ts +277 -0
- package/src/load-config.ts +168 -0
- package/src/main.ts +1636 -0
- package/src/orchestrator.ts +953 -0
- package/src/prompt.ts +98 -0
- package/src/providers/providers.ts +222 -0
- package/src/providers/run-agent.ts +90 -0
- package/src/providers/types.ts +26 -0
- package/src/resolved-config.ts +55 -0
- package/templates/.env.example +17 -5
- package/templates/container/Dockerfile +128 -19
- package/templates/container/compose.local.yml +37 -0
- package/templates/container/compose.yml +43 -13
- package/templates/phoebe.config.ts +30 -6
- package/dist/phoebe.config.d.ts +0 -2
- package/dist/phoebe.config.js +0 -43
- package/dist/src/agent-env.d.ts +0 -6
- package/dist/src/agent-env.js +0 -24
- package/dist/src/cli.d.ts +0 -25
- package/dist/src/cli.js +0 -161
- package/dist/src/config-schema.d.ts +0 -163
- package/dist/src/config-schema.js +0 -140
- package/dist/src/execution-gate.d.ts +0 -9
- package/dist/src/execution-gate.js +0 -17
- package/dist/src/git-model.d.ts +0 -30
- package/dist/src/git-model.js +0 -71
- package/dist/src/index.d.ts +0 -2
- package/dist/src/index.js +0 -12
- package/dist/src/init.d.ts +0 -82
- package/dist/src/init.js +0 -207
- package/dist/src/load-config.d.ts +0 -88
- package/dist/src/load-config.js +0 -153
- package/dist/src/main.d.ts +0 -7
- package/dist/src/main.js +0 -1180
- package/dist/src/orchestrator.d.ts +0 -275
- package/dist/src/orchestrator.js +0 -579
- package/dist/src/prompt.d.ts +0 -13
- package/dist/src/prompt.js +0 -55
- package/dist/src/providers/providers.d.ts +0 -3
- package/dist/src/providers/providers.js +0 -210
- package/dist/src/providers/run-agent.d.ts +0 -34
- package/dist/src/providers/run-agent.js +0 -57
- package/dist/src/providers/types.d.ts +0 -27
- package/dist/src/providers/types.js +0 -6
- package/dist/src/resolved-config.d.ts +0 -8
- package/dist/src/resolved-config.js +0 -49
- package/dist/src/supervisor-decision.d.ts +0 -70
- package/dist/src/supervisor-decision.js +0 -94
- package/templates/container/compose.daemon.yml +0 -16
- package/templates/container/supervisor.sh +0 -50
- /package/prompts/{prompt.md → issues-prompt.md} +0 -0
package/dist/src/init.js
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
// `phoebe init` scaffolder. Given a target directory, drops the consumer-owned
|
|
2
|
-
// runtime into place: `phoebe.config.ts`, a `prompts/` dir with copies of the
|
|
3
|
-
// shipped defaults (edit-and-commit), `.env.example`, `.gitignore` entries,
|
|
4
|
-
// and the `container/` templates (Dockerfile, base compose, daemon overlay,
|
|
5
|
-
// supervisor script). Re-runs are guarded — an existing file is skipped, not
|
|
6
|
-
// silently overwritten, so consumer edits are safe.
|
|
7
|
-
//
|
|
8
|
-
// The plan/render split keeps the pure logic (what files, what placeholders)
|
|
9
|
-
// separately testable from the fs I/O in `runInit`.
|
|
10
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
-
import { basename, dirname, join, resolve as resolvePath } from "node:path";
|
|
12
|
-
import { fileURLToPath } from "node:url";
|
|
13
|
-
import { CONFIG_DEFAULTS } from "./config-schema.js";
|
|
14
|
-
export const DEFAULT_TEMPLATE_PARAMS = {
|
|
15
|
-
installCommand: "npm ci",
|
|
16
|
-
cliBin: "phoebe-agent",
|
|
17
|
-
};
|
|
18
|
-
const GITIGNORE_ENTRIES = [".env", "node_modules/"];
|
|
19
|
-
/**
|
|
20
|
-
* Enumerate every file init will produce. The prompt list is derived from
|
|
21
|
-
* `CONFIG_DEFAULTS.promptFiles` so adding a new prompt kind to the engine
|
|
22
|
-
* automatically gets scaffolded — no drift between the two lists.
|
|
23
|
-
*/
|
|
24
|
-
export function planInitOutputs() {
|
|
25
|
-
const promptOutputs = Object.values(CONFIG_DEFAULTS.promptFiles).map((relPath) => ({
|
|
26
|
-
destRelPath: relPath,
|
|
27
|
-
source: { kind: "shipped-prompt", promptRelPath: relPath },
|
|
28
|
-
}));
|
|
29
|
-
return [
|
|
30
|
-
{
|
|
31
|
-
destRelPath: "phoebe.config.ts",
|
|
32
|
-
source: { kind: "template", templateRelPath: "phoebe.config.ts" },
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
destRelPath: ".env.example",
|
|
36
|
-
source: { kind: "template", templateRelPath: ".env.example" },
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
destRelPath: "container/Dockerfile",
|
|
40
|
-
source: { kind: "template", templateRelPath: "container/Dockerfile" },
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
destRelPath: "container/compose.yml",
|
|
44
|
-
source: { kind: "template", templateRelPath: "container/compose.yml" },
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
destRelPath: "container/compose.daemon.yml",
|
|
48
|
-
source: { kind: "template", templateRelPath: "container/compose.daemon.yml" },
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
destRelPath: "container/supervisor.sh",
|
|
52
|
-
source: { kind: "template", templateRelPath: "container/supervisor.sh", executable: true },
|
|
53
|
-
},
|
|
54
|
-
...promptOutputs,
|
|
55
|
-
{
|
|
56
|
-
destRelPath: ".gitignore",
|
|
57
|
-
source: { kind: "gitignore", entries: GITIGNORE_ENTRIES },
|
|
58
|
-
},
|
|
59
|
-
];
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Substitute `{{KEY}}` tokens with the matching value. Unknown tokens throw
|
|
63
|
-
* so a typo in a template surfaces during tests rather than silently landing
|
|
64
|
-
* in a consumer's repo. Iteration is over the params (not a regex over the
|
|
65
|
-
* source) so a value that happens to contain a `{{…}}`-shaped substring is
|
|
66
|
-
* never re-scanned.
|
|
67
|
-
*/
|
|
68
|
-
export function renderTemplate(source, params) {
|
|
69
|
-
let rendered = source;
|
|
70
|
-
for (const [key, value] of Object.entries(params)) {
|
|
71
|
-
const token = `{{${keyToToken(key)}}}`;
|
|
72
|
-
rendered = rendered.split(token).join(value);
|
|
73
|
-
}
|
|
74
|
-
const leftover = /\{\{([A-Z_]+)\}\}/.exec(rendered);
|
|
75
|
-
if (leftover) {
|
|
76
|
-
throw new Error(`Template contained an unrenderable placeholder \`{{${leftover[1]}}}\` — ` +
|
|
77
|
-
`every {{TOKEN}} in a scaffolded file must map to a TemplateParams field.`);
|
|
78
|
-
}
|
|
79
|
-
return rendered;
|
|
80
|
-
}
|
|
81
|
-
function keyToToken(key) {
|
|
82
|
-
// installCommand -> INSTALL_COMMAND, cliBin -> CLI_BIN
|
|
83
|
-
return key.replace(/([A-Z])/g, "_$1").toUpperCase();
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Additive `.gitignore` merge — append any missing entries under a `# Phoebe`
|
|
87
|
-
* header, leaving existing lines untouched. An empty file becomes a bare list
|
|
88
|
-
* (no header) because there's nothing else to distinguish it from.
|
|
89
|
-
*/
|
|
90
|
-
export function mergeGitignore(existing, entries) {
|
|
91
|
-
const existingLines = new Set(existing
|
|
92
|
-
.split("\n")
|
|
93
|
-
.map((line) => line.trim())
|
|
94
|
-
.filter((line) => line.length > 0 && !line.startsWith("#")));
|
|
95
|
-
const missing = entries.filter((entry) => !existingLines.has(entry));
|
|
96
|
-
if (missing.length === 0) {
|
|
97
|
-
return existing;
|
|
98
|
-
}
|
|
99
|
-
if (existing.trim().length === 0) {
|
|
100
|
-
return `${missing.join("\n")}\n`;
|
|
101
|
-
}
|
|
102
|
-
const separator = existing.endsWith("\n") ? "" : "\n";
|
|
103
|
-
return `${existing}${separator}\n# Phoebe\n${missing.join("\n")}\n`;
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Walk up from this module's directory to find the shipped resource root. We
|
|
107
|
-
* emit to `dist/src/init.js` and read `templates/…` + `prompts/…` from the
|
|
108
|
-
* package root, so the walk-up mirrors `resolvePackageFile` in main.ts. Stops
|
|
109
|
-
* at a `node_modules` boundary so an installed dep never resolves resources
|
|
110
|
-
* from the consuming repo.
|
|
111
|
-
*/
|
|
112
|
-
function resolvePackageResource(relativePath, moduleDir) {
|
|
113
|
-
let dir = moduleDir;
|
|
114
|
-
while (true) {
|
|
115
|
-
const candidate = join(dir, relativePath);
|
|
116
|
-
if (existsSync(candidate)) {
|
|
117
|
-
return candidate;
|
|
118
|
-
}
|
|
119
|
-
const parent = dirname(dir);
|
|
120
|
-
if (parent === dir || basename(parent) === "node_modules") {
|
|
121
|
-
throw new Error(`Could not find ${relativePath} within the Phoebe package (searched from ${moduleDir})`);
|
|
122
|
-
}
|
|
123
|
-
dir = parent;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
function readShippedFile(relPath, packageRoot, moduleDir) {
|
|
127
|
-
const absolute = packageRoot
|
|
128
|
-
? resolvePath(packageRoot, relPath)
|
|
129
|
-
: resolvePackageResource(relPath, moduleDir);
|
|
130
|
-
return readFileSync(absolute, "utf8");
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Execute the plan: create missing files, additively update `.gitignore`, and
|
|
134
|
-
* leave every existing file alone. Returns a report so the CLI (and tests)
|
|
135
|
-
* can render a summary without re-walking the filesystem.
|
|
136
|
-
*
|
|
137
|
-
* Not idempotent in the "produces the same output twice" sense — running init
|
|
138
|
-
* twice on a directory the consumer has edited must not change their files.
|
|
139
|
-
* That's the entire guarded-re-run contract. A second run into an empty
|
|
140
|
-
* directory *does* reproduce the first-run output.
|
|
141
|
-
*/
|
|
142
|
-
export function runInit(opts) {
|
|
143
|
-
const targetDir = resolvePath(opts.targetDir);
|
|
144
|
-
const params = { ...DEFAULT_TEMPLATE_PARAMS, ...opts.params };
|
|
145
|
-
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
146
|
-
mkdirSync(targetDir, { recursive: true });
|
|
147
|
-
const report = { created: [], updated: [], skipped: [] };
|
|
148
|
-
for (const output of planInitOutputs()) {
|
|
149
|
-
const destAbs = join(targetDir, output.destRelPath);
|
|
150
|
-
mkdirSync(dirname(destAbs), { recursive: true });
|
|
151
|
-
if (output.source.kind === "gitignore") {
|
|
152
|
-
const existing = existsSync(destAbs) ? readFileSync(destAbs, "utf8") : "";
|
|
153
|
-
const merged = mergeGitignore(existing, output.source.entries);
|
|
154
|
-
if (merged === existing) {
|
|
155
|
-
report.skipped.push(output.destRelPath);
|
|
156
|
-
}
|
|
157
|
-
else if (existing.length === 0) {
|
|
158
|
-
writeFileSync(destAbs, merged);
|
|
159
|
-
report.created.push(output.destRelPath);
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
writeFileSync(destAbs, merged);
|
|
163
|
-
report.updated.push(output.destRelPath);
|
|
164
|
-
}
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
if (existsSync(destAbs)) {
|
|
168
|
-
report.skipped.push(output.destRelPath);
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
if (output.source.kind === "template") {
|
|
172
|
-
const rawTemplate = readShippedFile(join("templates", output.source.templateRelPath), opts.packageRoot, moduleDir);
|
|
173
|
-
const rendered = renderTemplate(rawTemplate, params);
|
|
174
|
-
writeFileSync(destAbs, rendered, {
|
|
175
|
-
mode: output.source.executable ? 0o755 : 0o644,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
// Shipped prompts ship verbatim — the engine's own render step handles
|
|
180
|
-
// their `{{PLACEHOLDER}}` tokens at run time.
|
|
181
|
-
const prompt = readShippedFile(output.source.promptRelPath, opts.packageRoot, moduleDir);
|
|
182
|
-
writeFileSync(destAbs, prompt);
|
|
183
|
-
}
|
|
184
|
-
report.created.push(output.destRelPath);
|
|
185
|
-
}
|
|
186
|
-
return report;
|
|
187
|
-
}
|
|
188
|
-
/** Human-readable summary suitable for the CLI to stdout after init runs. */
|
|
189
|
-
export function formatInitReport(report, targetDir) {
|
|
190
|
-
const lines = [`[phoebe] init → ${targetDir}`];
|
|
191
|
-
const emit = (label, paths) => {
|
|
192
|
-
if (paths.length === 0)
|
|
193
|
-
return;
|
|
194
|
-
lines.push(` ${label}:`);
|
|
195
|
-
for (const path of paths) {
|
|
196
|
-
lines.push(` ${path}`);
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
emit("created", report.created);
|
|
200
|
-
emit("updated", report.updated);
|
|
201
|
-
emit("skipped (already present)", report.skipped);
|
|
202
|
-
if (report.skipped.length > 0) {
|
|
203
|
-
lines.push("");
|
|
204
|
-
lines.push("Existing files were left untouched. Delete them and re-run init to regenerate.");
|
|
205
|
-
}
|
|
206
|
-
return `${lines.join("\n")}\n`;
|
|
207
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import type { PhoebeUserConfig } from "./config-schema.ts";
|
|
2
|
-
/**
|
|
3
|
-
* Identity function that types a consumer's `phoebe.config.ts` export as
|
|
4
|
-
* `PhoebeUserConfig`. Consumers write:
|
|
5
|
-
*
|
|
6
|
-
* ```ts
|
|
7
|
-
* import { defineConfig } from "phoebe-agent";
|
|
8
|
-
* export default defineConfig({ repoSlug: "...", ... });
|
|
9
|
-
* ```
|
|
10
|
-
*
|
|
11
|
-
* The engine never reads this at runtime beyond forwarding the value; the
|
|
12
|
-
* whole benefit is editor autocomplete and a compile-time check that only
|
|
13
|
-
* known fields appear.
|
|
14
|
-
*/
|
|
15
|
-
export declare function defineConfig(config: PhoebeUserConfig): PhoebeUserConfig;
|
|
16
|
-
/**
|
|
17
|
-
* Scalar-only overlay: each `PHOEBE_*` env var, when set to a non-empty
|
|
18
|
-
* string, replaces the corresponding user-config field. Nested records
|
|
19
|
-
* (`promptFiles`, `paths`, `defaultModels`, `providerEnv`, `workOrder`) stay
|
|
20
|
-
* config-file territory — env vars are for one-off run overrides where the
|
|
21
|
-
* consumer doesn't want to edit `phoebe.config.ts`, and expanding structured
|
|
22
|
-
* shapes into env keys defeats the point.
|
|
23
|
-
*
|
|
24
|
-
* A field-scoped list (rather than magic name-mangling) keeps the surface
|
|
25
|
-
* documented and predictable: users can grep for `PHOEBE_` here and see the
|
|
26
|
-
* complete overlay contract.
|
|
27
|
-
*/
|
|
28
|
-
export declare const ENV_OVERLAY_KEYS: readonly [{
|
|
29
|
-
readonly env: "PHOEBE_REPO_SLUG";
|
|
30
|
-
readonly key: "repoSlug";
|
|
31
|
-
}, {
|
|
32
|
-
readonly env: "PHOEBE_REPO_URL";
|
|
33
|
-
readonly key: "repoUrl";
|
|
34
|
-
}, {
|
|
35
|
-
readonly env: "PHOEBE_DEFAULT_BRANCH";
|
|
36
|
-
readonly key: "defaultBranch";
|
|
37
|
-
}, {
|
|
38
|
-
readonly env: "PHOEBE_BRANCH_PREFIX";
|
|
39
|
-
readonly key: "branchPrefix";
|
|
40
|
-
}, {
|
|
41
|
-
readonly env: "PHOEBE_READY_LABEL";
|
|
42
|
-
readonly key: "readyLabel";
|
|
43
|
-
}, {
|
|
44
|
-
readonly env: "PHOEBE_PROCESSING_LABEL";
|
|
45
|
-
readonly key: "processingLabel";
|
|
46
|
-
}, {
|
|
47
|
-
readonly env: "PHOEBE_PR_OPT_OUT_LABEL";
|
|
48
|
-
readonly key: "prOptOutLabel";
|
|
49
|
-
}, {
|
|
50
|
-
readonly env: "PHOEBE_INSTALL_COMMAND";
|
|
51
|
-
readonly key: "installCommand";
|
|
52
|
-
}, {
|
|
53
|
-
readonly env: "PHOEBE_CHECK_COMMAND";
|
|
54
|
-
readonly key: "checkCommand";
|
|
55
|
-
}, {
|
|
56
|
-
readonly env: "PHOEBE_TEST_COMMAND";
|
|
57
|
-
readonly key: "testCommand";
|
|
58
|
-
}, {
|
|
59
|
-
readonly env: "PHOEBE_READY_COMMAND";
|
|
60
|
-
readonly key: "readyCommand";
|
|
61
|
-
}, {
|
|
62
|
-
readonly env: "PHOEBE_BLOCKED_BY_PATTERN";
|
|
63
|
-
readonly key: "blockedByPattern";
|
|
64
|
-
}, {
|
|
65
|
-
readonly env: "PHOEBE_REVIEWS_SUCCESS_HEADING";
|
|
66
|
-
readonly key: "reviewsSuccessHeading";
|
|
67
|
-
}];
|
|
68
|
-
/**
|
|
69
|
-
* Apply the `PHOEBE_*` overlay onto a user config and return a new object.
|
|
70
|
-
* The overlay is additive over what the config file declared — an unset env
|
|
71
|
-
* var leaves the field untouched (so `resolveConfig` can still fall back to
|
|
72
|
-
* `CONFIG_DEFAULTS` if the field was also absent from the config file).
|
|
73
|
-
*/
|
|
74
|
-
export declare function applyEnvOverlay(user: PhoebeUserConfig, env: NodeJS.ProcessEnv): PhoebeUserConfig;
|
|
75
|
-
/**
|
|
76
|
-
* Resolve a `--config` argument (or the default) to an absolute path and
|
|
77
|
-
* assert the file exists. Split from `loadUserConfig` so the CLI can print
|
|
78
|
-
* a precise "file not found" message before attempting the dynamic import.
|
|
79
|
-
*/
|
|
80
|
-
export declare function resolveConfigPath(argPath: string | undefined, cwd: string): string;
|
|
81
|
-
/**
|
|
82
|
-
* Dynamically import a `phoebe.config.ts` and return the user shape. Native
|
|
83
|
-
* Node type-stripping (Node ≥ 22.7 with `--experimental-strip-types`, or
|
|
84
|
-
* ≥ 23 by default) handles the TS syntax — no bundler needed on the consumer
|
|
85
|
-
* side. Accepts either a default export or a named `config` export so the
|
|
86
|
-
* pre-`defineConfig` scaffold still loads.
|
|
87
|
-
*/
|
|
88
|
-
export declare function loadUserConfig(configPath: string): Promise<PhoebeUserConfig>;
|
package/dist/src/load-config.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
// Consumer-facing config plumbing: `defineConfig` (identity typing helper),
|
|
2
|
-
// `loadUserConfig` (dynamic TS import via native Node type-stripping), and
|
|
3
|
-
// `applyEnvOverlay` (`PHOEBE_*` overrides for scalar fields).
|
|
4
|
-
//
|
|
5
|
-
// The Phoebe CLI (src/cli.ts) chains these three: load the user's config,
|
|
6
|
-
// overlay env vars, then `resolveConfig` fills the shipped defaults. Kept
|
|
7
|
-
// separate from `config-schema.ts` so the schema stays a pure data contract
|
|
8
|
-
// and only the CLI path pulls in Node's fs/url runtime.
|
|
9
|
-
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
10
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
11
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
12
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
return path;
|
|
16
|
-
};
|
|
17
|
-
import { pathToFileURL } from "node:url";
|
|
18
|
-
import { existsSync } from "node:fs";
|
|
19
|
-
import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
20
|
-
import { PROVIDER_NAMES } from "./config-schema.js";
|
|
21
|
-
/**
|
|
22
|
-
* Identity function that types a consumer's `phoebe.config.ts` export as
|
|
23
|
-
* `PhoebeUserConfig`. Consumers write:
|
|
24
|
-
*
|
|
25
|
-
* ```ts
|
|
26
|
-
* import { defineConfig } from "phoebe-agent";
|
|
27
|
-
* export default defineConfig({ repoSlug: "...", ... });
|
|
28
|
-
* ```
|
|
29
|
-
*
|
|
30
|
-
* The engine never reads this at runtime beyond forwarding the value; the
|
|
31
|
-
* whole benefit is editor autocomplete and a compile-time check that only
|
|
32
|
-
* known fields appear.
|
|
33
|
-
*/
|
|
34
|
-
export function defineConfig(config) {
|
|
35
|
-
return config;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Scalar-only overlay: each `PHOEBE_*` env var, when set to a non-empty
|
|
39
|
-
* string, replaces the corresponding user-config field. Nested records
|
|
40
|
-
* (`promptFiles`, `paths`, `defaultModels`, `providerEnv`, `workOrder`) stay
|
|
41
|
-
* config-file territory — env vars are for one-off run overrides where the
|
|
42
|
-
* consumer doesn't want to edit `phoebe.config.ts`, and expanding structured
|
|
43
|
-
* shapes into env keys defeats the point.
|
|
44
|
-
*
|
|
45
|
-
* A field-scoped list (rather than magic name-mangling) keeps the surface
|
|
46
|
-
* documented and predictable: users can grep for `PHOEBE_` here and see the
|
|
47
|
-
* complete overlay contract.
|
|
48
|
-
*/
|
|
49
|
-
export const ENV_OVERLAY_KEYS = [
|
|
50
|
-
{ env: "PHOEBE_REPO_SLUG", key: "repoSlug" },
|
|
51
|
-
{ env: "PHOEBE_REPO_URL", key: "repoUrl" },
|
|
52
|
-
{ env: "PHOEBE_DEFAULT_BRANCH", key: "defaultBranch" },
|
|
53
|
-
{ env: "PHOEBE_BRANCH_PREFIX", key: "branchPrefix" },
|
|
54
|
-
{ env: "PHOEBE_READY_LABEL", key: "readyLabel" },
|
|
55
|
-
{ env: "PHOEBE_PROCESSING_LABEL", key: "processingLabel" },
|
|
56
|
-
{ env: "PHOEBE_PR_OPT_OUT_LABEL", key: "prOptOutLabel" },
|
|
57
|
-
{ env: "PHOEBE_INSTALL_COMMAND", key: "installCommand" },
|
|
58
|
-
{ env: "PHOEBE_CHECK_COMMAND", key: "checkCommand" },
|
|
59
|
-
{ env: "PHOEBE_TEST_COMMAND", key: "testCommand" },
|
|
60
|
-
{ env: "PHOEBE_READY_COMMAND", key: "readyCommand" },
|
|
61
|
-
{ env: "PHOEBE_BLOCKED_BY_PATTERN", key: "blockedByPattern" },
|
|
62
|
-
{ env: "PHOEBE_REVIEWS_SUCCESS_HEADING", key: "reviewsSuccessHeading" },
|
|
63
|
-
];
|
|
64
|
-
const PR_SCOPE_VALUES = ["phoebe", "all"];
|
|
65
|
-
const DRAFT_PRS_VALUES = ["skip-non-phoebe", "skip-all", "include"];
|
|
66
|
-
function readNonEmpty(env, key) {
|
|
67
|
-
const raw = env[key];
|
|
68
|
-
if (typeof raw !== "string" || raw.length === 0)
|
|
69
|
-
return undefined;
|
|
70
|
-
return raw;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Apply the `PHOEBE_*` overlay onto a user config and return a new object.
|
|
74
|
-
* The overlay is additive over what the config file declared — an unset env
|
|
75
|
-
* var leaves the field untouched (so `resolveConfig` can still fall back to
|
|
76
|
-
* `CONFIG_DEFAULTS` if the field was also absent from the config file).
|
|
77
|
-
*/
|
|
78
|
-
export function applyEnvOverlay(user, env) {
|
|
79
|
-
const overlaid = { ...user };
|
|
80
|
-
for (const { env: envKey, key } of ENV_OVERLAY_KEYS) {
|
|
81
|
-
const value = readNonEmpty(env, envKey);
|
|
82
|
-
if (value !== undefined) {
|
|
83
|
-
overlaid[key] = value;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
const prScope = readNonEmpty(env, "PHOEBE_PR_SCOPE");
|
|
87
|
-
if (prScope !== undefined) {
|
|
88
|
-
if (!PR_SCOPE_VALUES.includes(prScope)) {
|
|
89
|
-
throw new Error(`PHOEBE_PR_SCOPE must be one of ${PR_SCOPE_VALUES.join(", ")} (got "${prScope}").`);
|
|
90
|
-
}
|
|
91
|
-
overlaid.prScope = prScope;
|
|
92
|
-
}
|
|
93
|
-
const draftPrs = readNonEmpty(env, "PHOEBE_DRAFT_PRS");
|
|
94
|
-
if (draftPrs !== undefined) {
|
|
95
|
-
if (!DRAFT_PRS_VALUES.includes(draftPrs)) {
|
|
96
|
-
throw new Error(`PHOEBE_DRAFT_PRS must be one of ${DRAFT_PRS_VALUES.join(", ")} (got "${draftPrs}").`);
|
|
97
|
-
}
|
|
98
|
-
overlaid.draftPrs = draftPrs;
|
|
99
|
-
}
|
|
100
|
-
const defaultProvider = readNonEmpty(env, "PHOEBE_DEFAULT_PROVIDER");
|
|
101
|
-
if (defaultProvider !== undefined) {
|
|
102
|
-
if (!PROVIDER_NAMES.includes(defaultProvider)) {
|
|
103
|
-
throw new Error(`PHOEBE_DEFAULT_PROVIDER must be one of ${PROVIDER_NAMES.join(", ")} (got "${defaultProvider}").`);
|
|
104
|
-
}
|
|
105
|
-
overlaid.defaultProvider = defaultProvider;
|
|
106
|
-
}
|
|
107
|
-
return overlaid;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Resolve a `--config` argument (or the default) to an absolute path and
|
|
111
|
-
* assert the file exists. Split from `loadUserConfig` so the CLI can print
|
|
112
|
-
* a precise "file not found" message before attempting the dynamic import.
|
|
113
|
-
*/
|
|
114
|
-
export function resolveConfigPath(argPath, cwd) {
|
|
115
|
-
const candidate = argPath ?? "phoebe.config.ts";
|
|
116
|
-
const absolute = isAbsolute(candidate) ? candidate : resolvePath(cwd, candidate);
|
|
117
|
-
if (!existsSync(absolute)) {
|
|
118
|
-
throw new Error(argPath
|
|
119
|
-
? `Config file not found: ${absolute} (passed via --config).`
|
|
120
|
-
: `Config file not found: ${absolute}. ` +
|
|
121
|
-
`Create a phoebe.config.ts in the current directory or pass --config <path>.`);
|
|
122
|
-
}
|
|
123
|
-
return absolute;
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Dynamically import a `phoebe.config.ts` and return the user shape. Native
|
|
127
|
-
* Node type-stripping (Node ≥ 22.7 with `--experimental-strip-types`, or
|
|
128
|
-
* ≥ 23 by default) handles the TS syntax — no bundler needed on the consumer
|
|
129
|
-
* side. Accepts either a default export or a named `config` export so the
|
|
130
|
-
* pre-`defineConfig` scaffold still loads.
|
|
131
|
-
*/
|
|
132
|
-
export async function loadUserConfig(configPath) {
|
|
133
|
-
const url = pathToFileURL(configPath).href;
|
|
134
|
-
let mod;
|
|
135
|
-
try {
|
|
136
|
-
mod = await import(__rewriteRelativeImportExtension(url));
|
|
137
|
-
}
|
|
138
|
-
catch (error) {
|
|
139
|
-
throw new Error(`Failed to load ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
140
|
-
}
|
|
141
|
-
const record = mod;
|
|
142
|
-
const candidate = (typeof record["default"] === "object" && record["default"] !== null
|
|
143
|
-
? record["default"]
|
|
144
|
-
: undefined) ??
|
|
145
|
-
(typeof record["config"] === "object" && record["config"] !== null
|
|
146
|
-
? record["config"]
|
|
147
|
-
: undefined);
|
|
148
|
-
if (!candidate) {
|
|
149
|
-
throw new Error(`${configPath} must export a Phoebe config as \`export default defineConfig({ ... })\` ` +
|
|
150
|
-
`or a named \`export const config = { ... }\`.`);
|
|
151
|
-
}
|
|
152
|
-
return candidate;
|
|
153
|
-
}
|
package/dist/src/main.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Drive the Phoebe worker loop until it exits (persistent mode) or completes
|
|
3
|
-
* one unit (`--run-once`). Called by src/cli.ts after the resolved config is
|
|
4
|
-
* installed; the CLI passes its argv with `--config <path>` already stripped
|
|
5
|
-
* so this only sees engine-level flags.
|
|
6
|
-
*/
|
|
7
|
-
export declare function runEngine(argv?: readonly string[]): Promise<void>;
|