Guidance for AI coding assistants working on this repo. For guidance on how to use the CLI as an agent, see skill/SKILL.md.
Review sessions MUST follow the required procedure in REVIEW.md. It
also carries the LGTM verdict contract the auto-approve workflow depends on.
- Name:
@coreplane/polylaneon npm — binary still invoked aspolylane - Type: Node.js CLI tool (ESM, bundled with esbuild)
- Engine: Node.js 20+ (ships as a single bundled file; dev tooling needs a recent Node)
- Language: TypeScript, strict mode
- Runtime deps are intentionally minimal — check
package.jsonbefore adding a new one.
This CLI is for agents, not humans.
- Top-level resources are nouns; each has 2–10 agent-oriented verbs. Verbs are tasks agents actually do, not thin wrappers around HTTP methods. For the current resource/verb surface, run
polylane --help. polylane --helpstays short (≤ 35 lines). Compact root help is load-bearing.- List commands return narrow projections by default (
--fullto opt out). - Single-object reads include a
Console: …/Next: …footer surfacing_html_urland_links. - Positional args for "the obvious thing" (IDs, prompts); flags for everything else.
- JSON when piped, text in TTY;
--outputalways overrides. - Every error ends with the exact command that fixes it.
- The full API surface lives under
polylane api call <operationId>as an escape hatch — wrap an op only when it earns a first-class command. - The install script is a thin shim over this CLI.
curl polylane.com/setup | sh(polylanedotcomsrc/assets/setup.sh) only bootstraps: download, PATH, sign-in prompts, thenpolylane setup. Everything that knows about coding agents — detection, config paths, ids — lives insrc/agents/registry.tsand is exposed for the installer to call (polylane setup --list-detected --ids skills-shhands skills.sh its agent list; the polylane→skills.sh id table isSKILLS_SH_IDShere), never re-implemented in shell — not even an id map. When installer work needs new agent knowledge, add the flag or command here first.
# Install
npm install
# Regenerate HTTP client from the OpenAPI spec
npm run codegen
# Type check (strict, must pass before merging)
npm run typecheck
# Lint
npm run lint
# Tests (node --test with tsx loader — does not load .env.local)
npm run test
npm run test:watch
# Dev server (loads .env.local if present)
npm run dev -- <command> [args...]
# Production-style build (codegen + esbuild → dist/polylane.mjs)
npm run buildA dev build bakes every POLYLANE_* env var it sees (from .env.local or shell) into the bundle via esbuild define. CI publishes use GitHub Actions secrets the same way. Prod checkouts without a .env.local ship source defaults.
- Strict mode.
- No
any. Useunknownonly at trust boundaries (catch clauses, JSON parsing, dynamic dispatch via the generated client). - No silent unused vars. Prefix with
_if intentionally unused (e.g._flags). - No JSDoc or comments unless the logic is non-obvious.
Order: external → internal absolute → relative. Group by imports → types → constants → functions.
- Files & directories:
kebab-case. - Test files:
test/<topic>.test.ts. - Command files live under
src/commands/<resource>/<verb>.ts.
- Functions:
camelCase. - Constants:
SCREAMING_SNAKE_CASE. - Interfaces / types:
PascalCase. - Command exports:
<resource><Verb>Command.
Throw CLIError (src/errors/base.ts) for anything user-actionable. Always pass an ExitCode and, when useful, a hint string. Let handleError at the main-loop level own the rendering.
throw new CLIError(
'No workspace set',
ExitCode.USAGE,
'polylane workspace use <id>\n' +
'or pass --workspace <id>\n' +
'or set POLYLANE_WORKSPACE_ID=<id>'
);Always await; never .then() chains. Catch at command-level if cleanup is needed (e.g. stop a spinner), otherwise let handleError catch.
Use the output helpers (src/output/). Respect --output, --quiet, --verbose. Never write progress/spinners to stdout — only stderr. stdout is pure data.
Node's built-in test runner with the tsx loader. Co-locate under test/. Clear stateful env (delete process.env.POLYLANE_*) in beforeEach when touching config / auth. Tests deliberately don't load .env.local — they verify defaults.
- Commit format:
<type>: <description>—feat,fix,refactor,docs,test,chore. - Keep commits atomic. Never bundle a feature with a drive-by rename.
- Don't commit
.env.local,dist/, or anything undersrc/generated/.
Top-level:
src/ source code
codegen/ OpenAPI → TypeScript code generator
test/ Node-native tests
skill/ Agent-facing usage reference (SKILL.md)
build.ts Build orchestrator (codegen + esbuild; loads .env.local)
Inside src/:
main.ts/args.ts/command.ts/registry.ts— entry point, arg parser, command interface, command tree + compact help.commands/<resource>/<verb>.ts— one file per command.commands/<resource>/index.tscollects them;commands/index.tsregisters every group.commands/helpers.ts— shared helpers (requireWorkspace,requirePositional,parseDuration,promptIfMissing, arg accessors).client/http.ts— auth-injectingfetch+ envelope parsing.client/thread-poll.ts— poll a thread until the agent's reply lands.auth/— credentials, OAuth (PKCE + device code), signup helpers.config/— schema, loader, paths.errors/—CLIError, exit codes, API error mapping, handler.output/—formatter.ts(with_html_url/_linksfooter),text.ts,json.ts,progress.ts, projection helpers.utils/— browser open, TTY detection, clack prompts, etc.generated/— auto-generated, do not edit. Regenerate withnpm run codegen.
When in doubt, read the tree (ls -R src/ or polylane api list) and follow the conventions of the files already there.
import type { Command } from '../../command';
import type { Config } from '../../config/schema';
import { PolylaneAPI } from '../../generated/client';
import { formatOutput } from '../../output/formatter';
import { requireWorkspace, requirePositional } from '../helpers';
export const serviceShowCommand: Command = {
name: 'service show',
description: 'Show a service with its properties',
operationId: 'cloud_infra.nodes.get',
positional: [{ name: 'service-id', description: 'Service identifier' }],
async execute(config: Config, _flags, args: Record<string, unknown>): Promise<void> {
const workspaceId = await requireWorkspace(config);
const id = requirePositional(args, 0, 'service-id');
const api = new PolylaneAPI(config);
formatOutput(config, await api.cloudInfraNodesGet(workspaceId, id));
},
};import { projectItems } from '../../output/project';
import { formatList } from '../../output/formatter';
const FIELDS = ['id', 'name', 'type', 'created', '_html_url'];
formatList(
config,
{ items: projectItems(result.items, FIELDS), count: result.count },
{ headers: [...], rows: (item) => [...] }
);The API returns _html_url (console deep link) and _links (next-step operations) on most resources. formatOutput and formatSingle strip them from the key-value dump and render a footer automatically. Don't re-render them manually. For list tables, opt in by including _html_url in your FIELDS.
const api = new PolylaneAPI(config);
await api.<methodName>(...);Discover operation names with polylane api list or by searching src/generated/client.ts. Method names come from the operationId with dots / underscores / hyphens collapsed to camelCase.
Several commands generate an install / consent URL and open it. The pattern:
import { openBrowser } from '../../utils/browser';
import { isInteractive } from '../../utils/env';
const shouldOpen = !noBrowser && isInteractive(config.nonInteractive);
process.stdout.write(url + '\n'); // always print the URL (for logs / copy-paste)
if (shouldOpen) openBrowser(url); // best-effort open- Pick the right resource group (or add one to
RESOURCE_ORDERinsrc/registry.ts). - Create
src/commands/<resource>/<verb>.tsexporting aCommand. - Add the import + array entry to
src/commands/<resource>/index.ts. - If the resource is new, wire its commands into
src/commands/index.ts. - For list commands, define a narrow
FIELDSprojection. - Positional args for 1–2 obvious things; flags for the rest.
- Add a test if the command does non-trivial work.
npm run typecheck && npm run lint && npm run test.
Usually no code change needed. After the spec updates, npm run codegen pulls in new operations — they become callable via polylane api call <operationId>.
Promote to a first-class command only if any of:
- Every agent using the CLI will use it.
- It needs custom UX (prompts, projection, smart ID resolution, browser-opening).
- It composes multiple API calls.
- Add the field to
ConfigandRawConfiginsrc/config/schema.ts. - Validate it in the loader (
src/config/loader.ts). - Handle it in
src/commands/config/set.ts. - Document it in
README.md.
- Throw a
CLIErrorwith the rightExitCodeand ahint. - Add a row to the right section of
ERRORS.mdif it's a new category.
- Don't edit
src/generated/. It's regenerated on every build. requireWorkspacefirst in any workspace-scoped command.- Every error needs a hint. Tell the user the next command to run.
- Prefer positional args for 1–2 required pieces of data.
- Narrow projection, not full objects, in list commands.
--fullopts out. - Run typecheck after every change. Strict mode surfaces most bugs.
- Don't write to stdout from progress / spinners. That corrupts piped JSON.
- Tests don't load
.env.local. If a test fails only locally but not in CI, check your env.