@agentplate/cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/agents/architect.md +108 -0
- package/agents/builder.md +97 -0
- package/agents/coordinator.md +113 -0
- package/agents/deployer.md +117 -0
- package/agents/devops.md +114 -0
- package/agents/lead.md +107 -0
- package/agents/merger.md +103 -0
- package/agents/reviewer.md +90 -0
- package/agents/scout.md +95 -0
- package/agents/verifier.md +106 -0
- package/package.json +64 -0
- package/src/agents/guard-rules.ts +55 -0
- package/src/agents/identity.test.ts +161 -0
- package/src/agents/identity.ts +229 -0
- package/src/agents/manifest.test.ts +260 -0
- package/src/agents/manifest.ts +286 -0
- package/src/agents/overlay.test.ts +190 -0
- package/src/agents/overlay.ts +212 -0
- package/src/agents/system-prompt.test.ts +53 -0
- package/src/agents/system-prompt.ts +95 -0
- package/src/agents/turn-runner.ts +79 -0
- package/src/commands/coordinator.test.ts +75 -0
- package/src/commands/coordinator.ts +259 -0
- package/src/commands/deploy.test.ts +504 -0
- package/src/commands/deploy.ts +874 -0
- package/src/commands/doctor.test.ts +106 -0
- package/src/commands/doctor.ts +208 -0
- package/src/commands/init.ts +71 -0
- package/src/commands/log.ts +51 -0
- package/src/commands/mail.ts +197 -0
- package/src/commands/merge.ts +127 -0
- package/src/commands/model.ts +58 -0
- package/src/commands/prime.ts +61 -0
- package/src/commands/reap.ts +87 -0
- package/src/commands/serve.ts +61 -0
- package/src/commands/setup.ts +48 -0
- package/src/commands/ship.test.ts +106 -0
- package/src/commands/ship.ts +202 -0
- package/src/commands/skill.test.ts +458 -0
- package/src/commands/skill.ts +730 -0
- package/src/commands/sling.ts +365 -0
- package/src/commands/status.ts +60 -0
- package/src/commands/stop.ts +56 -0
- package/src/commands/tui.ts +199 -0
- package/src/commands/worktree.ts +77 -0
- package/src/config.test.ts +92 -0
- package/src/config.ts +202 -0
- package/src/db/sqlite.test.ts +77 -0
- package/src/db/sqlite.ts +102 -0
- package/src/deploy/audit.test.ts +233 -0
- package/src/deploy/audit.ts +245 -0
- package/src/deploy/context.test.ts +243 -0
- package/src/deploy/context.ts +72 -0
- package/src/deploy/registry.test.ts +101 -0
- package/src/deploy/registry.ts +86 -0
- package/src/deploy/secrets.test.ts +129 -0
- package/src/deploy/secrets.ts +69 -0
- package/src/deploy/targets/docker-gha.test.ts +323 -0
- package/src/deploy/targets/docker-gha.ts +841 -0
- package/src/deploy/types.ts +153 -0
- package/src/errors.test.ts +42 -0
- package/src/errors.ts +69 -0
- package/src/events/store.test.ts +183 -0
- package/src/events/store.ts +201 -0
- package/src/index.ts +137 -0
- package/src/insights/quality-gates.ts +73 -0
- package/src/json.test.ts +28 -0
- package/src/json.ts +50 -0
- package/src/logging/color.ts +62 -0
- package/src/logging/logger.ts +60 -0
- package/src/logging/sanitizer.test.ts +36 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/client.test.ts +192 -0
- package/src/mail/client.ts +188 -0
- package/src/mail/store.test.ts +279 -0
- package/src/mail/store.ts +311 -0
- package/src/merge/lock.test.ts +88 -0
- package/src/merge/lock.ts +84 -0
- package/src/merge/queue.test.ts +136 -0
- package/src/merge/queue.ts +177 -0
- package/src/merge/resolver.test.ts +219 -0
- package/src/merge/resolver.ts +274 -0
- package/src/paths.ts +36 -0
- package/src/providers/apply.test.ts +90 -0
- package/src/providers/apply.ts +66 -0
- package/src/providers/registry.test.ts +74 -0
- package/src/providers/registry.ts +254 -0
- package/src/runtimes/claude.ts +313 -0
- package/src/runtimes/codex.ts +280 -0
- package/src/runtimes/cursor.ts +247 -0
- package/src/runtimes/gemini.ts +173 -0
- package/src/runtimes/mock.ts +71 -0
- package/src/runtimes/opencode.ts +259 -0
- package/src/runtimes/registry.test.ts +924 -0
- package/src/runtimes/registry.ts +63 -0
- package/src/runtimes/resolve.ts +45 -0
- package/src/runtimes/types.ts +97 -0
- package/src/scaffold.ts +68 -0
- package/src/secrets.test.ts +51 -0
- package/src/secrets.ts +78 -0
- package/src/serve/api.ts +667 -0
- package/src/serve/server.test.ts +433 -0
- package/src/serve/server.ts +271 -0
- package/src/serve/system.ts +90 -0
- package/src/serve/weather.ts +140 -0
- package/src/sessions/reaper.test.ts +162 -0
- package/src/sessions/reaper.ts +149 -0
- package/src/sessions/store.test.ts +351 -0
- package/src/sessions/store.ts +350 -0
- package/src/skills/distiller.test.ts +498 -0
- package/src/skills/distiller.ts +426 -0
- package/src/skills/feedback.test.ts +300 -0
- package/src/skills/feedback.ts +168 -0
- package/src/skills/lifecycle.ts +169 -0
- package/src/skills/retrieval.test.ts +421 -0
- package/src/skills/retrieval.ts +365 -0
- package/src/skills/safety.test.ts +335 -0
- package/src/skills/safety.ts +216 -0
- package/src/skills/store.test.ts +425 -0
- package/src/skills/store.ts +684 -0
- package/src/skills/types.ts +107 -0
- package/src/types.ts +442 -0
- package/src/utils/detect.test.ts +35 -0
- package/src/utils/detect.ts +82 -0
- package/src/version.test.ts +19 -0
- package/src/version.ts +7 -0
- package/src/wizard/setup.ts +254 -0
- package/src/worktree/manager.test.ts +181 -0
- package/src/worktree/manager.ts +229 -0
- package/templates/overlay.md.tmpl +102 -0
- package/ui/dist/assets/index-C7rXIMER.css +1 -0
- package/ui/dist/assets/index-W4kbr4by.js +4526 -0
- package/ui/dist/favicon.svg +21 -0
- package/ui/dist/index.html +16 -0
- package/ui/dist/logo-clay.svg +21 -0
- package/ui/dist/logo.svg +18 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { DEFAULT_CONFIG, serializeConfig, setProjectRootOverride } from "../config.ts";
|
|
6
|
+
import { setSecret } from "../secrets.ts";
|
|
7
|
+
import { createDoctorCommand } from "./doctor.ts";
|
|
8
|
+
|
|
9
|
+
let root: string;
|
|
10
|
+
|
|
11
|
+
/** Run a doctor subcommand and capture its JSON stdout (unwrapping the envelope). */
|
|
12
|
+
async function runDoctorJson(args: string[]): Promise<{ ok: boolean; checks: Check[] }> {
|
|
13
|
+
const original = process.stdout.write.bind(process.stdout);
|
|
14
|
+
let buffer = "";
|
|
15
|
+
process.stdout.write = ((chunk: unknown): boolean => {
|
|
16
|
+
buffer += typeof chunk === "string" ? chunk : String(chunk);
|
|
17
|
+
return true;
|
|
18
|
+
}) as typeof process.stdout.write;
|
|
19
|
+
try {
|
|
20
|
+
const program = createDoctorCommand();
|
|
21
|
+
program.exitOverride();
|
|
22
|
+
await program.parseAsync(["node", "doctor", ...args, "--json"]);
|
|
23
|
+
} finally {
|
|
24
|
+
process.stdout.write = original;
|
|
25
|
+
}
|
|
26
|
+
// jsonOutput wraps payloads as { ok: true, data: <payload> }.
|
|
27
|
+
const envelope = JSON.parse(buffer) as { ok: boolean; data: { ok: boolean; checks: Check[] } };
|
|
28
|
+
return envelope.data;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface Check {
|
|
32
|
+
category: string;
|
|
33
|
+
name: string;
|
|
34
|
+
ok: boolean;
|
|
35
|
+
detail: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function initProject(deployTarget?: string): void {
|
|
39
|
+
mkdirSync(join(root, ".agentplate"), { recursive: true });
|
|
40
|
+
const config = structuredClone(DEFAULT_CONFIG);
|
|
41
|
+
config.project.name = "doctor-test";
|
|
42
|
+
config.project.root = root;
|
|
43
|
+
if (deployTarget) {
|
|
44
|
+
config.deploy.default = deployTarget;
|
|
45
|
+
config.deploy.targets[deployTarget] = {
|
|
46
|
+
settings: { registry: "ghcr.io/acme" },
|
|
47
|
+
secretEnv: { GHCR_TOKEN: { fromEnv: "GHCR_TOKEN" } },
|
|
48
|
+
environments: ["preview", "production"],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
writeFileSync(join(root, ".agentplate", "config.yaml"), serializeConfig(config), "utf8");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
root = mkdtempSync(join(tmpdir(), "agentplate-doctor-"));
|
|
56
|
+
setProjectRootOverride(root);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
setProjectRootOverride(null);
|
|
61
|
+
rmSync(root, { recursive: true, force: true });
|
|
62
|
+
delete process.env.GHCR_TOKEN;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("doctor providers category", () => {
|
|
66
|
+
test("reports missing credentials before a key is set", async () => {
|
|
67
|
+
initProject();
|
|
68
|
+
const result = await runDoctorJson(["--category", "providers"]);
|
|
69
|
+
const creds = result.checks.find((c) => c.name === "credentials");
|
|
70
|
+
expect(creds?.ok).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("flips to ok once the provider key is present", async () => {
|
|
74
|
+
initProject();
|
|
75
|
+
setSecret(root, "ANTHROPIC_API_KEY", "sk-ant-doctor-test-value");
|
|
76
|
+
const result = await runDoctorJson(["--category", "providers"]);
|
|
77
|
+
const creds = result.checks.find((c) => c.name === "credentials");
|
|
78
|
+
expect(creds?.ok).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("doctor deploy category", () => {
|
|
83
|
+
test("reports a configured target with missing secrets", async () => {
|
|
84
|
+
initProject("docker-gha");
|
|
85
|
+
const result = await runDoctorJson(["--category", "deploy"]);
|
|
86
|
+
const target = result.checks.find((c) => c.name === "target docker-gha");
|
|
87
|
+
expect(target).toBeDefined();
|
|
88
|
+
expect(target?.ok).toBe(false);
|
|
89
|
+
expect(target?.detail).toContain("GHCR_TOKEN");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("target is ok once its secret is present", async () => {
|
|
93
|
+
initProject("docker-gha");
|
|
94
|
+
setSecret(root, "GHCR_TOKEN", "ghp_doctor_test_token_value_1234");
|
|
95
|
+
const result = await runDoctorJson(["--category", "deploy"]);
|
|
96
|
+
const target = result.checks.find((c) => c.name === "target docker-gha");
|
|
97
|
+
expect(target?.ok).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("default target check reflects config", async () => {
|
|
101
|
+
initProject("docker-gha");
|
|
102
|
+
const result = await runDoctorJson(["--category", "deploy"]);
|
|
103
|
+
const def = result.checks.find((c) => c.name === "default target");
|
|
104
|
+
expect(def?.detail).toBe("docker-gha");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agentplate doctor` — health checks for the Agentplate setup.
|
|
3
|
+
*
|
|
4
|
+
* Checks are grouped into categories. Phase 0/1 ship `core` (toolchain + repo +
|
|
5
|
+
* initialization) and `providers` (active provider + credential presence, by env
|
|
6
|
+
* var name only — values are never read into the report). Later phases register
|
|
7
|
+
* more categories (databases, deploy, …).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
import { findProjectRoot, isInitialized, loadConfig } from "../config.ts";
|
|
12
|
+
import { jsonOutput } from "../json.ts";
|
|
13
|
+
import { brand, printError, printHint, printInfo, printSuccess } from "../logging/color.ts";
|
|
14
|
+
import { getProviderSpec } from "../providers/registry.ts";
|
|
15
|
+
import { hasSecret } from "../secrets.ts";
|
|
16
|
+
import { commandOnPath, resolveArgv } from "../utils/detect.ts";
|
|
17
|
+
|
|
18
|
+
interface DoctorCheck {
|
|
19
|
+
category: string;
|
|
20
|
+
name: string;
|
|
21
|
+
ok: boolean;
|
|
22
|
+
detail: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function commandOutput(cmd: string, args: string[]): Promise<string | null> {
|
|
26
|
+
try {
|
|
27
|
+
const proc = Bun.spawn(resolveArgv([cmd, ...args]), { stdout: "pipe", stderr: "pipe" });
|
|
28
|
+
const code = await proc.exited;
|
|
29
|
+
if (code !== 0) return null;
|
|
30
|
+
return (await new Response(proc.stdout).text()).trim();
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function coreChecks(root: string): Promise<DoctorCheck[]> {
|
|
37
|
+
const checks: DoctorCheck[] = [];
|
|
38
|
+
checks.push({ category: "core", name: "bun", ok: true, detail: `v${Bun.version}` });
|
|
39
|
+
|
|
40
|
+
const gitVersion = await commandOutput("git", ["--version"]);
|
|
41
|
+
checks.push({
|
|
42
|
+
category: "core",
|
|
43
|
+
name: "git",
|
|
44
|
+
ok: gitVersion !== null,
|
|
45
|
+
detail: gitVersion ?? "not found on PATH",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const isRepo = (await commandOutput("git", ["rev-parse", "--is-inside-work-tree"])) === "true";
|
|
49
|
+
checks.push({
|
|
50
|
+
category: "core",
|
|
51
|
+
name: "git repository",
|
|
52
|
+
ok: isRepo,
|
|
53
|
+
detail: isRepo ? root : "not inside a git repository",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const initialized = isInitialized(root);
|
|
57
|
+
checks.push({
|
|
58
|
+
category: "core",
|
|
59
|
+
name: "agentplate initialized",
|
|
60
|
+
ok: initialized,
|
|
61
|
+
detail: initialized ? `${root}/.agentplate` : "run `agentplate setup` to get started",
|
|
62
|
+
});
|
|
63
|
+
return checks;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function providerChecks(root: string): Promise<DoctorCheck[]> {
|
|
67
|
+
if (!isInitialized(root)) {
|
|
68
|
+
return [
|
|
69
|
+
{
|
|
70
|
+
category: "providers",
|
|
71
|
+
name: "provider",
|
|
72
|
+
ok: false,
|
|
73
|
+
detail: "not initialized — run `agentplate setup`",
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
const config = loadConfig(root);
|
|
78
|
+
const providerId = config.activeProvider;
|
|
79
|
+
const providerConfig = config.providers[providerId];
|
|
80
|
+
const spec = getProviderSpec(providerId);
|
|
81
|
+
const checks: DoctorCheck[] = [];
|
|
82
|
+
|
|
83
|
+
checks.push({
|
|
84
|
+
category: "providers",
|
|
85
|
+
name: "active provider",
|
|
86
|
+
ok: providerConfig !== undefined,
|
|
87
|
+
detail: providerConfig
|
|
88
|
+
? `${spec?.label ?? providerId} (model: ${providerConfig.model ?? "unset"})`
|
|
89
|
+
: `"${providerId}" is not configured`,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Auth-mode-aware credentials check. Legacy configs (no authMode) are treated
|
|
93
|
+
// as api-key when a token env var is present, else none.
|
|
94
|
+
const authMode =
|
|
95
|
+
providerConfig?.authMode ??
|
|
96
|
+
(spec?.keyless ? "none" : providerConfig?.authTokenEnv ? "api-key" : "none");
|
|
97
|
+
const envVar = providerConfig?.authTokenEnv ?? spec?.authEnvVar;
|
|
98
|
+
|
|
99
|
+
if (authMode === "none") {
|
|
100
|
+
checks.push({
|
|
101
|
+
category: "providers",
|
|
102
|
+
name: "credentials",
|
|
103
|
+
ok: true,
|
|
104
|
+
detail: "no credentials required (local provider)",
|
|
105
|
+
});
|
|
106
|
+
} else if (authMode === "subscription") {
|
|
107
|
+
const cli = spec?.subscriptionRuntime ?? config.runtime.default;
|
|
108
|
+
const installed = await commandOnPath(cli);
|
|
109
|
+
checks.push({
|
|
110
|
+
category: "providers",
|
|
111
|
+
name: "credentials",
|
|
112
|
+
ok: installed,
|
|
113
|
+
detail: installed
|
|
114
|
+
? `subscription via \`${cli}\` (ensure it is logged in)`
|
|
115
|
+
: `subscription auth needs \`${cli}\` on PATH — install/login first`,
|
|
116
|
+
});
|
|
117
|
+
} else if (envVar) {
|
|
118
|
+
const present = hasSecret(root, envVar);
|
|
119
|
+
checks.push({
|
|
120
|
+
category: "providers",
|
|
121
|
+
name: "credentials",
|
|
122
|
+
ok: present,
|
|
123
|
+
detail: present
|
|
124
|
+
? `${envVar} is set`
|
|
125
|
+
: `${envVar} not found (add it via \`agentplate setup\` or the environment)`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return checks;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Deploy checks: report each configured deploy target and whether its required
|
|
133
|
+
* secrets are present (by env-var NAME only — values are never read here).
|
|
134
|
+
*/
|
|
135
|
+
function deployChecks(root: string): DoctorCheck[] {
|
|
136
|
+
if (!isInitialized(root)) return [];
|
|
137
|
+
const config = loadConfig(root);
|
|
138
|
+
const checks: DoctorCheck[] = [];
|
|
139
|
+
const defaultTarget = config.deploy.default;
|
|
140
|
+
checks.push({
|
|
141
|
+
category: "deploy",
|
|
142
|
+
name: "default target",
|
|
143
|
+
ok: true,
|
|
144
|
+
detail: defaultTarget || "(none — set with `agentplate target configure`)",
|
|
145
|
+
});
|
|
146
|
+
for (const [name, targetConfig] of Object.entries(config.deploy.targets)) {
|
|
147
|
+
const required = Object.values(targetConfig.secretEnv).map((b) => b.fromEnv);
|
|
148
|
+
const missing = required.filter((envVar) => !hasSecret(root, envVar));
|
|
149
|
+
checks.push({
|
|
150
|
+
category: "deploy",
|
|
151
|
+
name: `target ${name}`,
|
|
152
|
+
ok: missing.length === 0,
|
|
153
|
+
detail:
|
|
154
|
+
missing.length === 0
|
|
155
|
+
? `secrets present (${required.join(", ") || "none required"})`
|
|
156
|
+
: `missing secrets: ${missing.join(", ")}`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return checks;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function createDoctorCommand(): Command {
|
|
163
|
+
return new Command("doctor")
|
|
164
|
+
.description("Run health checks on the Agentplate setup")
|
|
165
|
+
.option("--category <name>", "run a single category: core | providers | deploy")
|
|
166
|
+
.option("--json", "output JSON")
|
|
167
|
+
.action(async (opts: { category?: string; json?: boolean }, command: Command) => {
|
|
168
|
+
const useJson = command.optsWithGlobals().json === true;
|
|
169
|
+
const root = findProjectRoot();
|
|
170
|
+
const category = opts.category;
|
|
171
|
+
|
|
172
|
+
const checks: DoctorCheck[] = [];
|
|
173
|
+
if (!category || category === "core") checks.push(...(await coreChecks(root)));
|
|
174
|
+
if (!category || category === "providers") checks.push(...(await providerChecks(root)));
|
|
175
|
+
if (!category || category === "deploy") checks.push(...deployChecks(root));
|
|
176
|
+
|
|
177
|
+
if (category && checks.length === 0) {
|
|
178
|
+
printError(`Unknown category "${category}". Try: core, providers, deploy`);
|
|
179
|
+
process.exitCode = 2;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const allOk = checks.every((c) => c.ok);
|
|
184
|
+
|
|
185
|
+
if (useJson) {
|
|
186
|
+
jsonOutput({ ok: allOk, checks });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
printInfo(brand("agentplate doctor"));
|
|
191
|
+
let currentCategory = "";
|
|
192
|
+
for (const check of checks) {
|
|
193
|
+
if (check.category !== currentCategory) {
|
|
194
|
+
currentCategory = check.category;
|
|
195
|
+
printInfo(`\n${currentCategory}`);
|
|
196
|
+
}
|
|
197
|
+
if (check.ok) {
|
|
198
|
+
printSuccess(`${check.name}: ${check.detail}`);
|
|
199
|
+
} else {
|
|
200
|
+
printError(`${check.name}: ${check.detail}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (!allOk) {
|
|
204
|
+
printHint("\nSome checks failed. Address the items above and re-run `agentplate doctor`.");
|
|
205
|
+
process.exitCode = 1;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agentplate init` — non-interactive scaffold.
|
|
3
|
+
*
|
|
4
|
+
* Creates `.agentplate/` with a sensible auto-detected config and exits. For an
|
|
5
|
+
* interactive provider/runtime walkthrough, use `agentplate setup` instead.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { DEFAULT_CONFIG, findProjectRoot, isInitialized } from "../config.ts";
|
|
10
|
+
import { jsonOutput } from "../json.ts";
|
|
11
|
+
import { brand, printHint, printInfo, printSuccess, printWarning } from "../logging/color.ts";
|
|
12
|
+
import { scaffoldAgentplateDir } from "../scaffold.ts";
|
|
13
|
+
import type { AgentplateConfig } from "../types.ts";
|
|
14
|
+
import { detectCanonicalBranch, detectDefaultRuntime, detectProjectName } from "../utils/detect.ts";
|
|
15
|
+
|
|
16
|
+
export interface InitOptions {
|
|
17
|
+
yes?: boolean;
|
|
18
|
+
name?: string;
|
|
19
|
+
json?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Build an auto-detected config for a fresh project (no secrets touched). */
|
|
23
|
+
export async function buildInitialConfig(
|
|
24
|
+
root: string,
|
|
25
|
+
nameOverride?: string,
|
|
26
|
+
): Promise<AgentplateConfig> {
|
|
27
|
+
const [name, canonicalBranch, runtime] = await Promise.all([
|
|
28
|
+
nameOverride ? Promise.resolve(nameOverride) : detectProjectName(root),
|
|
29
|
+
detectCanonicalBranch(root),
|
|
30
|
+
detectDefaultRuntime(),
|
|
31
|
+
]);
|
|
32
|
+
const config = structuredClone(DEFAULT_CONFIG);
|
|
33
|
+
config.project.name = name;
|
|
34
|
+
config.project.root = root;
|
|
35
|
+
config.project.canonicalBranch = canonicalBranch;
|
|
36
|
+
config.runtime.default = runtime;
|
|
37
|
+
return config;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function runInit(opts: InitOptions): Promise<AgentplateConfig> {
|
|
41
|
+
const root = findProjectRoot();
|
|
42
|
+
const already = isInitialized(root);
|
|
43
|
+
if (already && !opts.yes && !opts.json) {
|
|
44
|
+
printWarning(`Agentplate is already initialized at ${root}/.agentplate`);
|
|
45
|
+
printHint("Re-running will refresh config.yaml. Pass --yes to silence this notice.");
|
|
46
|
+
}
|
|
47
|
+
const config = await buildInitialConfig(root, opts.name);
|
|
48
|
+
scaffoldAgentplateDir(root, config);
|
|
49
|
+
return config;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createInitCommand(): Command {
|
|
53
|
+
return new Command("init")
|
|
54
|
+
.description("Initialize .agentplate/ with an auto-detected config (non-interactive)")
|
|
55
|
+
.option("-y, --yes", "skip the already-initialized notice")
|
|
56
|
+
.option("--name <name>", "set the project name (default: auto-detect)")
|
|
57
|
+
.option("--json", "output JSON")
|
|
58
|
+
.action(async (opts: InitOptions, command: Command) => {
|
|
59
|
+
const useJson = command.optsWithGlobals().json === true;
|
|
60
|
+
const config = await runInit({ ...opts, json: useJson });
|
|
61
|
+
if (useJson) {
|
|
62
|
+
jsonOutput({ initialized: true, root: config.project.root, config });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
printSuccess(`Initialized ${brand("Agentplate")} at ${config.project.root}/.agentplate`);
|
|
66
|
+
printInfo(` project: ${config.project.name}`);
|
|
67
|
+
printInfo(` branch: ${config.project.canonicalBranch}`);
|
|
68
|
+
printInfo(` runtime: ${config.runtime.default}`);
|
|
69
|
+
printHint("\nNext: run `agentplate setup` to choose your AI provider and add your API key.");
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agentplate log <event>` — record an agent lifecycle/tool event (hook target).
|
|
3
|
+
*
|
|
4
|
+
* Called by runtime hooks (e.g. PreToolUse/PostToolUse/Stop) to feed the event
|
|
5
|
+
* store that powers `agentplate status` and observability. Kept minimal in the
|
|
6
|
+
* basic core; session-end quality-gate scoring layers on in a later phase.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import { findProjectRoot, isInitialized } from "../config.ts";
|
|
11
|
+
import { ValidationError } from "../errors.ts";
|
|
12
|
+
import { createEventStore } from "../events/store.ts";
|
|
13
|
+
import { jsonOutput } from "../json.ts";
|
|
14
|
+
import { eventsDbPath } from "../paths.ts";
|
|
15
|
+
|
|
16
|
+
export function createLogCommand(): Command {
|
|
17
|
+
return new Command("log")
|
|
18
|
+
.description("Record an agent event (hook target)")
|
|
19
|
+
.argument("<event>", "event type, e.g. tool-start | tool-end | session-end")
|
|
20
|
+
.option("--agent <name>", "agent name", "unknown")
|
|
21
|
+
.option("--tool <name>", "tool name (for tool events)")
|
|
22
|
+
.option("--detail <json>", "JSON detail blob")
|
|
23
|
+
.option("--run <id>", "run id")
|
|
24
|
+
.option("--json", "output JSON")
|
|
25
|
+
.action(
|
|
26
|
+
(
|
|
27
|
+
event: string,
|
|
28
|
+
opts: { agent: string; tool?: string; detail?: string; run?: string; json?: boolean },
|
|
29
|
+
command: Command,
|
|
30
|
+
) => {
|
|
31
|
+
const useJson = command.optsWithGlobals().json === true;
|
|
32
|
+
const root = findProjectRoot();
|
|
33
|
+
if (!isInitialized(root)) {
|
|
34
|
+
throw new ValidationError("Not initialized. Run `agentplate setup` first.");
|
|
35
|
+
}
|
|
36
|
+
const events = createEventStore(eventsDbPath(root));
|
|
37
|
+
try {
|
|
38
|
+
const record = events.record({
|
|
39
|
+
agentName: opts.agent,
|
|
40
|
+
runId: opts.run ?? null,
|
|
41
|
+
type: event,
|
|
42
|
+
tool: opts.tool ?? null,
|
|
43
|
+
detail: opts.detail ?? null,
|
|
44
|
+
});
|
|
45
|
+
if (useJson) jsonOutput(record);
|
|
46
|
+
} finally {
|
|
47
|
+
events.close();
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agentplate mail` — inter-agent messaging over the SQLite mail bus.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands: send, check, list, read, reply, purge. Used by agents (from
|
|
5
|
+
* inside worktrees, via `--project`) and by operators.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { findProjectRoot, isInitialized } from "../config.ts";
|
|
10
|
+
import { ValidationError } from "../errors.ts";
|
|
11
|
+
import { jsonOutput } from "../json.ts";
|
|
12
|
+
import { muted, printInfo, printSuccess } from "../logging/color.ts";
|
|
13
|
+
import { createMailClient } from "../mail/client.ts";
|
|
14
|
+
import type { MailMessage, MailType } from "../types.ts";
|
|
15
|
+
|
|
16
|
+
function requireInit(): string {
|
|
17
|
+
const root = findProjectRoot();
|
|
18
|
+
if (!isInitialized(root)) {
|
|
19
|
+
throw new ValidationError("Not initialized. Run `agentplate setup` first.");
|
|
20
|
+
}
|
|
21
|
+
return root;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function printMessage(m: MailMessage): void {
|
|
25
|
+
printInfo(`${m.read ? " " : "•"} [${m.type}] ${m.from} → ${m.to}: ${m.subject}`);
|
|
26
|
+
printInfo(muted(` ${m.body.replace(/\n/g, "\n ")}`));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sendCommand(): Command {
|
|
30
|
+
return new Command("send")
|
|
31
|
+
.description("Send a message")
|
|
32
|
+
.requiredOption("--to <agent>", "recipient")
|
|
33
|
+
.requiredOption("--subject <text>", "subject")
|
|
34
|
+
.requiredOption("--body <text>", "body")
|
|
35
|
+
.option("--from <name>", "sender", "operator")
|
|
36
|
+
.option("--type <type>", "message type", "status")
|
|
37
|
+
.option("--priority <level>", "low|normal|high|urgent", "normal")
|
|
38
|
+
.option("--payload <json>", "structured JSON payload")
|
|
39
|
+
.option("--json", "output JSON")
|
|
40
|
+
.action(
|
|
41
|
+
(
|
|
42
|
+
opts: {
|
|
43
|
+
to: string;
|
|
44
|
+
subject: string;
|
|
45
|
+
body: string;
|
|
46
|
+
from: string;
|
|
47
|
+
type: string;
|
|
48
|
+
priority: string;
|
|
49
|
+
payload?: string;
|
|
50
|
+
json?: boolean;
|
|
51
|
+
},
|
|
52
|
+
command: Command,
|
|
53
|
+
) => {
|
|
54
|
+
const useJson = command.optsWithGlobals().json === true;
|
|
55
|
+
const root = requireInit();
|
|
56
|
+
const client = createMailClient(root);
|
|
57
|
+
try {
|
|
58
|
+
const message = client.send({
|
|
59
|
+
from: opts.from,
|
|
60
|
+
to: opts.to,
|
|
61
|
+
subject: opts.subject,
|
|
62
|
+
body: opts.body,
|
|
63
|
+
type: opts.type as MailType,
|
|
64
|
+
priority: opts.priority as MailMessage["priority"],
|
|
65
|
+
payload: opts.payload ?? null,
|
|
66
|
+
});
|
|
67
|
+
if (useJson) jsonOutput(message);
|
|
68
|
+
else printSuccess(`Sent ${message.id} to ${opts.to}`);
|
|
69
|
+
} finally {
|
|
70
|
+
client.close();
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function checkCommand(): Command {
|
|
77
|
+
return new Command("check")
|
|
78
|
+
.description("Check an agent's inbox")
|
|
79
|
+
.requiredOption("--agent <name>", "agent whose inbox to check")
|
|
80
|
+
.option("--inject", "format unread for prompt injection and mark read")
|
|
81
|
+
.option("--json", "output JSON")
|
|
82
|
+
.action((opts: { agent: string; inject?: boolean; json?: boolean }, command: Command) => {
|
|
83
|
+
const useJson = command.optsWithGlobals().json === true;
|
|
84
|
+
const root = requireInit();
|
|
85
|
+
const client = createMailClient(root);
|
|
86
|
+
try {
|
|
87
|
+
if (opts.inject) {
|
|
88
|
+
const text = client.checkInject(opts.agent);
|
|
89
|
+
if (useJson) jsonOutput({ injected: text });
|
|
90
|
+
else printInfo(text || muted("(no new mail)"));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const messages = client.check(opts.agent, { unreadOnly: true });
|
|
94
|
+
if (useJson) {
|
|
95
|
+
jsonOutput(messages);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (messages.length === 0) printInfo(muted("(no unread mail)"));
|
|
99
|
+
for (const m of messages) printMessage(m);
|
|
100
|
+
} finally {
|
|
101
|
+
client.close();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function listCommand(): Command {
|
|
107
|
+
return new Command("list")
|
|
108
|
+
.description("List messages")
|
|
109
|
+
.option("--from <name>")
|
|
110
|
+
.option("--to <name>")
|
|
111
|
+
.option("--unread", "only unread")
|
|
112
|
+
.option("--json", "output JSON")
|
|
113
|
+
.action(
|
|
114
|
+
(
|
|
115
|
+
opts: { from?: string; to?: string; unread?: boolean; json?: boolean },
|
|
116
|
+
command: Command,
|
|
117
|
+
) => {
|
|
118
|
+
const useJson = command.optsWithGlobals().json === true;
|
|
119
|
+
const root = requireInit();
|
|
120
|
+
const client = createMailClient(root);
|
|
121
|
+
try {
|
|
122
|
+
const messages = client.list({ from: opts.from, to: opts.to, unread: opts.unread });
|
|
123
|
+
if (useJson) jsonOutput(messages);
|
|
124
|
+
else for (const m of messages) printMessage(m);
|
|
125
|
+
} finally {
|
|
126
|
+
client.close();
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function readCommand(): Command {
|
|
133
|
+
return new Command("read")
|
|
134
|
+
.description("Mark a message as read")
|
|
135
|
+
.argument("<id>", "message id")
|
|
136
|
+
.action((id: string) => {
|
|
137
|
+
const root = requireInit();
|
|
138
|
+
const client = createMailClient(root);
|
|
139
|
+
try {
|
|
140
|
+
client.markRead(id);
|
|
141
|
+
printSuccess(`Marked ${id} read`);
|
|
142
|
+
} finally {
|
|
143
|
+
client.close();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function replyCommand(): Command {
|
|
149
|
+
return new Command("reply")
|
|
150
|
+
.description("Reply to a message in the same thread")
|
|
151
|
+
.argument("<id>", "message id to reply to")
|
|
152
|
+
.requiredOption("--body <text>", "reply body")
|
|
153
|
+
.option("--from <name>", "sender", "operator")
|
|
154
|
+
.action((id: string, opts: { body: string; from: string }) => {
|
|
155
|
+
const root = requireInit();
|
|
156
|
+
const client = createMailClient(root);
|
|
157
|
+
try {
|
|
158
|
+
const message = client.reply(id, opts.body, opts.from);
|
|
159
|
+
printSuccess(`Replied (${message.id})`);
|
|
160
|
+
} finally {
|
|
161
|
+
client.close();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function purgeCommand(): Command {
|
|
167
|
+
return new Command("purge")
|
|
168
|
+
.description("Delete old messages")
|
|
169
|
+
.option("--all", "delete everything")
|
|
170
|
+
.option("--days <n>", "delete older than N days")
|
|
171
|
+
.option("--agent <name>", "delete for one agent")
|
|
172
|
+
.action((opts: { all?: boolean; days?: string; agent?: string }) => {
|
|
173
|
+
const root = requireInit();
|
|
174
|
+
const client = createMailClient(root);
|
|
175
|
+
try {
|
|
176
|
+
const count = client.purge({
|
|
177
|
+
all: opts.all,
|
|
178
|
+
olderThanDays: opts.days ? Number(opts.days) : undefined,
|
|
179
|
+
agent: opts.agent,
|
|
180
|
+
});
|
|
181
|
+
printSuccess(`Purged ${count} message(s)`);
|
|
182
|
+
} finally {
|
|
183
|
+
client.close();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function createMailCommand(): Command {
|
|
189
|
+
return new Command("mail")
|
|
190
|
+
.description("Inter-agent messaging")
|
|
191
|
+
.addCommand(sendCommand())
|
|
192
|
+
.addCommand(checkCommand())
|
|
193
|
+
.addCommand(listCommand())
|
|
194
|
+
.addCommand(readCommand())
|
|
195
|
+
.addCommand(replyCommand())
|
|
196
|
+
.addCommand(purgeCommand());
|
|
197
|
+
}
|