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
11 changes: 6 additions & 5 deletions ERRORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,24 @@ Every command inherits these. Specific messages are subject to change, but the *

| Scenario | Typical message |
|---|---|
| No credentials | `Not logged in.` with a hint listing `auth login` variants |
| HTTP 401 | `Unauthorized — check your API key` |
| HTTP 403 | `Forbidden — insufficient permissions` |
| No credentials | `Not signed in.` with a hint listing `auth login` variants |
| HTTP 401 | `Not authenticated` |
| HTTP 403 | `Permission denied` |
| OAuth refresh failed | `Token refresh failed` with a hint to re-authenticate |
| WebSocket upgrade rejected (for streaming commands) | `WebSocket upgrade rejected (<status>)` |

### Rate limit / plan (exit `4`)

| Scenario | Typical message |
|---|---|
| HTTP 429 | `Rate limit exceeded` + any `Retry-After` |
| HTTP 429 | `Rate limited` + any `Retry-After` |
| HTTP 426 | `Plan upgrade required` |

### Usage (exit `2`)

| Scenario | Typical message |
|---|---|
| Unknown command | `Unknown command: polylane <path>` with a hint pointing at `polylane --help` |
| Unknown flag | `Unknown flag: <flag>` |
| Flag requires a value | `Flag <flag> requires a value` |
| Flag expects a number | `Flag <flag> expects a number, got "<value>"` |
Expand All @@ -86,7 +87,7 @@ Every command inherits these. Specific messages are subject to change, but the *
| Invalid domain / workspace ID / timeout / output format | per-validator error with a hint |
| `--body` invalid JSON | `Invalid JSON in --body: <message>` |
| `--body-file` unreadable | bubbled fs error (`ENOENT`, `EACCES`, …) |
| Destructive command without `--yes` in non-interactive mode | `Refusing to <verb> without --yes in non-interactive mode` |
| Destructive command without `--yes` in non-interactive mode | `Confirmation required` with a hint to re-run with `--yes` |

### Workspace context (exit `2`)

Expand Down
10 changes: 5 additions & 5 deletions src/auth/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,8 +344,8 @@ export async function oauthBrowserFlow(config: Config): Promise<OAuthTokenRespon
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('response_type', 'code');

process.stderr.write(`Opening browser to authenticate...\n`);
process.stderr.write(`If the browser does not open, visit:\n ${authUrl.toString()}\n\n`);
process.stderr.write(`Opening your browser to sign in…\n`);
process.stderr.write(`If it doesn't open, visit:\n ${authUrl.toString()}\n\n`);

const serverPromise = startCallbackServer(state);
openBrowser(authUrl.toString());
Expand Down Expand Up @@ -402,10 +402,10 @@ export async function oauthDeviceCodeFlow(config: Config): Promise<OAuthTokenRes
interval: number;
};

process.stderr.write(`\nTo authenticate, visit:\n`);
process.stderr.write(`\nTo sign in, visit:\n`);
process.stderr.write(` ${deviceData.verification_uri}\n\n`);
process.stderr.write(`And enter the code: ${deviceData.user_code}\n\n`);
process.stderr.write(`Waiting for authorization...\n`);
process.stderr.write(`Waiting for authorization\n`);

