Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.9.9
0.9.10
97 changes: 95 additions & 2 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# capy CLI reference

Version `0.9.9`. Generated from `capy help --json`.
Version `0.9.10`. Generated from `capy help --json`.

## Commands

Expand Down Expand Up @@ -119,11 +119,24 @@ capy deploy [target] [options]
| `--env-name <name>` | gh-actions: env name when --scope env | |
| `--no-deploy` | write and verify the target, but skip the platform deploy/redeploy (target mode) | |
| `--json` | describe the route (unanswered stops + any known branch problem) as JSON instead of travelling it | |
| `--discover` | dokploy: find the services that match Capy projects and print JSON (never prompts) | |
| `--plan <file>` | dokploy --discover: the plan file to check (see schemas.deploy_dokploy_plan in `capy help --json`) | |
| `--confirm <plan_id>` | dokploy --discover: write the plan that --dry-run printed (one PR per repo) | |
| `--base-url <url>` | dokploy --discover: the Dokploy dashboard URL (else the org system variable _CONNECTOR_DOKPLOY_BASE_URL) | |

JSON support: yes (`--json`)

Dry run: yes (`--dry-run`)

Mode: `capy deploy dokploy --discover`

Find the Dokploy services that match Capy projects, check a plan for them, and write the deploy targets as one pull request per repo. Pushes no values.

- Flags: `--discover`, `--plan`, `--confirm`, `--base-url`, `--dry-run`, `--json`
- Always prints JSON: yes
- Dry run: yes
- Plan schema: `schemas.deploy_dokploy_plan` in `capy help --json`

### `capy deploy revoke`

