Skip to content
Closed
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
7 changes: 7 additions & 0 deletions packages/boxel-cli/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ export {
type CancelIndexingResult,
} from './src/lib/boxel-cli-client';

export {
getToolDefinitions,
requireStringArg,
type BoxelToolDefinition,
type BoxelToolConfig,
} from './src/lib/tool-definitions';

export {
resetProfileManager,
setProfileManager,
Expand Down
50 changes: 38 additions & 12 deletions packages/boxel-cli/src/commands/realm/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ interface PushOptions extends SyncOptions {

class RealmPusher extends RealmSyncBase {
hasError = false;
uploadedFiles: string[] = [];

constructor(
private pushOptions: PushOptions,
Expand Down Expand Up @@ -220,6 +221,7 @@ class RealmPusher extends RealmSyncBase {
for (const { rel, hash } of uploaded) {
newManifest.files[rel] = hash;
}
this.uploadedFiles.push(...result.succeeded);
}
}

Expand Down Expand Up @@ -362,11 +364,16 @@ export function registerPushCommand(realm: Command): void {
);
}

export async function pushCommand(
export interface PushResult {
files: string[];
error?: string;
}

export async function push(
localDir: string,
realmUrl: string,
options: PushCommandOptions,
): Promise<void> {
): Promise<PushResult> {
let authenticator: RealmAuthenticator;
if (options.authenticator) {
authenticator = options.authenticator;
Expand All @@ -377,15 +384,16 @@ export async function pushCommand(
profileManager: options.profileManager,
});
if (!resolution.ok) {
console.error(`Error: ${resolution.error}`);
process.exit(1);
return { files: [], error: resolution.error };
}
authenticator = resolution.authenticator;
}

if (!(await pathExists(localDir))) {
console.error(`Local directory does not exist: ${localDir}`);
process.exit(1);
return {
files: [],
error: `Local directory does not exist: ${localDir}`,
};
}

try {
Expand All @@ -403,13 +411,31 @@ export async function pushCommand(
await pusher.sync();

if (pusher.hasError) {
console.log('Push did not complete successfully. View logs for details');
process.exit(2);
} else {
console.log('Push completed successfully');
return {
files: pusher.uploadedFiles.sort(),
error:
'Push completed with errors. Some files may not have been uploaded.',
};
}

return { files: pusher.uploadedFiles.sort() };
} catch (error) {
console.error('Push failed:', error);
process.exit(1);
return {
files: [],
error: `Push failed: ${error instanceof Error ? error.message : String(error)}`,
};
}
}

export async function pushCommand(
localDir: string,
realmUrl: string,
options: PushCommandOptions,
): Promise<void> {
const result = await push(localDir, realmUrl, options);
if (result.error) {
console.error(`Error: ${result.error}`);
process.exit(result.files.length > 0 ? 2 : 1);
}
console.log('Push completed successfully');
}
38 changes: 20 additions & 18 deletions packages/boxel-cli/src/commands/run-command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { Command } from 'commander';
import { getProfileManager, type ProfileManager } from '../lib/profile-manager';
import {
getProfileManager,
NO_ACTIVE_PROFILE_ERROR,
type ProfileManager,
} from '../lib/profile-manager';
import { ensureTrailingSlash } from '@cardstack/runtime-common/paths';
import { FG_GREEN, FG_RED, FG_CYAN, DIM, RESET } from '../lib/colors';
import { cliLog } from '../lib/cli-log';

Expand All @@ -13,6 +18,8 @@ export interface RunCommandOptions {
input?: Record<string, unknown>;
json?: boolean;
profileManager?: ProfileManager;
/** Override the realm server URL. Defaults to the active profile's. */
realmServerUrl?: string;
}

interface RunCommandCliOptions {
Expand All @@ -27,15 +34,18 @@ export async function runCommand(
options?: RunCommandOptions,
): Promise<RunCommandResult> {
let pm = options?.profileManager ?? getProfileManager();
let active = pm.getActiveProfile();
if (!active) {
throw new Error(
'No active profile. Run `boxel profile add` to create one.',
);
let realmServerUrl = options?.realmServerUrl;
if (!realmServerUrl) {
let active = pm.getActiveProfile();
if (!active) {
return {
status: 'error',
error: NO_ACTIVE_PROFILE_ERROR,
};
}
realmServerUrl = active.profile.realmServerUrl;
}

let realmServerUrl = active.profile.realmServerUrl.replace(/\/$/, '');
let url = `${realmServerUrl}/_run-command`;
let url = `${ensureTrailingSlash(realmServerUrl)}_run-command`;

let body = {
data: {
Expand Down Expand Up @@ -140,15 +150,7 @@ export function registerRunCommand(program: Command): void {
}
}

let result: RunCommandResult;
try {
result = await runCommand(commandSpecifier, opts.realm, { input });
} catch (err) {
console.error(
`${FG_RED}Error:${RESET} ${err instanceof Error ? err.message : String(err)}`,
);
process.exit(1);
}
let result = await runCommand(commandSpecifier, opts.realm, { input });

if (opts.json) {
cliLog.output(JSON.stringify(result, null, 2));
Expand Down
97 changes: 39 additions & 58 deletions packages/boxel-cli/src/lib/boxel-cli-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,17 @@ import {
type ReadTranspiledResult,
} from '../commands/read-transpiled';
import { write as coreWrite, type WriteResult } from '../commands/file/write';
import {
runCommand as coreRunCommand,
type RunCommandResult,
} from '../commands/run-command';
import {
cancelIndexing as coreCancelIndexing,
type CancelIndexingResult,
} from '../commands/realm/cancel-indexing';
import { createRealm as coreCreateRealm } from '../commands/realm/create';
import { pull as realmPull } from '../commands/realm/pull';
import { push as realmPush, type PushResult } from '../commands/realm/push';
import { sync as realmSync, type SyncResult } from '../commands/realm/sync';
import { waitForReady as coreWaitForReady } from '../commands/realm/wait-for-ready';
import { getProfileManager, type ProfileManager } from './profile-manager';
Expand All @@ -35,6 +40,7 @@ export type {
ListFilesResult,
ReadTranspiledResult,
SyncResult,
PushResult,
SearchResult,
SearchCommandOptions,
};
Expand Down Expand Up @@ -73,6 +79,15 @@ export interface PullResult {
error?: string;
}

export interface PushOptions {
/** Delete remote files that don't exist locally (default: false). */
delete?: boolean;
/** Preview without making changes. */
dryRun?: boolean;
/** Upload all files, even if unchanged (default: false). */
force?: boolean;
}

export interface SyncOptions {
/** Resolve conflicts by keeping the local version. */
preferLocal?: boolean;
Expand All @@ -88,13 +103,7 @@ export interface SyncOptions {

export type { DeleteResult };
export type { WriteResult };

export interface RunCommandResult {
status: 'ready' | 'error' | 'unusable';
/** Serialized command result (JSON string), or null. */
result?: string | null;
error?: string | null;
}
export type { RunCommandResult };

export type { LintMessage, LintResult };

Expand Down Expand Up @@ -233,57 +242,11 @@ export class BoxelCLIClient {
command: string,
commandInput?: Record<string, unknown>,
): Promise<RunCommandResult> {
let url = `${ensureTrailingSlash(realmServerUrl)}_run-command`;
let body = {
data: {
type: 'run-command',
attributes: {
realmURL: realmUrl,
command,
commandInput: commandInput ?? null,
},
},
};

let response: Response;
try {
response = await this.pm.authedRealmServerFetch(url, {
method: 'POST',
headers: {
Accept: MIME.JSONAPI,
'Content-Type': MIME.JSONAPI,
},
body: JSON.stringify(body),
});
} catch (err) {
return {
status: 'error',
error: `run-command fetch failed: ${err instanceof Error ? err.message : String(err)}`,
};
}

if (!response.ok) {
return {
status: 'error',
error: `run-command HTTP ${response.status}: ${await response.text().catch(() => '(no body)')}`,
};
}

let json = (await response.json()) as {
data?: {
attributes?: {
status?: string;
cardResultString?: string | null;
error?: string | null;
};
};
};
let attrs = json.data?.attributes;
return {
status: (attrs?.status as RunCommandResult['status']) ?? 'error',
result: attrs?.cardResultString ?? null,
error: attrs?.error ?? null,
};
return coreRunCommand(command, realmUrl, {
realmServerUrl,
input: commandInput,
profileManager: this.pm,
});
}

/**
Expand Down Expand Up @@ -430,6 +393,24 @@ export class BoxelCLIClient {
});
}

/**
* One-way upload from a local workspace to a realm. Thin wrapper
* around the `realm push` command's programmatic `push()` function so
* the CLI and programmatic API share one implementation.
*/
async push(
realmUrl: string,
localDir: string,
options?: PushOptions,
): Promise<PushResult> {
return realmPush(localDir, realmUrl, {
delete: options?.delete,
dryRun: options?.dryRun,
force: options?.force,
profileManager: this.pm,
});
}

/**
* Bidirectional sync between a local workspace and a realm. Thin wrapper
* around the `realm sync` command's programmatic `sync()` function so the
Expand Down
Loading