let interval = deviceData.interval * 1000;
const deadline = Date.now() + deviceData.expires_in * 1000;
Expand Down Expand Up @@ -435,7 +435,7 @@ export async function oauthDeviceCodeFlow(config: Config): Promise<OAuthTokenRes
continue;
}
if (errBody.error === 'expired_token') {
throw new CLIError('Device code expired', ExitCode.AUTH, 'Try again');
throw new CLIError('Device code expired', ExitCode.AUTH, 'Run `polylane auth login` again');
}
throw new CLIError(
`Device code flow failed: ${errBody.error ?? 'unknown error'}`,
Expand Down
2 changes: 1 addition & 1 deletion src/client/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export async function request(config: Config, opts: RequestOpts): Promise<Respon
} catch (err) {
recordHttpRequest(Date.now() - httpStart);
if (err instanceof Error && err.name === 'TimeoutError') {
throw new CLIError('Request timed out', ExitCode.TIMEOUT, 'Try increasing --timeout');
throw new CLIError('Request timed out', ExitCode.TIMEOUT, 'Raise --timeout');
}
if (err instanceof Error && err.name === 'AbortError') {
throw new CLIError('Request aborted', ExitCode.TIMEOUT);
Expand Down
20 changes: 10 additions & 10 deletions src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ interface WorkspaceItem {
}

async function validateApiKey(config: Config, key: string): Promise<WhoamiResult> {
const spinner = new Spinner('Validating API key');
const spinner = new Spinner('Validating API key');
spinner.start();
try {
const res = await request(
Expand All @@ -41,7 +41,7 @@ async function validateApiKey(config: Config, key: string): Promise<WhoamiResult
);
if (!res.ok) {
spinner.stop();
throw new CLIError(`API key validation failed: ${res.status}`, ExitCode.AUTH);
throw new CLIError(`API key validation failed (${res.status})`, ExitCode.AUTH);
}
const json = (await res.json()) as {
success: boolean;
Expand All @@ -64,7 +64,7 @@ async function validateApiKey(config: Config, key: string): Promise<WhoamiResult
}

async function selectWorkspace(config: Config, user: WhoamiResult): Promise<string | undefined> {
const spinner = new Spinner('Fetching workspaces');
const spinner = new Spinner('Fetching workspaces');
spinner.start();
try {
const list = await requestJson<{ items: WorkspaceItem[]; count: number }>(
Expand All @@ -73,20 +73,20 @@ async function selectWorkspace(config: Config, user: WhoamiResult): Promise<stri
);
spinner.stop();
if (list.items.length === 0) {
process.stderr.write(`No workspaces found for ${user.email ?? user.id}\n`);
process.stderr.write(`No workspaces for ${user.email ?? user.id}. Create one with \`polylane workspace create\`.\n`);
return undefined;
}
if (list.items.length === 1) {
const ws = list.items[0]!;
process.stderr.write(`Using workspace: ${ws.name} (${ws.id})\n`);
process.stderr.write(`Using workspace ${ws.name} (${ws.id})\n`);
return ws.id;
}
if (!isInteractive(config.nonInteractive)) {
return undefined;
}
const selected = await promptSelect<string>(
{ nonInteractive: config.nonInteractive },
'Select default workspace',
'Default workspace',
list.items.map((ws) => ({
value: ws.id,
label: ws.name,
Expand All @@ -104,7 +104,7 @@ async function apiKeyLogin(config: Config, key: string): Promise<void> {
const user = await validateApiKey(config, key);

const name = user.forename ? `${user.forename}${user.surname ? ' ' + user.surname : ''}` : user.email ?? user.id;
process.stderr.write(`\nAuthenticated as ${name} (${user.email ?? user.id})\n`);
process.stderr.write(`\nSigned in as ${name} (${user.email ?? user.id})\n`);

const configWithKey: Config = { ...config, apiKey: key };
const wsId = await selectWorkspace(configWithKey, user);
Expand Down Expand Up @@ -148,7 +148,7 @@ async function oauthLogin(config: Config, useBrowser: boolean): Promise<void> {
url: '/v1/auth/whoami',
});
const name = user.forename ? `${user.forename}${user.surname ? ' ' + user.surname : ''}` : user.email ?? user.id;
process.stderr.write(`\nAuthenticated as ${name} (${user.email ?? user.id})\n`);
process.stderr.write(`\nSigned in as ${name} (${user.email ?? user.id})\n`);
const wsId = await selectWorkspace(configWithAuth, user);
if (wsId) {
writeConfigFile({ workspace_id: wsId });
Expand Down Expand Up @@ -194,7 +194,7 @@ export const authLoginCommand: Command = {

const method = await promptSelect<'browser' | 'device' | 'api-key'>(
{ nonInteractive: config.nonInteractive },
'How would you like to authenticate?',
'Sign in with',
[
{ value: 'browser', label: 'OAuth (browser)', hint: 'Recommended' },
{ value: 'device', label: 'OAuth (device code)', hint: 'For SSH/headless' },
Expand All @@ -203,7 +203,7 @@ export const authLoginCommand: Command = {
);

if (method === 'api-key') {
const key = await promptPassword({ nonInteractive: config.nonInteractive }, 'Enter API key');
const key = await promptPassword({ nonInteractive: config.nonInteractive }, 'API key');
await apiKeyLogin(config, key);
return;
}
Expand Down
6 changes: 3 additions & 3 deletions src/commands/auth/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,18 @@ export const authLogoutCommand: Command = {
if (cred) {
await revokeToken(config, cred.accessToken);
deleteCredentials();
process.stderr.write('OAuth credentials revoked and removed\n');
process.stderr.write('Revoked OAuth token and removed credentials\n');
}

const existing = loadConfigFile();
if (existing?.api_key) {
const next = { ...existing };
delete next.api_key;
replaceConfigFile(next);
process.stderr.write('API key cleared from config file\n');
process.stderr.write('Removed API key from config\n');
}

process.stderr.write('Logged out\n');
process.stderr.write('Signed out\n');

void flags;
},
Expand Down
6 changes: 3 additions & 3 deletions src/commands/auth/signup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface SignupEnvelope {

export const authSignupCommand: Command = {
name: 'auth signup',
description: 'Bootstrap a new Polylane account with email + password',
description: 'Create a Polylane account with email and password',
operationId: 'auth.signup',
options: [
{ flag: '--email <email>', description: 'Email address', type: 'string' },
Expand Down Expand Up @@ -78,7 +78,7 @@ export const authSignupCommand: Command = {
if (result.token) {
note(
[
`You are signed in. The session is valid until ${expiresAt}.`,
`Signed in. Session valid until ${expiresAt}.`,
``,
`Onboarding (in order):`,
``,
Expand Down Expand Up @@ -111,7 +111,7 @@ export const authSignupCommand: Command = {
);
outro('Signed in.');
} else {
outro('Signup accepted but no session returned. Run `polylane auth login` to authenticate.');
outro('Account created, but no session returned. Run `polylane auth login`.');
}
},
};
3 changes: 2 additions & 1 deletion src/commands/cloud/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ export const cloudConnectCommand: Command = {
// All other providers return { accounts, failures } synchronously.
formatOutput(config, result);
if ('failures' in result && result.failures.length > 0 && !config.quiet) {
process.stderr.write(`\n${result.failures.length} account(s) failed to connect — see "failures" above.\n`);
const n = result.failures.length;
process.stderr.write(`\n${n} account${n === 1 ? '' : 's'} failed to connect — see "failures" above.\n`);
}
},
};
2 changes: 1 addition & 1 deletion src/commands/cloud/disconnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const cloudDisconnectCommand: Command = {
const yes = getArgBoolean(args, 'yes') === true;
if (!yes) {
if (!isInteractive(config.nonInteractive)) {
throw new CLIError('Refusing to disconnect without --yes in non-interactive mode', ExitCode.USAGE);
throw new CLIError('Confirmation required', ExitCode.USAGE, `Re-run with --yes to disconnect ${id}`);
}
const confirmed = await promptConfirm(
{ nonInteractive: config.nonInteractive },
Expand Down
2 changes: 1 addition & 1 deletion src/commands/config/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const configSetCommand: Command = {
throw new CLIError(
`Invalid telemetry value: "${value}"`,
ExitCode.USAGE,
`Use one of: ${[...truthy, ...falsy].join(', ')}`
'Use true/false, yes/no, on/off, 1/0, or enabled/disabled'
);
}
break;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/feed/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export const feedListCommand: Command = {

const limit = getArgNumber(args, 'limit') ?? 20;
if (limit < 1 || limit > 200) {
throw new CLIError('Invalid --limit', ExitCode.USAGE, 'Must be between 1 and 200');
throw new CLIError(`Invalid --limit: ${limit}`, ExitCode.USAGE, 'Use 1–200');
}

const since = getArgString(args, 'since');
Expand Down
10 changes: 7 additions & 3 deletions src/commands/help.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Command } from '../command';
import type { Config } from '../config/schema';
import { registry, renderHelp } from '../registry';
import { CLIError } from '../errors/base';
import { ExitCode } from '../errors/codes';

export const helpCommand: Command = {
name: 'help',
Expand All @@ -13,9 +15,11 @@ export const helpCommand: Command = {
}
const resolved = registry.resolve(positional);
if (!resolved) {
process.stderr.write(`Unknown command: ${positional.join(' ')}\n`);
process.stdout.write(renderHelp(null, config.noColor));
return;
throw new CLIError(
`Unknown command: polylane ${positional.join(' ')}`,
ExitCode.USAGE,
'Run `polylane --help` to list commands'
);
}
process.stdout.write(renderHelp(resolved.command, config.noColor));
},
Expand Down
2 changes: 1 addition & 1 deletion src/commands/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function parseDuration(value: string): number {
throw new CLIError(
`Invalid duration: "${value}"`,
ExitCode.USAGE,
'Use forms like 1h, 24h, 30m, 7d'
'Use a duration like 30m, 1h, 24h, or 7d'
);
}
const n = Number(match[1]);
Expand Down
2 changes: 1 addition & 1 deletion src/commands/integration/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export const integrationConnectCommand: Command = {
} else if (isInteractive(config.nonInteractive)) {
authMethod = await promptSelect<'none' | 'bearer' | 'oauth'>(
{ nonInteractive: config.nonInteractive },
'How should this MCP server be authenticated?',
'Authentication',
[
{ value: 'none', label: 'No authentication', hint: 'Public server' },
{ value: 'bearer', label: 'Bearer token', hint: 'API key / token' },
Expand Down
2 changes: 1 addition & 1 deletion src/commands/integration/disconnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const integrationDisconnectCommand: Command = {
const yes = getArgBoolean(args, 'yes') === true;
if (!yes) {
if (!isInteractive(config.nonInteractive)) {
throw new CLIError('Refusing to disconnect without --yes in non-interactive mode', ExitCode.USAGE);
throw new CLIError('Confirmation required', ExitCode.USAGE, `Re-run with --yes to disconnect ${id}`);
}
const confirmed = await promptConfirm(
{ nonInteractive: config.nonInteractive },
Expand Down
9 changes: 7 additions & 2 deletions src/commands/integration/show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { Config } from '../../config/schema';
import { PolylaneAPI } from '../../generated/client';
import { formatOutput } from '../../output/formatter';
import { requireWorkspace, requirePositional } from '../helpers';
import { CLIError } from '../../errors/base';
import { ExitCode } from '../../errors/codes';

export const integrationShowCommand: Command = {
name: 'integration show',
Expand All @@ -17,8 +19,11 @@ export const integrationShowCommand: Command = {
const result = await api.integrationsList(workspaceId, { id, perPage: 1 });
const integration = result.items[0];
if (!integration) {
process.stderr.write(`No integration with id ${id}\n`);
process.exit(1);
throw new CLIError(
`No integration with ID ${id}`,
ExitCode.GENERAL,
'List integrations with `polylane integration list`'
);
}
formatOutput(config, integration);
},
Expand Down
2 changes: 1 addition & 1 deletion src/commands/note/global-clear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ export const noteGlobalClearCommand: Command = {

const api = new PolylaneAPI(config);
await api.notesGlobalDel(workspaceId);
process.stdout.write(`Cleared\n`);
process.stdout.write(`Cleared global note\n`);
},
};
6 changes: 3 additions & 3 deletions src/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ export const setupCommand: Command = {

if (selected.length === 0) {
say('No coding agents detected.');
say(`Target one explicitly with --agent <id> (${AGENTS.map((a) => a.id).join(', ')}).`);
say(`Configure one anyway with --agent <id> (${AGENTS.map((a) => a.id).join(', ')}).`);
return;
}

Expand Down Expand Up @@ -350,9 +350,9 @@ export const setupCommand: Command = {

const credential = await tryResolveCredential(config);
if (credential) {
say('Authentication: signed in.');
say('Signed in.');
} else {
say('Authentication: not signed in. Run `polylane auth login` to authenticate.');
say('Not signed in. Run `polylane auth login`.');
}
},
};
2 changes: 1 addition & 1 deletion src/commands/telemetry/enable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const telemetryEnableCommand: Command = {
process.stderr.write('Telemetry enabled.\n');
if (process.env.DO_NOT_TRACK && process.env.DO_NOT_TRACK !== '0' && process.env.DO_NOT_TRACK !== 'false') {
process.stderr.write(
' Note: DO_NOT_TRACK is set in your environment, which overrides the config. Unset it to actually send telemetry.\n'
' Note: DO_NOT_TRACK is set and overrides this setting. Unset it to send telemetry.\n'
);
}
},
Expand Down
2 changes: 1 addition & 1 deletion src/commands/thread/ask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const threadAskCommand: Command = {
}

const useSpinner = !config.quiet && config.output !== 'json';
const spinner = useSpinner ? new Spinner(`Waiting for assistant on ${thread.id}`) : null;
const spinner = useSpinner ? new Spinner(`Waiting for reply on ${thread.id}`) : null;
if (spinner) spinner.start();

try {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/thread/continue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const threadContinueCommand: Command = {
}

const useSpinner = !config.quiet && config.output !== 'json';
const spinner = useSpinner ? new Spinner(`Waiting for assistant on ${threadId}`) : null;
const spinner = useSpinner ? new Spinner(`Waiting for reply on ${threadId}`) : null;
if (spinner) spinner.start();

try {
Expand Down
Loading
Loading