Revoke a deploy token
Expand Down Expand Up @@ -669,7 +682,7 @@ capy connect [provider] [options]
| `--json` | emit machine-readable JSON instead of the human UI (import connectors) | |
| `--dry-run` | dokploy import/discover: preview the plan only — resolve settings + read Dokploy, write/push nothing | |
| `--discover` | dokploy: find every Dokploy service matching a repo under cwd, instead of one named --application/--compose | |
| `-y, --yes` | dokploy import/discover: skip the confirmation prompt (import: --overwrite's clear/replace/import ask; discover: the real-run "proceed?" ask) — required non-interactively | |
| `-y, --yes` | dokploy import/discover: skip the confirmation prompt (import: --overwrite's clear/replace/import ask) — discover never prompts and needs --yes to write | |
| `--overwrite` | dokploy import: set the branch's vars to EXACTLY Dokploy's set — clear names not in Dokploy, replace differing values, import new ones | |
| `--environment <names>` | dokploy discover: restrict the plan to these Dokploy environment names, comma-separated (e.g. staging,preview) | |

Expand Down Expand Up @@ -708,6 +721,76 @@ _No options._

JSON support: no

## Schemas

JSON Schemas of the files commands read, as published under `schemas` in `capy help --json`.

### `deploy_dokploy_plan`

```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "capy deploy dokploy --discover plan",
"type": "object",
"additionalProperties": false,
"required": [
"version",
"entries"
],
"properties": {
"version": {
"const": 1
},
"entries": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"project_id",
"branch",
"service_id",
"git_branch",
"vars"
],
"properties": {
"project_id": {
"type": "string",
"minLength": 1,
"description": "Capy project id"
},
"branch": {
"type": "string",
"minLength": 1,
"description": "Capy branch the target ships from"
},
"service_id": {
"type": "string",
"minLength": 1,
"description": "Dokploy application or compose service id"
},
"git_branch": {
"type": "string",
"minLength": 1,
"description": "git branch the Dokploy service tracks (the CI deploy PR base)"
},
"vars": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
},
"description": "Variable names the target delivers; each must exist on the Capy branch"
}
}
}
}
}
}
```

## Error codes

Every refusal carries a stable `code` — branch on it, never on message text.
Expand Down Expand Up @@ -808,6 +891,16 @@ Every refusal carries a stable `code` — branch on it, never on message text.
- `GITHUB_TIMEOUT`
- `RATE_LIMITED`
- `GITHUB_RATE_LIMITED`
- `SERVICE_NOT_FOUND`
- `DUPLICATE_ENTRY`
- `TARGET_EXISTS`
- `NO_REPO_LINK`
- `REPO_MISMATCH`
- `PLAN_FILE_UNREADABLE`
- `PLAN_INVALID`
- `PLAN_REQUIRED`
- `DISCOVER_UNSUPPORTED_TARGET`
- `DISCOVER_PARTIAL`

## Conventions

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@capysc/cli",
"version": "0.9.9",
"version": "0.9.10",
"description": "Capy CLI - SecretOps for the AI age",
"main": "dist/index.js",
"bin": {
Expand Down
47 changes: 36 additions & 11 deletions src/commands/connectCommand.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { resolveContext, writeAndSync, writeImportOutcome, ResolvedContext } from './connectors/shared';
import { listProviders, loadProvider, ConnectOpts, ConnectorModule } from './connectors/registry';
import { isInteractive } from '../ui/interactive';
import { EXIT_NEEDS_INPUT, isInteractive } from '../ui/interactive';
import { resolveOrgContext } from '../core/orgContext';
import type { DiscoveryContext } from './connectors/dokployDiscovery';

const B = (s: string) => `\x1b[1m${s}\x1b[0m`;

/** A refusal that only a flag (or a person) can answer exits `EXIT_NEEDS_INPUT`, so an agent branches on it; every other refusal exits 1. */
function exitCodeForRefusalCode(code: string): number {
return code === 'DOKPLOY_SETTINGS_MISSING' ? EXIT_NEEDS_INPUT : 1;
}

/**
* Discovery's own context — org + auth + serviceClient, exactly what
* `resolveOrgContext` already gives org-level commands (`invite`, `kick`,
Expand Down Expand Up @@ -84,8 +89,13 @@ export class ConnectCommand {
// context instead: org + auth + serviceClient only, no project key, no
// `.env` decrypt — see `resolveDiscoveryContext`'s own doc.
if (effective.discover && mod.discover) {
// Discover mode (Vince, 2026-10-03): ALWAYS prints exactly what
// `--json` prints, in a terminal or not, and never prompts — every
// choice comes from flags (`--base-url`, `--yes`, …) or is refused
// with a code.
const discoveryOpts: ConnectOpts = { ...effective, json: true, nonTty: true };
const discoveryCtx = await resolveDiscoveryContext(this.devMode);
return await this.executeDiscovery(mod, provider, discoveryCtx, effective);
return await this.executeDiscovery(mod, provider, discoveryCtx, discoveryOpts);
}

const ctx = await resolveContext({ devMode: this.devMode, dryRun: effective.dryRun === true });
Expand Down Expand Up @@ -178,11 +188,21 @@ export class ConnectCommand {

if (!outcome.ok) {
if (opts.json) {
console.log(JSON.stringify({ ok: false, code: outcome.code, message: outcome.message }));
console.log(
JSON.stringify({
ok: false,
code: outcome.code,
message: outcome.message,
...(outcome.unanswered === undefined ? {} : { unanswered: outcome.unanswered }),
}),
);
} else {
console.error(`\n ${outcome.message}\n`);
console.error(`\n ${outcome.message}`);
// The refusal says what is still needed, as flags: print each hint beside it.
(outcome.unanswered ?? []).forEach((u) => console.error(` ${u.flag}: ${u.hint}`));
console.error('');
}
process.exitCode = 1;
process.exitCode = exitCodeForRefusalCode(outcome.code);
return { linked: false };
}

Expand Down Expand Up @@ -217,7 +237,6 @@ export class ConnectCommand {
skipped: outcome.skipped,
warnings: outcome.warnings,
pushed,
deployTargetSaved: outcome.deployTargetSaved,
}),
);
return { linked: wrote };
Expand Down Expand Up @@ -262,9 +281,6 @@ export class ConnectCommand {
for (const w of outcome.warnings) {
console.log(` ⚠ ${w.code}: ${w.names.join(', ')}`);
}
if (outcome.deployTargetSaved) {
console.log(` Saved a Dokploy deploy target for application ${outcome.applicationId}.`);
}
console.log('');
return { linked: wrote };
}
Expand All @@ -284,11 +300,18 @@ export class ConnectCommand {

if (!outcome.ok) {
if (opts.json) {
console.log(JSON.stringify({ ok: false, code: outcome.code, message: outcome.message }));
console.log(
JSON.stringify({
ok: false,
code: outcome.code,
message: outcome.message,
...(outcome.unanswered === undefined ? {} : { unanswered: outcome.unanswered }),
}),
);
} else {
console.error(`\n ${outcome.message}\n`);
}
process.exitCode = 1;
process.exitCode = exitCodeForRefusalCode(outcome.code);
return { linked: false };
}

Expand Down Expand Up @@ -392,6 +415,8 @@ export class ConnectCommand {
provider,
dryRun: outcome.dryRun,
cancelled: !!outcome.cancelled,
...(outcome.saved === undefined ? {} : { saved: outcome.saved }),
...(outcome.notices === undefined ? {} : { notices: outcome.notices }),
plan: planForOutput,
...(appliedForOutput ? { applied: appliedForOutput } : {}),
...(commitsForOutput ? { commits: commitsForOutput } : {}),
Expand Down
Loading
Loading