@wardby/cli 0.1.0 → 0.2.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 CHANGED
@@ -1,5 +1,5 @@
1
1
  <div>
2
- <img align="left" hspace="24" src="docs/assets/brand/wardby-mascot.png" alt="Wardby guardian robot protecting an agent budget" width="280">
2
+ <img align="left" hspace="24" src="https://raw.githubusercontent.com/wardby/wardby/main/docs/assets/brand/wardby-mascot.png" alt="Wardby guardian robot protecting an agent budget" width="280">
3
3
  <h3><big><big>Wardby</big></big> <small><em>(pronounced&nbsp;“WARD&#8209;bee”)</em></small></h3>
4
4
  <h3>Autonomous agents, bounded by design.</h3>
5
5
  <p><strong>Most agent runners focus on helping a model complete a task. Wardby is the self-hosted control plane that decides whether the task should run, limits what it can access, and returns a reviewable outcome governed by explicit policy.</strong></p>
@@ -11,19 +11,26 @@
11
11
  <a href="#a-full-cycle-agent-from-one-conversation">Full-cycle example</a> ·
12
12
  <a href="#host-it-in-your-cloud">Deployments</a> ·
13
13
  <a href="#bring-your-own-observability">Observability</a> ·
14
- <a href="#quickstart">Quickstart</a> ·
14
+ <a href="https://github.com/wardby/wardby/blob/main/docs/getting-started.md">Getting started</a> ·
15
15
  <a href="#security-boundaries">Security</a>
16
16
  </p>
17
17
  </div>
18
18
  <br clear="left">
19
19
 
20
- <!-- GitHub does not expose live Actions badge images for private repositories. These capability badges link authenticated readers to the authoritative workflow. -->
21
-
22
- [![Security CI](https://img.shields.io/badge/security%20CI-GitHub%20Actions-2088FF?logo=githubactions&logoColor=white)](https://github.com/wardby/wardby/actions/workflows/security.yml?query=branch%3Amain)
20
+ <!-- The first four badges are live: the npm version, Node floor and license are
21
+ read from the published package, and the security badge shows the latest run of
22
+ the security workflow on main. The last three are capability labels describing
23
+ what that workflow enforces -- they are not status and never change colour.
24
+ Every URL is absolute because npm renders this README too, and a relative path
25
+ there resolves against npmjs.com instead of this repository. -->
26
+
27
+ [![npm](https://img.shields.io/npm/v/@wardby/cli)](https://www.npmjs.com/package/@wardby/cli)
28
+ [![Security checks](https://github.com/wardby/wardby/actions/workflows/security.yml/badge.svg?branch=main)](https://github.com/wardby/wardby/actions/workflows/security.yml?query=branch%3Amain)
29
+ [![Node](https://img.shields.io/node/v/@wardby/cli)](https://www.npmjs.com/package/@wardby/cli)
30
+ [![License](https://img.shields.io/npm/l/@wardby/cli)](https://github.com/wardby/wardby/blob/main/LICENSE)
23
31
  [![Dependency audit](https://img.shields.io/badge/dependencies-audit%20policy-0f766e.svg)](https://github.com/wardby/wardby/actions/workflows/security.yml?query=branch%3Amain)
24
32
  [![Container scan](https://img.shields.io/badge/container%20images-Trivy%20CRITICAL%20gate-0f766e.svg)](https://github.com/wardby/wardby/actions/workflows/security.yml?query=branch%3Amain)
25
33
  [![SBOM](https://img.shields.io/badge/SBOM-SPDX%20JSON-0f766e.svg)](https://github.com/wardby/wardby/actions/workflows/security.yml?query=branch%3Amain)
26
- [![License](https://img.shields.io/badge/license-Apache--2.0-0f766e.svg)](LICENSE)
27
34
 
28
35
  > **Project status:** the control plane, scheduler, budget groups, MCP server,
29
36
  > native agents, isolated Codex and Claude Code workers, GitHub draft-PR flow,
@@ -68,7 +75,7 @@ result.
68
75
 
69
76
  ## One control plane, the full lifecycle
70
77
 
71
- ![Wardby workflow: ask in Claude or Codex, define an agent through MCP, govern it in Wardby, execute it in isolation, and apply review policy to its outcome](docs/assets/wardby-workflow.svg)
78
+ ![Wardby workflow: ask in Claude or Codex, define an agent through MCP, govern it in Wardby, execute it in isolation, and apply review policy to its outcome](https://raw.githubusercontent.com/wardby/wardby/main/docs/assets/wardby-workflow.svg)
72
79
 
73
80
  Claude and Codex are the operator experience. Wardby is the durable system
74
81
  behind them: it stores agent definitions, triggers work, reserves budget,
@@ -126,17 +133,17 @@ The core does not import a cloud SDK. Jobs, model access, email, secrets,
126
133
  authentication, and object storage sit behind provider interfaces so operators
127
134
  can choose the infrastructure boundary that fits their environment.
128
135
 
129
- | Target | Current support |
130
- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
131
- | **Local Docker, VM, or on-premises** | Portable PostgreSQL and container workflow for development and self-hosting. |
132
- | **Production container baseline** | Separate runtime and migration images plus a Compose/Caddy reference boundary. |
133
- | **GCP** | Terraform reference for the control plane on Cloud Run and Cloud SQL. Cloud-native coding jobs and production observability are follow-on work. |
134
- | **AWS** | The portable runtime and Bedrock model adapter are available; a native AWS deployment module is planned. |
135
- | **Other clouds** | Run the production image and provide equivalent PostgreSQL, secrets, ingress, egress, and monitoring controls. |
136
-
137
- Start with [deployment targets](deploy/README.md), the
138
- [portable production boundary](deploy/production/README.md), or the
139
- [GCP setup guide](deploy/gcp/SETUP.md). Reference deployments are examples,
136
+ | Target | Current support |
137
+ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
138
+ | **Local Docker, VM, or on-premises** | Portable PostgreSQL and container workflow for development and self-hosting. |
139
+ | **Production container baseline** | Separate runtime and migration images plus a Compose/Caddy reference boundary. |
140
+ | **GKE Autopilot** | Supported GCP reference with private Cloud SQL, isolated Codex worker pods, HTTPS Gateway, and immutable images. |
141
+ | **AWS** | The portable runtime and Bedrock model adapter are available; a native AWS deployment module is planned. |
142
+ | **Other clouds** | Run the production image and provide equivalent PostgreSQL, secrets, ingress, egress, and monitoring controls. |
143
+
144
+ Start with [deployment targets](https://github.com/wardby/wardby/blob/main/deploy/README.md), the
145
+ [portable production boundary](https://github.com/wardby/wardby/blob/main/deploy/production/README.md), or the
146
+ [GKE getting-started guide](https://github.com/wardby/wardby/blob/main/docs/getting-started-gke.md). Reference deployments are examples,
140
147
  not a requirement to use one vendor.
141
148
 
142
149
  **A complete deployment runs more than `wardby mcp`.** `mcp` serves the MCP
@@ -172,7 +179,7 @@ npm run observability:down
172
179
  The included cloud deployments do not yet provision AWS or GCP collectors.
173
180
  Current metrics primarily cover the coding proxy; application/MCP metrics,
174
181
  production alerts, and SLOs remain operator responsibilities. See the
175
- [observability guide](docs/observability.md) for boundaries and retention.
182
+ [observability guide](https://github.com/wardby/wardby/blob/main/docs/observability.md) for boundaries and retention.
176
183
 
177
184
  ## Security boundaries
178
185
 
@@ -195,9 +202,9 @@ production alerts, and SLOs remain operator responsibilities. See the
195
202
  - **Sanitized operations:** lifecycle events and metrics exclude prompts,
196
203
  repository content, credentials, diffs, and raw worker output.
197
204
 
198
- Read the [runtime architecture](docs/architecture-runtime.md),
199
- [coding-worker isolation model](docs/coding-worker-isolation.md), and
200
- [security deployment guide](docs/security-deployment.md) before enabling a
205
+ Read the [runtime architecture](https://github.com/wardby/wardby/blob/main/docs/architecture-runtime.md),
206
+ [coding-worker isolation model](https://github.com/wardby/wardby/blob/main/docs/coding-worker-isolation.md), and
207
+ [security deployment guide](https://github.com/wardby/wardby/blob/main/docs/security-deployment.md) before enabling a
201
208
  production repository.
202
209
 
203
210
  Review policy is designed to support designated agents as well as people.
@@ -210,33 +217,38 @@ Requirements: Node.js 24 or newer, Docker, and one supported model-provider
210
217
  credential.
211
218
 
212
219
  ```sh
213
- git clone https://github.com/wardby/wardby.git
214
- cd wardby
215
- npm ci
216
- cp .env.example .env
220
+ npx --yes @wardby/cli@latest quickstart
217
221
  ```
218
222
 
219
- Edit `.env` and set `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`. Generate a local
220
- secret-encryption key with `openssl rand -hex 32` and use it as
221
- `SECRET_APP_KEY`. Then initialize the local database and start the stdio MCP
222
- server:
223
+ The guided command checks prerequisites, creates private project-local
224
+ configuration, starts PostgreSQL in Docker, applies migrations, and offers to
225
+ run a small agent with a `$1` maximum budget. It can also register Wardby as a
226
+ local MCP server in Codex, Claude Code, or both.
227
+
228
+ Read the [Getting started guide](https://github.com/wardby/wardby/blob/main/docs/getting-started.md) for unattended setup,
229
+ MCP usage, lifecycle commands, and the boundary between the native demo and
230
+ isolated coding agents. For a shared cloud installation, follow the full
231
+ [GKE getting-started guide](https://github.com/wardby/wardby/blob/main/docs/getting-started-gke.md).
232
+
233
+ ### Installing from npm
223
234
 
224
235
  ```sh
225
- npm run db:up
226
- npm run prisma:generate
227
- npm run prisma:migrate
228
- npm run cli -- mcp
236
+ npm install @wardby/cli
229
237
  ```
230
238
 
231
- Point Claude or Codex at `npm run cli -- mcp` with the repository as its working
232
- directory. The exact client configuration format differs, but both use Wardby's
233
- stdio transport locally. For shared or remote access, use Streamable HTTP with
234
- OAuth and the production boundary documented above.
239
+ `npm audit` will report a high-severity advisory in `deepmerge-ts`
240
+ ([GHSA-ggr8-5vv4-36mx](https://github.com/advisories/GHSA-ggr8-5vv4-36mx)). It
241
+ is reached only through Prisma's CLI while it loads your own Prisma
242
+ configuration, and Prisma has not patched it in the 6.x line. This repository
243
+ clears it with an override, but npm does not apply a package's overrides to the
244
+ projects that install it — so add the same entry to your own `package.json`:
245
+
246
+ ```json
247
+ "overrides": { "deepmerge-ts": "8.0.2" }
248
+ ```
235
249
 
236
- Once connected, ask the client to list the available Wardby tools, create an
237
- agent with a small budget, trigger it, and inspect the run. Coding-agent setup
238
- additionally requires a dedicated GitHub App, immutable worker images, and the
239
- proxy boundary described in the [local coding-agent setup guide](docs/coding-agent-setup.md).
250
+ That override is tested against Prisma 6 and the CLI; the reasoning and the
251
+ retirement plan are in [SR-009](https://github.com/wardby/wardby/blob/main/docs/security-deployment.md#images-and-dependency-exception).
240
252
 
241
253
  ## What is implemented
242
254
 
@@ -251,12 +263,12 @@ proxy boundary described in the [local coding-agent setup guide](docs/coding-age
251
263
  - In-process reconciliation and optional DBOS durable workflows.
252
264
  - Self-hosted or delegated OAuth for remote MCP access.
253
265
  - Prometheus metrics and provisioned Grafana dashboards for local operations.
254
- - Portable production images, a Compose/Caddy boundary, and a GCP control-plane
255
- Terraform reference.
266
+ - Portable production images, a Compose/Caddy boundary, and a GKE Autopilot
267
+ Terraform reference deployment.
256
268
 
257
269
  This project is under active development. Run the
258
- [release verification](docs/release-verification.md) checks and review the
259
- [security deployment guide](docs/security-deployment.md) before production use.
270
+ [release verification](https://github.com/wardby/wardby/blob/main/docs/release-verification.md) checks and review the
271
+ [security deployment guide](https://github.com/wardby/wardby/blob/main/docs/security-deployment.md) before production use.
260
272
 
261
273
  ## Repository map
262
274
 
@@ -273,10 +285,10 @@ docs/ Architecture, security, setup, and operator guidance
273
285
 
274
286
  ## License
275
287
 
276
- Apache License 2.0. See [LICENSE](LICENSE) for the license terms and
277
- [NOTICE](NOTICE) for attribution.
288
+ Apache License 2.0. See [LICENSE](https://github.com/wardby/wardby/blob/main/LICENSE) for the license terms and
289
+ [NOTICE](https://github.com/wardby/wardby/blob/main/NOTICE) for attribution.
278
290
 
279
- See [CONTRIBUTING.md](CONTRIBUTING.md) to contribute and [SECURITY.md](SECURITY.md)
291
+ See [CONTRIBUTING.md](https://github.com/wardby/wardby/blob/main/CONTRIBUTING.md) to contribute and [SECURITY.md](https://github.com/wardby/wardby/blob/main/SECURITY.md)
280
292
  to report a vulnerability privately.
281
293
 
282
- Built as an independent reimplementation. See [CLEANROOM.md](CLEANROOM.md).
294
+ Built as an independent reimplementation. See [CLEANROOM.md](https://github.com/wardby/wardby/blob/main/CLEANROOM.md).
@@ -2,15 +2,15 @@ services:
2
2
  postgres:
3
3
  image: postgres:16-alpine
4
4
  environment:
5
- POSTGRES_USER: wardby
6
- POSTGRES_PASSWORD: wardby
7
- POSTGRES_DB: wardby
5
+ POSTGRES_USER: ${WARDBY_POSTGRES_USER:-wardby}
6
+ POSTGRES_PASSWORD: ${WARDBY_POSTGRES_PASSWORD:-wardby}
7
+ POSTGRES_DB: ${WARDBY_POSTGRES_DB:-wardby}
8
8
  ports:
9
- - "55432:5432"
9
+ - "${WARDBY_POSTGRES_PORT:-55432}:5432"
10
10
  volumes:
11
11
  - wardby-postgres-data:/var/lib/postgresql/data
12
12
  healthcheck:
13
- test: ["CMD-SHELL", "pg_isready -U wardby -d wardby"]
13
+ test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
14
14
  interval: 5s
15
15
  timeout: 5s
16
16
  retries: 12
@@ -1 +1 @@
1
- export declare const CLI_USAGE = "usage:\n wardby agent create --name <n> --model <m> --prompt <p> --budget <usd> [--schedule \"<cron>\"] [--timezone <tz>] [--max-turns <n>]\n wardby agent list\n wardby agent schedule <name> --cron \"<expr>\" [--timezone <tz>] [--disable]\n wardby tool create --name <n> --description <d> --params <file> --code <file>\n wardby tool attach <tool-name> <agent-name>\n wardby tool detach <tool-name> <agent-name>\n wardby tool list [--agent <name>]\n wardby run <name>\n wardby runs [--agent <name>] [--limit N] [--status <s>]\n wardby coding preflight (JOB_LAUNCHER=docker or kubernetes)\n wardby coding cleanup --run-id <id>\n wardby scheduler [--scope default]\n wardby mcp (MCP_TRANSPORT=stdio|http selects the transport)\n wardby serve [--scope default] (mcp + scheduler + reconciler in one process; http only)\n wardby import <bundle-dir> --owner <subject> [--public] [--include-secrets --transfer-key <pem>] [--default-budget <usd>] [--dry-run] [--prefix <p>] [--on-conflict fail|skip|rename] [--allow-open-fetch]\n\noptions:\n -h, --help Show this help\n -v, --version Show the installed Wardby version";
1
+ export declare const CLI_USAGE = "usage:\n wardby quickstart [--provider openai|anthropic] [--model <m>] [--budget <usd>] [--client none|codex|claude|both] [--skip-demo] [--non-interactive --yes]\n wardby doctor\n wardby status\n wardby logs [--tail N] [--follow]\n wardby down [--volumes]\n wardby agent create --name <n> --model <m> --prompt <p> --budget <usd> [--schedule \"<cron>\"] [--timezone <tz>] [--max-turns <n>]\n wardby agent list\n wardby agent schedule <name> --cron \"<expr>\" [--timezone <tz>] [--disable]\n wardby tool create --name <n> --description <d> --params <file> --code <file>\n wardby tool attach <tool-name> <agent-name>\n wardby tool detach <tool-name> <agent-name>\n wardby tool list [--agent <name>]\n wardby run <name>\n wardby runs [--agent <name>] [--limit N] [--status <s>]\n wardby coding preflight (JOB_LAUNCHER=docker or kubernetes)\n wardby coding cleanup --run-id <id>\n wardby scheduler [--scope default]\n wardby mcp (MCP_TRANSPORT=stdio|http selects the transport)\n wardby serve [--scope default] (mcp + scheduler + reconciler in one process; http only)\n wardby import <bundle-dir> --owner <subject> [--public] [--include-secrets --transfer-key <pem>] [--default-budget <usd>] [--dry-run] [--prefix <p>] [--on-conflict fail|skip|rename] [--allow-open-fetch]\n\noptions:\n -h, --help Show this help\n -v, --version Show the installed Wardby version";
package/dist/cli-help.js CHANGED
@@ -1,4 +1,9 @@
1
1
  export const CLI_USAGE = `usage:
2
+ wardby quickstart [--provider openai|anthropic] [--model <m>] [--budget <usd>] [--client none|codex|claude|both] [--skip-demo] [--non-interactive --yes]
3
+ wardby doctor
4
+ wardby status
5
+ wardby logs [--tail N] [--follow]
6
+ wardby down [--volumes]
2
7
  wardby agent create --name <n> --model <m> --prompt <p> --budget <usd> [--schedule "<cron>"] [--timezone <tz>] [--max-turns <n>]
3
8
  wardby agent list
4
9
  wardby agent schedule <name> --cron "<expr>" [--timezone <tz>] [--disable]
package/dist/cli.js CHANGED
File without changes
package/dist/env.js CHANGED
@@ -10,4 +10,11 @@
10
10
  */
11
11
  import "./core/http-runtime.js";
12
12
  import dotenvFlow from "dotenv-flow";
13
- dotenvFlow.config();
13
+ import { join, resolve } from "node:path";
14
+ const projectDir = resolve(process.env.WARDBY_PROJECT_DIR || process.cwd());
15
+ // A quickstart installation owns this directory, so load it before an
16
+ // application's root env files. dotenv-flow never overwrites an existing
17
+ // process variable, preserving explicit shell/container configuration as the
18
+ // highest-priority source in either mode.
19
+ dotenvFlow.config({ path: join(projectDir, ".wardby"), silent: true });
20
+ dotenvFlow.config({ path: projectDir });
@@ -0,0 +1,30 @@
1
+ export type QuickstartProvider = "openai" | "anthropic";
2
+ export type McpClient = "none" | "codex" | "claude" | "both";
3
+ export interface QuickstartState {
4
+ version: 1;
5
+ projectDir: string;
6
+ composeProject: string;
7
+ postgresPort: number;
8
+ provider: QuickstartProvider;
9
+ model: string;
10
+ packageVersion: string;
11
+ createdAt: string;
12
+ }
13
+ export interface QuickstartPaths {
14
+ projectDir: string;
15
+ wardbyDir: string;
16
+ envFile: string;
17
+ stateFile: string;
18
+ }
19
+ export declare function resolveProjectDir(cwd?: string, env?: NodeJS.ProcessEnv): string;
20
+ export declare function quickstartPaths(projectDir: string): QuickstartPaths;
21
+ export declare function composeProjectName(projectDir: string): string;
22
+ export declare function parseEnvFile(contents: string): Record<string, string>;
23
+ export declare function readQuickstartEnv(paths: QuickstartPaths): Record<string, string>;
24
+ export declare function writeQuickstartEnv(paths: QuickstartPaths, values: Record<string, string>): void;
25
+ export declare function readQuickstartState(paths: QuickstartPaths): QuickstartState | undefined;
26
+ export declare function writeQuickstartState(paths: QuickstartPaths, state: QuickstartState): void;
27
+ export declare function generatedSecret(): string;
28
+ export declare function ensureWardbyIgnored(projectDir: string): void;
29
+ export declare function defaultModel(provider: QuickstartProvider): string;
30
+ export declare function providerKeyName(provider: QuickstartProvider): "OPENAI_API_KEY" | "ANTHROPIC_API_KEY";
@@ -0,0 +1,106 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { basename, join, resolve } from "node:path";
4
+ export function resolveProjectDir(cwd = process.cwd(), env = process.env) {
5
+ return resolve(env.WARDBY_PROJECT_DIR || cwd);
6
+ }
7
+ export function quickstartPaths(projectDir) {
8
+ const wardbyDir = join(projectDir, ".wardby");
9
+ return {
10
+ projectDir,
11
+ wardbyDir,
12
+ envFile: join(wardbyDir, ".env"),
13
+ stateFile: join(wardbyDir, "state.json"),
14
+ };
15
+ }
16
+ export function composeProjectName(projectDir) {
17
+ const slug = basename(projectDir)
18
+ .toLowerCase()
19
+ .replace(/[^a-z0-9]+/g, "-")
20
+ .replace(/^-+|-+$/g, "")
21
+ .slice(0, 30);
22
+ const digest = createHash("sha256").update(resolve(projectDir)).digest("hex").slice(0, 10);
23
+ return `wardby-${slug || "project"}-${digest}`;
24
+ }
25
+ export function parseEnvFile(contents) {
26
+ const values = {};
27
+ for (const rawLine of contents.split(/\r?\n/)) {
28
+ const line = rawLine.trim();
29
+ if (!line || line.startsWith("#"))
30
+ continue;
31
+ const equals = line.indexOf("=");
32
+ if (equals <= 0)
33
+ continue;
34
+ const key = line.slice(0, equals).trim();
35
+ const rawValue = line.slice(equals + 1).trim();
36
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
37
+ continue;
38
+ if (rawValue.startsWith('"') && rawValue.endsWith('"')) {
39
+ try {
40
+ values[key] = JSON.parse(rawValue);
41
+ continue;
42
+ }
43
+ catch {
44
+ // Preserve malformed quoted values verbatim so setup never destroys them.
45
+ }
46
+ }
47
+ values[key] = rawValue.startsWith("'") && rawValue.endsWith("'") ? rawValue.slice(1, -1) : rawValue;
48
+ }
49
+ return values;
50
+ }
51
+ export function readQuickstartEnv(paths) {
52
+ if (!existsSync(paths.envFile))
53
+ return {};
54
+ return parseEnvFile(readFileSync(paths.envFile, "utf8"));
55
+ }
56
+ export function writeQuickstartEnv(paths, values) {
57
+ mkdirSync(paths.wardbyDir, { recursive: true, mode: 0o700 });
58
+ const heading = [
59
+ "# Managed by `wardby quickstart`.",
60
+ "# Keep this file private; it contains local credentials and encryption keys.",
61
+ ];
62
+ const lines = Object.entries(values)
63
+ .sort(([left], [right]) => left.localeCompare(right))
64
+ .map(([key, value]) => `${key}=${JSON.stringify(value)}`);
65
+ writeFileSync(paths.envFile, `${[...heading, ...lines].join("\n")}\n`, { mode: 0o600 });
66
+ chmodSync(paths.wardbyDir, 0o700);
67
+ chmodSync(paths.envFile, 0o600);
68
+ }
69
+ export function readQuickstartState(paths) {
70
+ if (!existsSync(paths.stateFile))
71
+ return undefined;
72
+ const value = JSON.parse(readFileSync(paths.stateFile, "utf8"));
73
+ if (value.version !== 1 ||
74
+ typeof value.projectDir !== "string" ||
75
+ typeof value.composeProject !== "string" ||
76
+ typeof value.postgresPort !== "number" ||
77
+ (value.provider !== "openai" && value.provider !== "anthropic") ||
78
+ typeof value.model !== "string" ||
79
+ typeof value.packageVersion !== "string" ||
80
+ typeof value.createdAt !== "string") {
81
+ throw new Error(`invalid Wardby quickstart state: ${paths.stateFile}`);
82
+ }
83
+ return value;
84
+ }
85
+ export function writeQuickstartState(paths, state) {
86
+ mkdirSync(paths.wardbyDir, { recursive: true, mode: 0o700 });
87
+ writeFileSync(paths.stateFile, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
88
+ chmodSync(paths.stateFile, 0o600);
89
+ }
90
+ export function generatedSecret() {
91
+ return randomBytes(32).toString("hex");
92
+ }
93
+ export function ensureWardbyIgnored(projectDir) {
94
+ const gitignore = join(projectDir, ".gitignore");
95
+ const existing = existsSync(gitignore) ? readFileSync(gitignore, "utf8") : "";
96
+ if (existing.split(/\r?\n/).some((line) => line.trim() === ".wardby/"))
97
+ return;
98
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
99
+ writeFileSync(gitignore, `${existing}${prefix}\n# Wardby local runtime and credentials\n.wardby/\n`);
100
+ }
101
+ export function defaultModel(provider) {
102
+ return provider === "openai" ? "gpt-5.6-luna" : "claude-haiku-4-5";
103
+ }
104
+ export function providerKeyName(provider) {
105
+ return provider === "openai" ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY";
106
+ }
@@ -0,0 +1,5 @@
1
+ export declare function quickstartCommand(args: string[]): Promise<void>;
2
+ export declare function doctorCommand(args: string[]): Promise<void>;
3
+ export declare function statusCommand(args: string[]): Promise<void>;
4
+ export declare function logsCommand(args: string[]): Promise<void>;
5
+ export declare function downCommand(args: string[]): Promise<void>;
@@ -0,0 +1,536 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createRequire } from "node:module";
3
+ import { createServer } from "node:net";
4
+ import { readFileSync, statSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { parseArgs } from "node:util";
8
+ import { createInterface } from "node:readline/promises";
9
+ import { composeProjectName, defaultModel, ensureWardbyIgnored, generatedSecret, providerKeyName, quickstartPaths, readQuickstartEnv, readQuickstartState, resolveProjectDir, writeQuickstartEnv, writeQuickstartState, } from "./config.js";
10
+ const packageRoot = fileURLToPath(new URL("../../", import.meta.url));
11
+ const composeFile = join(packageRoot, "deploy/local/docker-compose.yml");
12
+ const schemaFile = join(packageRoot, "prisma/schema.prisma");
13
+ const wardbyBin = join(packageRoot, "bin/wardby.js");
14
+ const packageJson = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
15
+ const require = createRequire(import.meta.url);
16
+ const DEMO_AGENT = "hello-wardby";
17
+ const DEMO_PROMPT = "You are the Wardby quickstart agent. Reply with a short greeting, then explain in one sentence that Wardby admitted this run against an explicit maximum budget. Do not call tools.";
18
+ function runCommand(command, args, options = {}) {
19
+ const spawnOptions = {
20
+ cwd: options.cwd,
21
+ env: options.env,
22
+ encoding: "utf8",
23
+ stdio: options.inherit ? "inherit" : "pipe",
24
+ };
25
+ const result = spawnSync(command, args, spawnOptions);
26
+ if (result.error) {
27
+ return { status: 1, stdout: result.stdout ?? "", stderr: result.error.message };
28
+ }
29
+ return { status: result.status ?? 1, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
30
+ }
31
+ function commandFailure(label, result) {
32
+ const detail = (result.stderr || result.stdout).trim();
33
+ throw new Error(`${label} failed${detail ? `: ${detail}` : ""}`);
34
+ }
35
+ function runtimeEnv(paths, values = readQuickstartEnv(paths)) {
36
+ return { ...process.env, ...values, WARDBY_PROJECT_DIR: paths.projectDir };
37
+ }
38
+ function composeArgs(paths, state, args) {
39
+ return [
40
+ "compose",
41
+ "--project-name",
42
+ state.composeProject,
43
+ "--env-file",
44
+ paths.envFile,
45
+ "--file",
46
+ composeFile,
47
+ ...args,
48
+ ];
49
+ }
50
+ function runCompose(paths, state, args, options = {}) {
51
+ return runCommand("docker", composeArgs(paths, state, args), {
52
+ cwd: paths.projectDir,
53
+ env: runtimeEnv(paths),
54
+ inherit: options.inherit,
55
+ });
56
+ }
57
+ function nodeMajor() {
58
+ return Number(process.versions.node.split(".")[0]);
59
+ }
60
+ function assertPrerequisites() {
61
+ if (nodeMajor() < 24)
62
+ throw new Error(`Node.js 24 or newer is required; found ${process.versions.node}.`);
63
+ const docker = runCommand("docker", ["--version"]);
64
+ if (docker.status !== 0)
65
+ throw new Error("Docker is not installed or is not on PATH.");
66
+ const compose = runCommand("docker", ["compose", "version"]);
67
+ if (compose.status !== 0)
68
+ throw new Error("Docker Compose v2 is required (`docker compose`).");
69
+ const daemon = runCommand("docker", ["info"]);
70
+ if (daemon.status !== 0)
71
+ throw new Error("Docker is installed but its daemon is not available.");
72
+ }
73
+ async function portAvailable(port) {
74
+ return await new Promise((resolve) => {
75
+ const server = createServer();
76
+ server.unref();
77
+ server.once("error", () => resolve(false));
78
+ server.listen({ host: "0.0.0.0", port }, () => server.close(() => resolve(true)));
79
+ });
80
+ }
81
+ async function selectPostgresPort(firstPort = 55432) {
82
+ for (let port = firstPort; port <= 55532; port += 1) {
83
+ if (await portAvailable(port))
84
+ return port;
85
+ }
86
+ throw new Error("No available local port was found between 55432 and 55532.");
87
+ }
88
+ async function startPostgres(paths, state, config) {
89
+ for (let attempt = 0; attempt < 10; attempt += 1) {
90
+ const up = runCompose(paths, state, ["up", "--detach", "--wait", "--wait-timeout", "90"]);
91
+ if (up.status === 0)
92
+ return;
93
+ const output = `${up.stderr}\n${up.stdout}`;
94
+ if (!/port is already allocated|address already in use|bind for .* failed/i.test(output)) {
95
+ commandFailure("PostgreSQL startup", up);
96
+ }
97
+ // Docker Desktop's port forwarder is not always visible to a host socket
98
+ // probe. Clean up the failed container and converge on the next port.
99
+ runCompose(paths, state, ["down"]);
100
+ state.postgresPort = await selectPostgresPort(state.postgresPort + 1);
101
+ config.WARDBY_POSTGRES_PORT = String(state.postgresPort);
102
+ config.DATABASE_URL = databaseUrl(config, state.postgresPort);
103
+ writeQuickstartEnv(paths, config);
104
+ writeQuickstartState(paths, state);
105
+ console.log(`! Port was occupied; retrying PostgreSQL on ${state.postgresPort}`);
106
+ }
107
+ throw new Error("PostgreSQL startup could not find a usable port after 10 attempts.");
108
+ }
109
+ function databaseUrl(config, port) {
110
+ const user = encodeURIComponent(config.WARDBY_POSTGRES_USER);
111
+ const password = encodeURIComponent(config.WARDBY_POSTGRES_PASSWORD);
112
+ const database = encodeURIComponent(config.WARDBY_POSTGRES_DB);
113
+ return `postgresql://${user}:${password}@127.0.0.1:${port}/${database}`;
114
+ }
115
+ async function promptLine(question, fallback) {
116
+ const terminal = createInterface({ input: process.stdin, output: process.stdout });
117
+ try {
118
+ const answer = (await terminal.question(question)).trim();
119
+ return answer || fallback || "";
120
+ }
121
+ finally {
122
+ terminal.close();
123
+ }
124
+ }
125
+ async function promptYesNo(question, defaultYes) {
126
+ const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
127
+ const answer = (await promptLine(`${question}${suffix}`)).toLowerCase();
128
+ if (!answer)
129
+ return defaultYes;
130
+ return answer === "y" || answer === "yes";
131
+ }
132
+ async function promptSecret(question) {
133
+ if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) {
134
+ throw new Error("A provider key is required in the environment when stdin is not an interactive terminal.");
135
+ }
136
+ process.stdout.write(question);
137
+ process.stdin.setRawMode(true);
138
+ process.stdin.resume();
139
+ process.stdin.setEncoding("utf8");
140
+ return await new Promise((resolve, reject) => {
141
+ let value = "";
142
+ const finish = (error) => {
143
+ process.stdin.off("data", onData);
144
+ process.stdin.setRawMode(false);
145
+ process.stdin.pause();
146
+ process.stdout.write("\n");
147
+ if (error)
148
+ reject(error);
149
+ else
150
+ resolve(value);
151
+ };
152
+ const onData = (chunk) => {
153
+ for (const character of chunk) {
154
+ if (character === "\u0003")
155
+ return finish(new Error("cancelled"));
156
+ if (character === "\r" || character === "\n")
157
+ return finish();
158
+ if (character === "\u007f" || character === "\b")
159
+ value = value.slice(0, -1);
160
+ else if (character >= " ")
161
+ value += character;
162
+ }
163
+ };
164
+ process.stdin.on("data", onData);
165
+ });
166
+ }
167
+ function parseProvider(value) {
168
+ if (value === undefined)
169
+ return undefined;
170
+ if (value !== "openai" && value !== "anthropic") {
171
+ throw new Error(`--provider must be openai or anthropic; got "${value}".`);
172
+ }
173
+ return value;
174
+ }
175
+ function parseClient(value) {
176
+ if (value === undefined)
177
+ return undefined;
178
+ if (!["none", "codex", "claude", "both"].includes(value)) {
179
+ throw new Error(`--client must be none, codex, claude, or both; got "${value}".`);
180
+ }
181
+ return value;
182
+ }
183
+ async function chooseProvider(requested, previous, existing, nonInteractive) {
184
+ if (requested)
185
+ return requested;
186
+ if (previous)
187
+ return previous.provider;
188
+ if (existing.LLM_PROVIDER === "openai" || existing.LLM_PROVIDER === "anthropic")
189
+ return existing.LLM_PROVIDER;
190
+ if (process.env.OPENAI_API_KEY || existing.OPENAI_API_KEY)
191
+ return "openai";
192
+ if (process.env.ANTHROPIC_API_KEY || existing.ANTHROPIC_API_KEY)
193
+ return "anthropic";
194
+ if (nonInteractive)
195
+ throw new Error("--provider is required in non-interactive mode.");
196
+ const answer = (await promptLine("Model provider: [1] OpenAI, [2] Anthropic [1] ", "1")).toLowerCase();
197
+ if (answer === "1" || answer === "openai")
198
+ return "openai";
199
+ if (answer === "2" || answer === "anthropic")
200
+ return "anthropic";
201
+ throw new Error(`Unknown provider selection "${answer}".`);
202
+ }
203
+ function prismaCliPath() {
204
+ return require.resolve("prisma/build/index.js");
205
+ }
206
+ function applyMigrations(paths) {
207
+ const result = runCommand(process.execPath, [prismaCliPath(), "migrate", "deploy", "--schema", schemaFile], {
208
+ cwd: paths.projectDir,
209
+ env: runtimeEnv(paths),
210
+ });
211
+ if (result.status !== 0)
212
+ commandFailure("Database migration", result);
213
+ }
214
+ async function seedDemo(paths, state, budget) {
215
+ const previousDatabaseUrl = process.env.DATABASE_URL;
216
+ process.env.DATABASE_URL = readQuickstartEnv(paths).DATABASE_URL;
217
+ const { PrismaClient } = await import("@prisma/client");
218
+ const db = new PrismaClient();
219
+ try {
220
+ const existing = await db.agent.findUnique({ where: { name: DEMO_AGENT } });
221
+ if (existing && existing.systemPrompt !== DEMO_PROMPT) {
222
+ throw new Error(`An agent named "${DEMO_AGENT}" already exists and was not created by quickstart; rename it or use --skip-demo.`);
223
+ }
224
+ if (existing) {
225
+ await db.agent.update({
226
+ where: { name: DEMO_AGENT },
227
+ data: { model: state.model, budgetUsd: budget, maxTurns: 2 },
228
+ });
229
+ }
230
+ else {
231
+ await db.agent.create({
232
+ data: {
233
+ name: DEMO_AGENT,
234
+ model: state.model,
235
+ budgetUsd: budget,
236
+ maxTurns: 2,
237
+ systemPrompt: DEMO_PROMPT,
238
+ },
239
+ });
240
+ }
241
+ }
242
+ finally {
243
+ await db.$disconnect();
244
+ if (previousDatabaseUrl === undefined)
245
+ delete process.env.DATABASE_URL;
246
+ else
247
+ process.env.DATABASE_URL = previousDatabaseUrl;
248
+ }
249
+ }
250
+ function runDemo(paths) {
251
+ const result = runCommand(process.execPath, [wardbyBin, "run", DEMO_AGENT], {
252
+ cwd: paths.projectDir,
253
+ env: runtimeEnv(paths),
254
+ inherit: true,
255
+ });
256
+ if (result.status !== 0)
257
+ throw new Error("The sample agent run failed; run `wardby doctor` for diagnostics.");
258
+ }
259
+ function clientInstalled(command) {
260
+ return runCommand(command, ["--version"]).status === 0;
261
+ }
262
+ function configureOneMcpClient(client, paths) {
263
+ if (!clientInstalled(client)) {
264
+ console.warn(`! ${client} is not installed; skipped its MCP configuration.`);
265
+ return;
266
+ }
267
+ const existing = runCommand(client, ["mcp", "get", "wardby"], { cwd: paths.projectDir });
268
+ if (existing.status === 0) {
269
+ console.log(`✓ ${client} already has an MCP server named wardby.`);
270
+ return;
271
+ }
272
+ const packageSpec = `@wardby/cli@${packageJson.version}`;
273
+ const args = client === "codex"
274
+ ? [
275
+ "mcp",
276
+ "add",
277
+ "--env",
278
+ `WARDBY_PROJECT_DIR=${paths.projectDir}`,
279
+ "wardby",
280
+ "--",
281
+ "npx",
282
+ "--yes",
283
+ packageSpec,
284
+ "mcp",
285
+ ]
286
+ : [
287
+ "mcp",
288
+ "add",
289
+ "--scope",
290
+ "local",
291
+ "-e",
292
+ `WARDBY_PROJECT_DIR=${paths.projectDir}`,
293
+ "wardby",
294
+ "--",
295
+ "npx",
296
+ "--yes",
297
+ packageSpec,
298
+ "mcp",
299
+ ];
300
+ const result = runCommand(client, args, { cwd: paths.projectDir });
301
+ if (result.status !== 0)
302
+ commandFailure(`${client} MCP configuration`, result);
303
+ console.log(`✓ Configured Wardby MCP for ${client}.`);
304
+ }
305
+ function configureMcpClients(client, paths) {
306
+ if (client === "codex" || client === "both")
307
+ configureOneMcpClient("codex", paths);
308
+ if (client === "claude" || client === "both")
309
+ configureOneMcpClient("claude", paths);
310
+ }
311
+ async function chooseClient(nonInteractive) {
312
+ if (nonInteractive)
313
+ return "none";
314
+ const answer = (await promptLine("Configure MCP: [n]one, [c]odex, c[l]aude, [b]oth [n] ", "n")).toLowerCase();
315
+ if (answer === "" || answer === "n" || answer === "none")
316
+ return "none";
317
+ if (answer === "c" || answer === "codex")
318
+ return "codex";
319
+ if (answer === "l" || answer === "claude")
320
+ return "claude";
321
+ if (answer === "b" || answer === "both")
322
+ return "both";
323
+ throw new Error(`Unknown MCP client selection "${answer}".`);
324
+ }
325
+ export async function quickstartCommand(args) {
326
+ const { values } = parseArgs({
327
+ args,
328
+ options: {
329
+ provider: { type: "string" },
330
+ model: { type: "string" },
331
+ budget: { type: "string" },
332
+ client: { type: "string" },
333
+ "non-interactive": { type: "boolean" },
334
+ "skip-demo": { type: "boolean" },
335
+ yes: { type: "boolean", short: "y" },
336
+ },
337
+ });
338
+ const nonInteractive = values["non-interactive"] ?? false;
339
+ const skipDemo = values["skip-demo"] ?? false;
340
+ const budget = values.budget === undefined ? 1 : Number(values.budget);
341
+ if (!Number.isFinite(budget) || budget <= 0)
342
+ throw new Error(`--budget must be positive; got "${values.budget}".`);
343
+ if (nonInteractive && !skipDemo && !values.yes) {
344
+ throw new Error("Non-interactive demo execution requires --yes because it may incur provider charges.");
345
+ }
346
+ console.log("Wardby quickstart\n");
347
+ assertPrerequisites();
348
+ console.log(`✓ Node.js ${process.versions.node}`);
349
+ console.log("✓ Docker and Docker Compose are available");
350
+ const projectDir = resolveProjectDir();
351
+ const paths = quickstartPaths(projectDir);
352
+ const previous = readQuickstartState(paths);
353
+ const existing = readQuickstartEnv(paths);
354
+ const provider = await chooseProvider(parseProvider(values.provider), previous, existing, nonInteractive);
355
+ const model = values.model ?? (previous?.provider === provider ? previous.model : defaultModel(provider));
356
+ const keyName = providerKeyName(provider);
357
+ let providerKey = process.env[keyName] || existing[keyName];
358
+ if (!providerKey && !skipDemo) {
359
+ if (nonInteractive)
360
+ throw new Error(`${keyName} is required in non-interactive mode unless --skip-demo is used.`);
361
+ providerKey = await promptSecret(`${keyName}: `);
362
+ if (!providerKey)
363
+ throw new Error(`${keyName} cannot be empty.`);
364
+ }
365
+ const postgresPort = previous?.postgresPort ?? (await selectPostgresPort());
366
+ const postgresUser = existing.WARDBY_POSTGRES_USER || "wardby";
367
+ const postgresDatabase = existing.WARDBY_POSTGRES_DB || "wardby";
368
+ const postgresPassword = existing.WARDBY_POSTGRES_PASSWORD || generatedSecret();
369
+ const config = {
370
+ ...existing,
371
+ LLM_PROVIDER: provider,
372
+ LOCAL_PRINCIPAL: existing.LOCAL_PRINCIPAL || "local",
373
+ MCP_TRANSPORT: "stdio",
374
+ SECRET_APP_KEY: existing.SECRET_APP_KEY || generatedSecret(),
375
+ WARDBY_POSTGRES_DB: postgresDatabase,
376
+ WARDBY_POSTGRES_PASSWORD: postgresPassword,
377
+ WARDBY_POSTGRES_PORT: String(postgresPort),
378
+ WARDBY_POSTGRES_USER: postgresUser,
379
+ };
380
+ config.DATABASE_URL = databaseUrl(config, postgresPort);
381
+ if (providerKey)
382
+ config[keyName] = providerKey;
383
+ ensureWardbyIgnored(projectDir);
384
+ writeQuickstartEnv(paths, config);
385
+ const state = {
386
+ version: 1,
387
+ projectDir,
388
+ composeProject: previous?.composeProject ?? composeProjectName(projectDir),
389
+ postgresPort,
390
+ provider,
391
+ model,
392
+ packageVersion: packageJson.version,
393
+ createdAt: previous?.createdAt ?? new Date().toISOString(),
394
+ };
395
+ writeQuickstartState(paths, state);
396
+ console.log(`✓ Configuration ready in ${paths.wardbyDir}`);
397
+ await startPostgres(paths, state, config);
398
+ console.log(`✓ PostgreSQL is healthy on 127.0.0.1:${state.postgresPort}`);
399
+ applyMigrations(paths);
400
+ console.log("✓ Database migrations applied");
401
+ if (!skipDemo) {
402
+ await seedDemo(paths, state, budget);
403
+ console.log(`✓ Sample agent "${DEMO_AGENT}" is ready`);
404
+ const approved = values.yes || (await promptYesNo(`Run it now with a maximum budget of $${budget.toFixed(2)}?`, true));
405
+ if (approved)
406
+ runDemo(paths);
407
+ else
408
+ console.log(`Skipped the billed run. Start it later with: npx @wardby/cli@latest run ${DEMO_AGENT}`);
409
+ }
410
+ const client = parseClient(values.client) ?? (await chooseClient(nonInteractive));
411
+ if (client !== "none")
412
+ configureMcpClients(client, paths);
413
+ console.log("\nWardby is ready.");
414
+ console.log(" npx @wardby/cli@latest status");
415
+ console.log(" npx @wardby/cli@latest doctor");
416
+ console.log(" npx @wardby/cli@latest down");
417
+ }
418
+ async function databaseHealthy(paths) {
419
+ const env = readQuickstartEnv(paths);
420
+ if (!env.DATABASE_URL)
421
+ return false;
422
+ const previous = process.env.DATABASE_URL;
423
+ process.env.DATABASE_URL = env.DATABASE_URL;
424
+ const { PrismaClient } = await import("@prisma/client");
425
+ const db = new PrismaClient();
426
+ try {
427
+ await db.$queryRawUnsafe("SELECT 1");
428
+ return true;
429
+ }
430
+ catch {
431
+ return false;
432
+ }
433
+ finally {
434
+ await db.$disconnect();
435
+ if (previous === undefined)
436
+ delete process.env.DATABASE_URL;
437
+ else
438
+ process.env.DATABASE_URL = previous;
439
+ }
440
+ }
441
+ export async function doctorCommand(args) {
442
+ parseArgs({ args, options: {} });
443
+ const paths = quickstartPaths(resolveProjectDir());
444
+ const checks = [];
445
+ checks.push(["Node.js 24 or newer", nodeMajor() >= 24, process.versions.node]);
446
+ checks.push(["Docker CLI", runCommand("docker", ["--version"]).status === 0]);
447
+ checks.push(["Docker Compose v2", runCommand("docker", ["compose", "version"]).status === 0]);
448
+ checks.push(["Docker daemon", runCommand("docker", ["info"]).status === 0]);
449
+ let state;
450
+ try {
451
+ state = readQuickstartState(paths);
452
+ checks.push(["Quickstart state", state !== undefined, paths.stateFile]);
453
+ }
454
+ catch (error) {
455
+ checks.push(["Quickstart state", false, error instanceof Error ? error.message : String(error)]);
456
+ }
457
+ const env = readQuickstartEnv(paths);
458
+ const key = state ? providerKeyName(state.provider) : undefined;
459
+ checks.push(["Private configuration", Object.keys(env).length > 0, paths.envFile]);
460
+ if (Object.keys(env).length > 0) {
461
+ const mode = statSync(paths.envFile).mode & 0o777;
462
+ checks.push(["Configuration permissions", (mode & 0o077) === 0, mode.toString(8)]);
463
+ checks.push(["Secret encryption key", /^[a-f0-9]{64}$/i.test(env.SECRET_APP_KEY ?? "")]);
464
+ if (key)
465
+ checks.push([`${key} configured`, Boolean(env[key] || process.env[key])]);
466
+ }
467
+ if (state) {
468
+ const ps = runCompose(paths, state, ["ps", "--status", "running", "--quiet"]);
469
+ checks.push(["PostgreSQL container", ps.status === 0 && ps.stdout.trim().length > 0]);
470
+ checks.push(["Database connection", await databaseHealthy(paths)]);
471
+ const migrations = runCommand(process.execPath, [prismaCliPath(), "migrate", "status", "--schema", schemaFile], {
472
+ cwd: paths.projectDir,
473
+ env: runtimeEnv(paths),
474
+ });
475
+ checks.push(["Database migrations", migrations.status === 0]);
476
+ }
477
+ let failed = false;
478
+ for (const [name, ok, detail] of checks) {
479
+ failed ||= !ok;
480
+ console.log(`${ok ? "✓" : "✗"} ${name}${detail ? ` (${detail})` : ""}`);
481
+ }
482
+ if (failed)
483
+ process.exitCode = 1;
484
+ }
485
+ export async function statusCommand(args) {
486
+ parseArgs({ args, options: {} });
487
+ const paths = quickstartPaths(resolveProjectDir());
488
+ const state = readQuickstartState(paths);
489
+ if (!state)
490
+ throw new Error(`Wardby is not initialized in ${paths.projectDir}; run \`wardby quickstart\`.`);
491
+ console.log(`project: ${state.projectDir}`);
492
+ console.log(`version: ${state.packageVersion}`);
493
+ console.log(`provider/model: ${state.provider} / ${state.model}`);
494
+ console.log(`postgres: 127.0.0.1:${state.postgresPort}`);
495
+ const ps = runCompose(paths, state, ["ps"]);
496
+ if (ps.status !== 0)
497
+ commandFailure("Docker Compose status", ps);
498
+ process.stdout.write(ps.stdout);
499
+ const healthy = await databaseHealthy(paths);
500
+ console.log(`database: ${healthy ? "healthy" : "unavailable"}`);
501
+ if (!healthy)
502
+ process.exitCode = 1;
503
+ }
504
+ export async function logsCommand(args) {
505
+ const { values } = parseArgs({
506
+ args,
507
+ options: {
508
+ follow: { type: "boolean", short: "f" },
509
+ tail: { type: "string" },
510
+ },
511
+ });
512
+ const paths = quickstartPaths(resolveProjectDir());
513
+ const state = readQuickstartState(paths);
514
+ if (!state)
515
+ throw new Error(`Wardby is not initialized in ${paths.projectDir}; run \`wardby quickstart\`.`);
516
+ const composeLogArgs = ["logs", "--tail", values.tail ?? "100"];
517
+ if (values.follow)
518
+ composeLogArgs.push("--follow");
519
+ const result = runCompose(paths, state, composeLogArgs, { inherit: true });
520
+ if (result.status !== 0)
521
+ throw new Error("Docker Compose logs failed.");
522
+ }
523
+ export async function downCommand(args) {
524
+ const { values } = parseArgs({ args, options: { volumes: { type: "boolean" } } });
525
+ const paths = quickstartPaths(resolveProjectDir());
526
+ const state = readQuickstartState(paths);
527
+ if (!state)
528
+ throw new Error(`Wardby is not initialized in ${paths.projectDir}; nothing to stop.`);
529
+ const composeDownArgs = ["down"];
530
+ if (values.volumes)
531
+ composeDownArgs.push("--volumes");
532
+ const result = runCompose(paths, state, composeDownArgs, { inherit: true });
533
+ if (result.status !== 0)
534
+ throw new Error("Docker Compose shutdown failed.");
535
+ console.log(values.volumes ? "Wardby stopped and its local database volume was removed." : "Wardby stopped; data retained.");
536
+ }
@@ -8,13 +8,39 @@ function packageVersion() {
8
8
  }
9
9
  return packageJson.version;
10
10
  }
11
- const command = process.argv[2];
12
- if (command === undefined || command === "--help" || command === "-h") {
13
- process.stdout.write(`${CLI_USAGE}\n`);
14
- }
15
- else if (command === "--version" || command === "-v") {
16
- process.stdout.write(`${packageVersion()}\n`);
17
- }
18
- else {
19
- await import("./cli.js");
11
+ async function main() {
12
+ const command = process.argv[2];
13
+ if (command === undefined || command === "--help" || command === "-h") {
14
+ process.stdout.write(`${CLI_USAGE}\n`);
15
+ }
16
+ else if (command === "--version" || command === "-v") {
17
+ process.stdout.write(`${packageVersion()}\n`);
18
+ }
19
+ else if (command === "quickstart") {
20
+ const { quickstartCommand } = await import("./quickstart/index.js");
21
+ await quickstartCommand(process.argv.slice(3));
22
+ }
23
+ else if (command === "doctor") {
24
+ const { doctorCommand } = await import("./quickstart/index.js");
25
+ await doctorCommand(process.argv.slice(3));
26
+ }
27
+ else if (command === "status") {
28
+ const { statusCommand } = await import("./quickstart/index.js");
29
+ await statusCommand(process.argv.slice(3));
30
+ }
31
+ else if (command === "logs") {
32
+ const { logsCommand } = await import("./quickstart/index.js");
33
+ await logsCommand(process.argv.slice(3));
34
+ }
35
+ else if (command === "down") {
36
+ const { downCommand } = await import("./quickstart/index.js");
37
+ await downCommand(process.argv.slice(3));
38
+ }
39
+ else {
40
+ await import("./cli.js");
41
+ }
20
42
  }
43
+ main().catch((error) => {
44
+ console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
45
+ process.exitCode = 1;
46
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wardby/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Self-hosted control plane for budget-guarded AI agents.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/wardby/wardby#readme",
@@ -119,5 +119,8 @@
119
119
  "typescript": "^5.6.0",
120
120
  "typescript-eslint": "^8.69.0",
121
121
  "vitest": "^4.1.11"
122
+ },
123
+ "overrides": {
124
+ "deepmerge-ts": "8.0.2"
122
125
  }
123
126
  }
@@ -1 +0,0 @@
1
- export declare function assertSelfHostedReleased(): void;
@@ -1,4 +0,0 @@
1
- // Remove only after the database, browser, migration, and independent review gates pass.
2
- export function assertSelfHostedReleased() {
3
- throw new Error("Self-hosted OAuth issuance is quarantined pending the security release gates. Use delegating OAuth or stdio.");
4
- }