diff --git a/docs/code/configuration.md b/docs/code/configuration.md index 0301782..468e6bb 100644 --- a/docs/code/configuration.md +++ b/docs/code/configuration.md @@ -85,7 +85,7 @@ You can also pass file paths directly as arguments without setting `TASK_TRACKER **Personal / interactive:** a `GITHUB_TOKEN` (personal access token). That is enough for free CLI use from your terminal (`devintern TICKET-123`, `--create-pr`). -**Team / unattended automation:** a GitHub App (`GITHUB_APP_ID` plus a private key). That is what `@mention` matching, `devintern worker --listen` / webhooks, and `slug[bot]` commit attribution need so the bot has a shared team identity. Unattended runs also need a `LICENSE_KEY`. See [Pricing](https://devintern.com/pricing/). +**Team / unattended automation:** a GitHub App (`GITHUB_APP_ID` plus a private key). That is what `@mention` matching, `devintern webhook serve`, and `slug[bot]` commit attribution need so the bot has a shared team identity. Unattended runs also need a `LICENSE_KEY`. See [Pricing](https://devintern.com/pricing/). The two credentials are complementary, not drop-in replacements. A team setup that also uses GitHub Issues as the tracker still needs `GITHUB_TOKEN`. @@ -102,7 +102,7 @@ Set both when you run mention-driven automation and also use GitHub Issues as a **Precedence when both are set:** - CLI and PR creation use `GITHUB_TOKEN` -- `devintern worker --listen` and the webhook server prefer the App so the bot identity (`slug[bot]`) resolves +- `devintern webhook serve` prefers the App so the bot identity (`slug[bot]`) resolves Do not set `GITHUB_APP_ID` without `GITHUB_APP_PRIVATE_KEY_PATH` or `GITHUB_APP_PRIVATE_KEY_BASE64`. The ID alone is ignored for auth, but the worker treats it as "GitHub credentials present." @@ -120,7 +120,7 @@ GITHUB_TOKEN=your-github-token ### GitHub App Authentication -For team / unattended automation (`@mention` matching, webhook / `worker --listen`, `slug[bot]` commit attribution): +For team / unattended automation (`@mention` matching, `webhook serve`, `slug[bot]` commit attribution): ```bash GITHUB_APP_ID=123456 diff --git a/docs/code/github-integration.md b/docs/code/github-integration.md index a22cdea..2491f29 100644 --- a/docs/code/github-integration.md +++ b/docs/code/github-integration.md @@ -433,17 +433,17 @@ export WEBHOOK_DEBUG="true" # Verbose request/processing logging ### Start the Server -`devintern worker --listen` runs the webhook listener inside the worker daemon. The previous `devintern serve` command still works as a deprecated alias. +`devintern webhook serve` runs the advanced repo-local webhook listener. Keep it separate from the workspace worker so webhook delivery and tracker automation can be operated independently. The old `devintern worker --listen` combined mode and `devintern serve` alias remain temporarily available with deprecation warnings. ```bash # Development bun run src/webhook-server.ts # Production (after build) -devintern worker --listen --port 3000 +devintern webhook serve --port 3000 # With PM2 (process manager) -pm2 start "devintern worker --listen" --name devintern-webhooks +pm2 start "devintern webhook serve" --name devintern-webhooks ``` ### Systemd Service (Linux) @@ -467,7 +467,7 @@ Environment=WEBHOOK_SECRET=your-secret Environment=GITHUB_TOKEN=ghp_... Environment=WEBHOOK_AUTO_REPLY=true Environment=WEBHOOK_AUTO_REVIEW=true -ExecStart=/usr/local/bin/devintern worker --listen --port 3000 +ExecStart=/usr/local/bin/devintern webhook serve --port 3000 Restart=always RestartSec=10 @@ -537,7 +537,7 @@ curl https://webhooks.yourdomain.com/health Run with verbose logging: ```bash -WEBHOOK_DEBUG=true devintern worker --listen +WEBHOOK_DEBUG=true devintern webhook serve ``` --- @@ -577,7 +577,7 @@ export LICENSE_KEY="your-server-automation-key" export GITHUB_TOKEN="ghp_..." # or GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY_PATH # 4. Start server -devintern worker --listen & +devintern webhook serve & # 5. Start tunnel cloudflared tunnel run devintern-webhooks diff --git a/docs/code/worker.md b/docs/code/worker.md index bcd8b99..104f323 100644 --- a/docs/code/worker.md +++ b/docs/code/worker.md @@ -21,7 +21,7 @@ devintern worker init devintern worker ``` -`worker init` reuses tracker config from `devintern init` (or runs that subset if missing), writes a 1-repo [workspace](./workspaces.md), validates and stores the ready-tasks query, checks any automation license (Supporter or Team/Business), offers zero-port relay setup, and can generate a native user service for Linux or macOS. Polling is always on. `--listen` (direct webhooks) is an advanced path, not part of this wizard. +`worker init` reuses tracker config from `devintern init` (or runs that subset if missing), writes a 1-repo [workspace](./workspaces.md), validates and stores the ready-tasks query, checks any automation license (Supporter or Team/Business), offers zero-port relay setup, and can generate a native user service for Linux or macOS. Polling is always on. The repo-local direct webhook server is an advanced, separate service and is not part of this wizard. Or configure by hand and start directly: @@ -32,11 +32,11 @@ devintern worker # Override the workspace query for this process devintern worker --query "status=todo" -# Also run the GitHub webhook listener (direct webhooks; single-repo) -devintern worker --query "status=todo" --listen --no-workspace +# Advanced: run the repo-local GitHub webhook listener separately +devintern webhook serve ``` -`devintern serve` still works as a deprecated alias for `devintern worker --listen`. +`devintern worker --listen` preserves the old combined single-repo process for compatibility, but is deprecated. Run the workspace worker and `devintern webhook serve` as separate processes instead. `devintern serve` remains a deprecated alias for `devintern webhook serve`. ## Recurring automations @@ -155,9 +155,9 @@ The worker log is the diagnostic. Look for `[poll:]` (for Jira, `[poll: | Option | Description | | ------------------- | ------------------------------------------------------------------- | | `--query ` | Poll the tracker for ready tasks matching this query | -| `--listen` | Also run the GitHub webhook listener (direct webhooks) | -| `--port ` | Webhook listener port (default: 3000 or `WEBHOOK_PORT`) | -| `--host ` | Webhook listener host (default: 0.0.0.0 or `WEBHOOK_HOST`) | +| `--listen` | Deprecated combined repo-local webhook listener | +| `--port ` | Deprecated listener port used only with `--listen` | +| `--host ` | Deprecated listener host used only with `--listen` | | `--interval ` | Polling interval in seconds (default: 60 or `WORKER_POLL_INTERVAL`) | | `--ui` | Serve the local [observability dashboard](./dashboard.md) (default) | | `--no-ui` | Disable the dashboard for this worker process | @@ -173,7 +173,7 @@ In polling mode the worker also watches the pull requests it created (no webhook The watch list is scoped to the project the worker runs in: single-repo mode watches only PRs on that checkout's GitHub repo, and workspace mode only repos listed in `workspace.toml`. Registry entries for any other repo — typically left behind when a repository is renamed or transferred, or by an older checkout sharing the same `.devintern-code/` state — are unwatched automatically at startup instead of being polled (and failing auth) forever. -The regular polling requests use ETags, and GitHub does not count `304 Not Modified` responses against the API rate limit. The worker makes unconditional PR requests only once to hydrate state after startup and immediately before an eligible base-sync attempt. Comparison results are reused for each immutable base/head SHA pair. With `--listen`, review handling comes from webhooks instead and this poller stays off, so feedback is never handled twice. +The regular polling requests use ETags, and GitHub does not count `304 Not Modified` responses against the API rate limit. The worker makes unconditional PR requests only once to hydrate state after startup and immediately before an eligible base-sync attempt. Comparison results are reused for each immutable base/head SHA pair. The deprecated combined `--listen` mode disables this poller so feedback is never handled twice. ### Merge conflicts on the agent's PRs diff --git a/docs/code/workspaces.md b/docs/code/workspaces.md index f3b105a..5beb396 100644 --- a/docs/code/workspaces.md +++ b/docs/code/workspaces.md @@ -119,7 +119,7 @@ devintern worker --workspace /path/to/workspace.toml devintern worker --no-workspace # force single-repo mode in the current repo ``` -The fleet query comes from `[defaults].task_query`, or `--query` / `WORKER_TASK_QUERY` to override. A workspace with automations can omit the query and run as an automation-only worker. `--listen` (direct webhooks) is single-repo and cannot be combined with workspace mode. Workspace and automation configuration is loaded at startup; restart the worker after editing it. Schedule state and leases for automations live in the central workspace database. +The fleet query comes from `[defaults].task_query`, or `--query` / `WORKER_TASK_QUERY` to override. A workspace with automations can omit the query and run as an automation-only worker. Direct webhooks are an advanced repo-local service: run `devintern webhook serve` from that repository as a separate process. Workspace and automation configuration is loaded at startup; restart the worker after editing it. Schedule state and leases for automations live in the central workspace database. `devintern worker init` can generate a user-level systemd unit on Linux or launchd agent on macOS. One service runs the whole workspace. For a hand-written Linux unit: diff --git a/packages/code/CHANGELOG.md b/packages/code/CHANGELOG.md index 93ef9a0..81ec052 100644 --- a/packages/code/CHANGELOG.md +++ b/packages/code/CHANGELOG.md @@ -6,6 +6,7 @@ - **`devintern worker init` is the complete unattended setup**: the wizard reuses tracker config from `devintern init` (or runs that subset), imports the current repo into `~/.devintern/workspace.toml` as a 1-repo workspace, dry-runs the ready-tasks query into `[defaults].task_query`, checks any automation license, offers zero-port relay pairing, and generates a user-level systemd unit or macOS launchd agent. It no longer asks about `--listen` or writes worker env vars - **Worker dashboard is on by default**: `devintern worker` serves localhost:4400 unless `--no-ui` is passed; dashboard startup failures no longer stop task processing +- **Direct webhooks have a dedicated command**: `devintern webhook serve` is the canonical advanced repo-local listener. `devintern worker --listen` keeps its legacy combined behavior for compatibility but is deprecated, and `devintern serve` is now a deprecated alias for the dedicated command ### Fixed diff --git a/packages/code/src/index.ts b/packages/code/src/index.ts index 14b4be1..6e30707 100755 --- a/packages/code/src/index.ts +++ b/packages/code/src/index.ts @@ -470,6 +470,72 @@ function loadSupabaseConfig() { return createDefaultSupabaseAuthConfig(join(configDir, ".auth-session.json")); } +function printWebhookHelp(): void { + console.log("Usage: devintern webhook "); + console.log(""); + console.log("Run advanced direct-webhook services. Relay is recommended for normal workers."); + console.log(""); + console.log("Commands:"); + console.log(" serve Start the repo-local GitHub webhook server"); + console.log(""); + console.log("Run 'devintern webhook serve --help' for command-specific options."); +} + +function printWebhookServeHelp(): void { + console.log("Usage: devintern webhook serve [options]"); + console.log(""); + console.log("Start the repo-local webhook server for GitHub PR events."); + console.log(""); + console.log("Options:"); + console.log(" --port Port to listen on (default: 3000, or WEBHOOK_PORT env var)"); + console.log(" --host Host to bind to (default: 0.0.0.0, or WEBHOOK_HOST env var)"); + console.log(" -h, --help Display this help message"); + console.log(""); + console.log("Environment variables:"); + console.log(" WEBHOOK_SECRET (required) Secret for verifying GitHub webhook signatures"); + console.log(" WEBHOOK_PORT Port to listen on (default: 3000)"); + console.log(" WEBHOOK_HOST Host to bind to (default: 0.0.0.0)"); + console.log(" WEBHOOK_AUTO_REPLY Set to 'true' to automatically reply to review comments"); + console.log(" WEBHOOK_VALIDATE_IP Set to 'true' to only accept requests from GitHub IPs"); + console.log(" WEBHOOK_DEBUG Set to 'true' for verbose logging"); +} + +async function runWebhookServeCommand(args: string[]): Promise { + let portOverride: number | undefined; + let hostOverride: string | undefined; + + for (let i = 0; i < args.length; i++) { + if (args[i] === "--port" && args[i + 1]) { + portOverride = parseInt(args[i + 1], 10); + i++; + } else if (args[i] === "--host" && args[i + 1]) { + hostOverride = args[i + 1]; + i++; + } else if (args[i] === "--help" || args[i] === "-h") { + printWebhookServeHelp(); + return; + } else { + console.error(`❌ Unknown webhook serve option: ${args[i]}`); + console.error(" Run 'devintern webhook serve --help' for usage."); + process.exitCode = 1; + return; + } + } + + loadedEnvPath = loadEnvironment(); + const port = portOverride ?? parseInt(process.env.WEBHOOK_PORT || "3000", 10); + const host = hostOverride ?? (process.env.WEBHOOK_HOST || "0.0.0.0"); + const licenseResult = await checkLicense({ + productKey: "devintern/code", + supabaseConfig: loadSupabaseConfig(), + requireAutomation: true, + }); + requireLicense(licenseResult); + + const { startWebhookServer } = await import("./webhook-server"); + await startWebhookServer({ port, host }); +} + // Sentry error tracking — uses the baked-in DevIntern DSN unless SENTRY_DISABLED=1. let sentryInitialized = false; function initSentryOnce(): void { @@ -505,8 +571,8 @@ if (process.argv[2] === "init") { process.exit(0); })(); } else if (process.argv[2] === "worker") { - // Handle worker command - long-running daemon (webhook listener now; - // polling acquirers register here as they land) + // Handle worker command - long-running workspace daemon. Deprecated + // --listen keeps the previous combined single-repo behavior temporarily. (async () => { loadedEnvPath = loadEnvironment(); @@ -623,7 +689,7 @@ if (process.argv[2] === "init") { console.log(" (~/.devintern/workspace.toml, or the given path)."); console.log(" Auto-enabled when a workspace exists."); console.log(" --no-workspace Ignore an existing workspace; single-repo mode"); - console.log(" --listen Also run the GitHub webhook listener (direct webhooks)"); + console.log(" --listen Deprecated: also run the repo-local webhook listener"); console.log(" --port Webhook listener port (default: 3000 or WEBHOOK_PORT)"); console.log( " --host Webhook listener host (default: 0.0.0.0 or WEBHOOK_HOST)", @@ -657,7 +723,28 @@ if (process.argv[2] === "init") { const { hasWorkspace, resolveWorkspaceDir, workspaceEnvPath } = await import("./lib/workspace/paths"); - const workspaceMode = workspaceFlag || (!noWorkspace && hasWorkspace()); + const { resolveWorkerWorkspaceMode } = await import("./lib/worker-mode"); + const selectedWorkerMode = resolveWorkerWorkspaceMode({ + workspaceRequested: workspaceFlag, + noWorkspace, + listen, + workspaceExists: hasWorkspace(), + }); + if (selectedWorkerMode.conflict === "workspace-listen") { + console.error( + "❌ --workspace and --listen cannot be combined.\n" + + " Run `devintern worker --workspace ...` and `devintern webhook serve` as separate processes.", + ); + process.exit(1); + } + if (listen) { + console.warn( + "⚠️ `devintern worker --listen` is deprecated.\n" + + " Run `devintern worker` and `devintern webhook serve` as separate processes.\n" + + " Continuing in legacy single-repo mode for this release.", + ); + } + const workspaceMode = selectedWorkerMode.workspace; // Workspace credentials must be available before the license gate. This // matters for native services, whose working directory is the workspace @@ -699,14 +786,6 @@ if (process.argv[2] === "init") { }); return; } - if (workspaceMode && listen) { - console.error( - "❌ --listen (direct webhooks) is single-repo and cannot combine with workspace mode.\n" + - " Run `devintern worker --listen --no-workspace` inside the repo instead.", - ); - process.exit(1); - } - const { startWorker } = await import("./worker"); const { WorkerState } = await import("./lib/worker-state"); const { WebhookQueue, resolveQueueDbPath, LEGACY_DB_PATH } = @@ -1187,70 +1266,26 @@ if (process.argv[2] === "init") { process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown); })(); -} else if (process.argv[2] === "serve") { - // Handle serve command - start webhook server - // Deprecated alias for `devintern worker --listen`. +} else if (process.argv[2] === "webhook") { (async () => { - console.warn( - "⚠️ `devintern serve` is deprecated; use `devintern worker --listen` instead.\n" + - " The worker daemon also supports tracker polling (no webhook setup needed).", - ); - - // Load environment for webhook server - loadedEnvPath = loadEnvironment(); - - // Parse serve-specific options - const args = process.argv.slice(3); - let port = parseInt(process.env.WEBHOOK_PORT || "3000", 10); - let host = process.env.WEBHOOK_HOST || "0.0.0.0"; - - for (let i = 0; i < args.length; i++) { - if (args[i] === "--port" && args[i + 1]) { - port = parseInt(args[i + 1], 10); - i++; - } else if (args[i] === "--host" && args[i + 1]) { - host = args[i + 1]; - i++; - } else if (args[i] === "--help" || args[i] === "-h") { - console.log("Usage: devintern serve [options]"); - console.log(""); - console.log("Start the webhook server to automatically address PR review feedback"); - console.log(""); - console.log("Options:"); - console.log(" --port Port to listen on (default: 3000, or WEBHOOK_PORT env var)"); - console.log(" --host Host to bind to (default: 0.0.0.0, or WEBHOOK_HOST env var)"); - console.log(" -h, --help Display this help message"); - console.log(""); - console.log("Environment variables:"); - console.log( - " WEBHOOK_SECRET (required) Secret for verifying GitHub webhook signatures", - ); - console.log(" WEBHOOK_PORT Port to listen on (default: 3000)"); - console.log(" WEBHOOK_HOST Host to bind to (default: 0.0.0.0)"); - console.log( - " WEBHOOK_AUTO_REPLY Set to 'true' to automatically reply to review comments", - ); - console.log(" WEBHOOK_VALIDATE_IP Set to 'true' to only accept requests from GitHub IPs"); - console.log(" WEBHOOK_DEBUG Set to 'true' for verbose logging"); - console.log(""); - console.log("See docs/WEBHOOK-DEPLOYMENT.md for deployment instructions."); - process.exit(0); - } + const command = process.argv[3]; + if (!command || command === "--help" || command === "-h") { + printWebhookHelp(); + return; } - - // License check — the webhook server is unattended automation, so it - // always requires an automation license. - const supabaseConfig = loadSupabaseConfig(); - const licenseResult = await checkLicense({ - productKey: "devintern/code", - supabaseConfig, - requireAutomation: true, - }); - requireLicense(licenseResult); - - // Import and start webhook server - const { startWebhookServer } = await import("./webhook-server"); - startWebhookServer({ port, host }); + if (command !== "serve") { + console.error(`❌ Unknown webhook command: ${command}`); + console.error(" Run 'devintern webhook --help' for usage."); + process.exitCode = 1; + return; + } + await runWebhookServeCommand(process.argv.slice(4)); + })(); +} else if (process.argv[2] === "serve") { + // Deprecated alias for `devintern webhook serve`. + (async () => { + console.warn("⚠️ `devintern serve` is deprecated; use `devintern webhook serve` instead."); + await runWebhookServeCommand(process.argv.slice(3)); })(); } else if (process.argv[2] === "address-review") { // Handle address-review command - manually address PR review feedback @@ -1589,10 +1624,11 @@ Subcommands: init Initialize .devintern-code configuration in current directory Interactive wizard in a terminal; pass --yes (or --no-interactive) to write the config templates without prompts - worker Run the worker daemon (webhook listener via --listen); + worker Run the workspace worker daemon; 'worker init' writes a workspace and ready-tasks query dashboard Serve the local observability dashboard (run history and stats) - serve Deprecated alias for 'worker --listen' + webhook serve Start the advanced repo-local direct-webhook server + serve Deprecated alias for 'webhook serve' address-review Address review feedback on an existing pull request resolve-conflicts Merge a PR's base branch into it, resolving conflicts login [method] Sign in (github | google | x | email; prompts if omitted) @@ -1612,6 +1648,7 @@ const isSubcommand = [ "worker", "dashboard", "workspace", + "webhook", "serve", "address-review", "resolve-conflicts", diff --git a/packages/code/src/lib/init-scaffold.ts b/packages/code/src/lib/init-scaffold.ts index 7e21941..ca4e7f8 100644 --- a/packages/code/src/lib/init-scaffold.ts +++ b/packages/code/src/lib/init-scaffold.ts @@ -192,7 +192,7 @@ AGENT_HARNESS=claude-code # # GitHub App (GITHUB_APP_ID + private key) # Team / unattended automation: @mention matching on any PR (worker -# mention sweep and webhook / devintern worker --listen) and slug[bot] +# mention sweep and devintern webhook serve) and slug[bot] # commit attribution. # Also creates PRs when no GITHUB_TOKEN is set. # Both GITHUB_APP_ID and a private key are required; ID alone is ignored. @@ -208,7 +208,7 @@ AGENT_HARNESS=claude-code # # Precedence when both are set: # CLI / PR creation: GITHUB_TOKEN is used. -# worker --listen / webhook server: the App is used (bot identity). +# webhook serve: the App is used (bot identity). # Bitbucket app password for creating pull requests # Create at: https://bitbucket.org/account/settings/app-passwords/ diff --git a/packages/code/src/lib/worker-init.ts b/packages/code/src/lib/worker-init.ts index 2298b16..9da09b4 100644 --- a/packages/code/src/lib/worker-init.ts +++ b/packages/code/src/lib/worker-init.ts @@ -87,27 +87,27 @@ export function upsertEnvVars(content: string, vars: Record): st /** * Render a systemd service unit for the worker. * - * @param options - Binary path, working directory, and whether to pass --listen + * @param options - Binary path, working directory, and whether to run the direct webhook service */ export function renderSystemdUnit(options: { execPath: string; projectDir: string; listen?: boolean; }): string { - const listenFlag = options.listen ? " --listen" : ""; + const command = options.listen ? "webhook serve" : "worker"; const quote = (value: string) => /^[A-Za-z0-9_./:-]+$/.test(value) ? value.replace(/%/g, "%%") : `"${value.replace(/%/g, "%%").replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; return `[Unit] -Description=devintern worker (${options.projectDir}) +Description=devintern ${options.listen ? "webhook server" : "worker"} (${options.projectDir}) After=network-online.target Wants=network-online.target [Service] Type=simple WorkingDirectory=${quote(options.projectDir)} -ExecStart=${quote(options.execPath)} worker${listenFlag} +ExecStart=${quote(options.execPath)} ${command} Restart=on-failure RestartSec=30 diff --git a/packages/code/src/lib/worker-mode.ts b/packages/code/src/lib/worker-mode.ts new file mode 100644 index 0000000..e93a8f1 --- /dev/null +++ b/packages/code/src/lib/worker-mode.ts @@ -0,0 +1,26 @@ +export interface WorkerWorkspaceModeOptions { + workspaceRequested: boolean; + noWorkspace: boolean; + listen: boolean; + workspaceExists: boolean; +} + +export interface WorkerWorkspaceMode { + workspace: boolean; + conflict?: "workspace-listen"; +} + +/** Resolve legacy single-repo versus workspace mode before worker startup. */ +export function resolveWorkerWorkspaceMode( + options: WorkerWorkspaceModeOptions, +): WorkerWorkspaceMode { + if (options.workspaceRequested && options.listen) { + return { workspace: false, conflict: "workspace-listen" }; + } + + return { + workspace: + options.workspaceRequested || + (!options.noWorkspace && !options.listen && options.workspaceExists), + }; +} diff --git a/packages/code/src/worker.ts b/packages/code/src/worker.ts index f549da9..a4e07f9 100644 --- a/packages/code/src/worker.ts +++ b/packages/code/src/worker.ts @@ -7,8 +7,9 @@ * - Mode 1 (polling): per-tracker change detectors feed the queue. No public * endpoint, no DevIntern infrastructure. (Acquirers register here as they * land; the detect-then-evaluate loop ships separately.) - * - Mode 3 (direct webhooks): `--listen` runs the existing webhook server - * inside the daemon (the previous `devintern serve` behavior). + * - Mode 3 compatibility (direct webhooks): deprecated `--listen` runs the + * repo-local webhook server inside the daemon. New deployments run + * `devintern webhook serve` as a separate process. * * Code, credentials, and agent execution never leave this machine. */ @@ -18,7 +19,7 @@ import type { Server } from "http"; import { LockManager } from "./lib/lock-manager"; export interface WorkerOptions { - /** Mode 3: also run the GitHub webhook HTTP server. */ + /** Deprecated Mode 3 compatibility: also run the GitHub webhook HTTP server. */ listen: boolean; port?: number; host?: string; @@ -80,7 +81,7 @@ export async function startWorker( console.error( " Poll your tracker with: devintern worker --query ''", ); - console.error(" Or run the webhook listener with: devintern worker --listen"); + console.error(" Or run the webhook listener with: devintern webhook serve"); lock.release(); process.exit(1); } diff --git a/packages/code/tests/cli.test.ts b/packages/code/tests/cli.test.ts index e60221f..a5add13 100644 --- a/packages/code/tests/cli.test.ts +++ b/packages/code/tests/cli.test.ts @@ -98,6 +98,38 @@ describe.concurrent("CLI Argument Handling", () => { expect(result.stdout).not.toContain("--skip-jira-comments"); expect(result.stdout).toContain("devintern PROJ-123 PROJ-456 PROJ-789 --create-pr"); expect(result.stdout).toContain("devintern ENG-42 ENG-43 ENG-44 --create-pr"); + expect(result.stdout).toContain("webhook serve"); + expect(result.exitCode).toBe(0); + }); + + test("should show webhook command help", async () => { + const result = await runCLI(["webhook", "--help"]); + expect(result.stdout).toContain("Usage: devintern webhook "); + expect(result.stdout).toContain("serve"); + expect(result.stderr).not.toContain("deprecated"); + expect(result.exitCode).toBe(0); + }); + + test("should show canonical webhook serve help without a deprecation warning", async () => { + const result = await runCLI(["webhook", "serve", "--help"]); + expect(result.stdout).toContain("Usage: devintern webhook serve [options]"); + expect(result.stdout).toContain("--port "); + expect(result.stderr).not.toContain("deprecated"); + expect(result.exitCode).toBe(0); + }); + + test("should keep serve as a deprecated webhook serve alias", async () => { + const result = await runCLI(["serve", "--help"]); + expect(result.stdout).toContain("Usage: devintern webhook serve [options]"); + expect(result.stderr).toContain("`devintern serve` is deprecated"); + expect(result.stderr).toContain("devintern webhook serve"); + expect(result.exitCode).toBe(0); + }); + + test("should mark worker --listen as deprecated in help", async () => { + const result = await runCLI(["worker", "--help"]); + expect(result.stdout).toContain("--listen"); + expect(result.stdout).toContain("Deprecated"); expect(result.exitCode).toBe(0); }); diff --git a/packages/code/tests/worker-init.test.ts b/packages/code/tests/worker-init.test.ts index 5de820e..4ecb506 100644 --- a/packages/code/tests/worker-init.test.ts +++ b/packages/code/tests/worker-init.test.ts @@ -48,9 +48,9 @@ describe("renderSystemdUnit", () => { expect(unit).toContain("Restart=on-failure"); }); - test("appends --listen when webhook mode is chosen", () => { + test("uses the canonical webhook command when webhook mode is chosen", () => { const unit = renderSystemdUnit({ execPath: "devintern", projectDir: "/srv/app", listen: true }); - expect(unit).toContain("ExecStart=devintern worker --listen"); + expect(unit).toContain("ExecStart=devintern webhook serve"); }); }); diff --git a/packages/code/tests/worker-mode.test.ts b/packages/code/tests/worker-mode.test.ts new file mode 100644 index 0000000..052b2c4 --- /dev/null +++ b/packages/code/tests/worker-mode.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, test } from "bun:test"; + +import { resolveWorkerWorkspaceMode } from "../src/lib/worker-mode"; + +describe("resolveWorkerWorkspaceMode", () => { + test("uses an explicitly requested workspace", () => { + expect( + resolveWorkerWorkspaceMode({ + workspaceRequested: true, + noWorkspace: false, + listen: false, + workspaceExists: false, + }), + ).toEqual({ workspace: true }); + }); + + test("auto-detects an existing workspace", () => { + expect( + resolveWorkerWorkspaceMode({ + workspaceRequested: false, + noWorkspace: false, + listen: false, + workspaceExists: true, + }), + ).toEqual({ workspace: true }); + }); + + test("keeps deprecated listen mode repo-local when a workspace is auto-detected", () => { + expect( + resolveWorkerWorkspaceMode({ + workspaceRequested: false, + noWorkspace: false, + listen: true, + workspaceExists: true, + }), + ).toEqual({ workspace: false }); + }); + + test("rejects an explicit workspace with deprecated listen mode", () => { + expect( + resolveWorkerWorkspaceMode({ + workspaceRequested: true, + noWorkspace: false, + listen: true, + workspaceExists: true, + }), + ).toEqual({ workspace: false, conflict: "workspace-listen" }); + }); +});