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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
"pnpm": {
"overrides": {
"js-yaml@<3.14.2": ">=3.14.2",
"js-yaml@>=4.0.0 <4.2.0": ">=4.2.0",
"tar@<7.5.16": ">=7.5.16",
"@isaacs/brace-expansion": ">=5.0.1",
"fast-xml-parser": ">=5.5.7",
"flatted": ">=3.4.2",
Expand All @@ -100,7 +102,7 @@
"brace-expansion@<1.1.13": "1.1.13",
"brace-expansion@>=2.0.0 <2.0.3": "2.0.3",
"brace-expansion@>=4.0.0 <5.0.6": "5.0.6",
"ws@>=8.0.0 <8.20.1": "8.20.1",
"ws@>=8.0.0 <8.21.0": "8.21.0",
"esbuild@<0.28.1": ">=0.28.1",
"micromatch>picomatch": "^2.3.2",
"tinyglobby>picomatch": "^4.0.4"
Expand Down
36 changes: 19 additions & 17 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/commands/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { apiFlags } from '../config/flags/api.flags';
import { ReportDownloadService } from '../services/report-download.service';
import { resolveAuth } from '../utils/auth';
import { CliError, logger, validateEnum } from '../utils/cli';
import { resolveApiUrl } from '../utils/config-store';

const DOWNLOAD_OPTIONS = ['ALL', 'FAILED'] as const;
const REPORT_OPTIONS = ['allure', 'html', 'html-detailed', 'junit'] as const;
Expand Down Expand Up @@ -58,7 +59,7 @@ export const artifactsCommand = defineCommand({
},
async run({ args }) {
const apiKeyFlag = args['api-key'] as string | undefined;
const apiUrl = args['api-url'] as string;
const apiUrl = resolveApiUrl(args['api-url'] as string | undefined);
const debug = Boolean(args.debug);
const uploadId = args['upload-id'] as string;
const downloadArtifacts = validateEnum(
Expand Down
3 changes: 2 additions & 1 deletion src/commands/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
CompatibilityData,
fetchCompatibilityData,
} from '../utils/compatibility';
import { resolveApiUrl } from '../utils/config-store';
import { downloadExpoUrl, extractTarGz, findAppBundle, isUrl } from '../utils/expo';
import { toPortableRelativePath } from '../utils/paths';
import {
Expand Down Expand Up @@ -142,7 +143,7 @@ export const cloudCommand = defineCommand({
};
try {
const apiKeyFlag = args['api-key'] as string | undefined;
const apiUrl = args['api-url'] as string;
const apiUrl = resolveApiUrl(args['api-url'] as string | undefined);
const appBinaryId = args['app-binary-id'] as string | undefined;
const appFile = args['app-file'] as string | undefined;
const appUrl = args['app-url'] as string | undefined;
Expand Down
3 changes: 2 additions & 1 deletion src/commands/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { apiFlags } from '../config/flags/api.flags';
import { ApiGateway } from '../gateways/api-gateway';
import { resolveAuth } from '../utils/auth';
import { CliError, logger, parseIntFlag } from '../utils/cli';
import { resolveApiUrl } from '../utils/config-store';
import { colors, formatId, formatUrl, sectionHeader, symbols } from '../utils/styling';

type UploadListItem = {
Expand Down Expand Up @@ -126,7 +127,7 @@ export const listCommand = defineCommand({
},
async run({ args }) {
const apiKeyFlag = args['api-key'] as string | undefined;
const apiUrl = args['api-url'] as string;
const apiUrl = resolveApiUrl(args['api-url'] as string | undefined);
const from = args.from as string | undefined;
const to = args.to as string | undefined;
const name = args.name as string | undefined;
Expand Down
21 changes: 15 additions & 6 deletions src/commands/live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ApiGateway } from '../gateways/api-gateway';
import type { AuthContext } from '../types/domain/auth.types';
import { resolveAuth } from '../utils/auth';
import { logger, validateEnum } from '../utils/cli';
import { resolveApiUrl } from '../utils/config-store';
import { colors, sectionHeader, symbols } from '../utils/styling';

const PLATFORM_OPTIONS = ['android', 'ios'] as const;
Expand All @@ -31,7 +32,7 @@ const startSub = defineCommand({
},
async run({ args }) {
const auth = await requireAuth(args['api-key'] as string | undefined);
const apiUrl = args['api-url'] as string;
const apiUrl = resolveApiUrl(args['api-url'] as string | undefined);
const platform = validateEnum(
args.platform as string | undefined,
PLATFORM_OPTIONS,
Expand Down Expand Up @@ -81,7 +82,7 @@ const installSub = defineCommand({
},
async run({ args }) {
const auth = await requireAuth(args['api-key'] as string | undefined);
const apiUrl = args['api-url'] as string;
const apiUrl = resolveApiUrl(args['api-url'] as string | undefined);
const sessionName = args.session as string;
const binaryId = args['app-binary-id'] as string;

Expand All @@ -104,7 +105,7 @@ const execSub = defineCommand({
},
async run({ args }) {
const auth = await requireAuth(args['api-key'] as string | undefined);
const apiUrl = args['api-url'] as string;
const apiUrl = resolveApiUrl(args['api-url'] as string | undefined);
const sessionName = args.session as string;
const yaml = args.yaml as string;

Expand Down Expand Up @@ -140,7 +141,7 @@ const stopSub = defineCommand({
},
async run({ args }) {
const auth = await requireAuth(args['api-key'] as string | undefined);
const apiUrl = args['api-url'] as string;
const apiUrl = resolveApiUrl(args['api-url'] as string | undefined);
const sessionName = args.session as string;

logger.log(`${symbols.running} Stopping session ${colors.highlight(sessionName)}...`);
Expand All @@ -159,7 +160,7 @@ const statusSub = defineCommand({
},
async run({ args }) {
const auth = await requireAuth(args['api-key'] as string | undefined);
const apiUrl = args['api-url'] as string;
const apiUrl = resolveApiUrl(args['api-url'] as string | undefined);
const sessionName = args.session as string;

const session = await ApiGateway.getLiveSession(apiUrl, auth, sessionName);
Expand Down Expand Up @@ -189,7 +190,15 @@ export const liveCommand = defineCommand({
stop: stopSub,
status: statusSub,
},
run() {
// citty's runCommand does not early-return after dispatching to a
// subcommand — it still invokes the parent `run` afterwards. So when a
// subcommand (start/install/exec/...) was matched, bail out here; otherwise
// the menu would print after every successful subcommand.
run({ rawArgs }) {
const subNames = new Set(['start', 'install', 'exec', 'stop', 'status']);
const firstPositional = rawArgs.find((arg) => !arg.startsWith('-'));
if (firstPositional && subNames.has(firstPositional)) return;

logger.log(sectionHeader('Live Session Commands'));
logger.log(` ${colors.bold('start')} Start a new live device session`);
logger.log(` ${colors.bold('install')} Install a binary on the device`);
Expand Down
3 changes: 2 additions & 1 deletion src/commands/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ApiGateway } from '../gateways/api-gateway';
import { formatDurationSeconds } from '../methods';
import { resolveAuth } from '../utils/auth';
import { CliError, logger } from '../utils/cli';
import { resolveApiUrl } from '../utils/config-store';
import {
ConnectivityCheckResult,
checkInternetConnectivity,
Expand Down Expand Up @@ -81,7 +82,7 @@ export const statusCommand = defineCommand({
// eslint-disable-next-line complexity
async run({ args }) {
const apiKeyFlag = args['api-key'] as string | undefined;
const apiUrl = args['api-url'] as string;
const apiUrl = resolveApiUrl(args['api-url'] as string | undefined);
const json = Boolean(args.json);
const name = args.name as string | undefined;
const uploadId = args['upload-id'] as string | undefined;
Expand Down
7 changes: 2 additions & 5 deletions src/commands/switch-org.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { defineCommand } from 'citty';

import { resolveAuth } from '../utils/auth';
import { CliError, logger } from '../utils/cli';
import { readConfig, writeConfig } from '../utils/config-store';
import { readConfig, resolveApiUrl, writeConfig } from '../utils/config-store';
import { fetchOrgs, pickOrg, OrgListItem } from '../utils/orgs';
import { colors, symbols } from '../utils/styling';

Expand Down Expand Up @@ -37,10 +37,7 @@ export const switchOrgCommand = defineCommand({

// Honor the env the user logged into — defaulting to prod here would send
// a dev Bearer token to the prod API.
const apiUrl =
(args['api-url'] as string | undefined) ??
config.api_url ??
'https://api.devicecloud.dev';
const apiUrl = resolveApiUrl(args['api-url'] as string | undefined);
const target = args.org as string | undefined;

// sessionOnly: an exported DEVICE_CLOUD_API_KEY must not shadow the
Expand Down
3 changes: 2 additions & 1 deletion src/commands/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { outputFlags } from '../config/flags/output.flags';
import { uploadBinary, verifyAppZip } from '../methods';
import { resolveAuth } from '../utils/auth';
import { CliError, logger } from '../utils/cli';
import { resolveApiUrl } from '../utils/config-store';
import { downloadExpoUrl, extractTarGz, findAppBundle, isUrl } from '../utils/expo';
import { colors, formatId, sectionHeader, symbols } from '../utils/styling';

Expand Down Expand Up @@ -37,7 +38,7 @@ export const uploadCommand = defineCommand({
};
try {
const apiKeyFlag = args['api-key'] as string | undefined;
const apiUrl = args['api-url'] as string;
const apiUrl = resolveApiUrl(args['api-url'] as string | undefined);
const appUrl = args['app-url'] as string | undefined;
const ignoreShaCheck = Boolean(args['ignore-sha-check']);
const debug = Boolean(args.debug);
Expand Down
6 changes: 4 additions & 2 deletions src/config/flags/api.flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export const apiFlags = {
'api-url': {
type: 'string',
alias: ['apiURL', 'apiUrl'],
default: 'https://api.devicecloud.dev',
description: 'API base URL',
// No citty `default` here: commands resolve the effective URL via
// resolveApiUrl() so a value stored by `dcd login` (e.g. dev/staging) is
// honored instead of always defaulting to prod. See utils/config-store.ts.
description: 'API base URL (defaults to the URL stored by `dcd login`, else prod)',
},
} as const satisfies ArgsDef;
5 changes: 3 additions & 2 deletions src/config/flags/environment.flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ export const environmentFlags = {
env: {
type: 'string',
alias: ['e'],
description: 'One or more environment variable files to inject into your flows (may be repeated)',
valueHint: 'path',
description:
'One or more environment variables to inject into your flows (format: KEY=VALUE, may be repeated)',
valueHint: 'KEY=VALUE',
},
metadata: {
type: 'string',
Expand Down
4 changes: 3 additions & 1 deletion src/gateways/api-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,9 @@ export const ApiGateway = {
}

default: {
throw new ApiError(`${operation} failed: ${userMessage} (HTTP ${res.status})`, res.status);
// `operation` is already phrased as "Failed to …", so don't append
// another "failed" here (avoids "Failed to execute test failed: …").
throw new ApiError(`${operation}: ${userMessage} (HTTP ${res.status})`, res.status);
}
}
},
Expand Down
18 changes: 18 additions & 0 deletions src/utils/config-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
import { homedir } from 'node:os';
import * as path from 'node:path';

import { ENVIRONMENTS } from '../config/environments';

export const CONFIG_SCHEMA_VERSION = 1;

export interface StoredSession {
Expand Down Expand Up @@ -78,6 +80,22 @@ export function readConfig(): StoredConfig | null {
}
}

/**
* Resolve the API URL a command should talk to. Precedence:
* 1. explicit --api-url flag
* 2. api_url stored by `dcd login` (honors the env the user logged into)
* 3. prod default
*
* Without this, session commands default to prod and a dev/staging Bearer
* token is rejected with a misleading "Invalid or expired JWT". `switch-org`
* has always done this; this helper extends it to every command.
*/
export function resolveApiUrl(flag: string | undefined): string {
const explicit = flag?.trim();
if (explicit) return explicit;
return readConfig()?.api_url ?? ENVIRONMENTS.prod.apiUrl;
}

export function writeConfig(config: StoredConfig): void {
const dir = getConfigDir();
if (!existsSync(dir)) {
Expand Down
Loading
Loading