spaps
Version:
Sweet Potato Authentication & Payment Service CLI - Docker Compose orchestrator for local Python/FastAPI SPAPS server with built-in admin middleware
1,399 lines ⢠50.7 kB
JavaScript
const chalk = require('chalk');
const fs = require('fs');
const { DEFAULT_PORT } = require('./config');
const { handleError } = require('./error-handler');
const { hasInteractiveTerminal } = require('./auth/env');
const { showQuickHelp } = require('./help-quick');
const { showQuickReference } = require('./docs-quick');
function lazy(factory) {
let value;
return () => {
if (value === undefined) {
value = factory();
}
return value;
};
}
const loadHelpSystem = lazy(() => require('./help-system'));
const loadDocsSystem = lazy(() => require('./docs-system'));
const loadAiHelper = lazy(() => require('./ai-helper'));
const loadAiToolSpec = lazy(() => require('./ai-tool-spec'));
const loadDoctor = lazy(() => require('./doctor'));
const loadHomeView = lazy(() => require('./home-view'));
const loadFixtureKernel = lazy(() => require('./fixture-kernel'));
const loadProjectScaffolder = lazy(() => require('./project-scaffolder'));
const loadAuthCommandHandlers = lazy(() => require('./auth/handlers'));
const loadDomainCli = lazy(() => require('./domain-cli'));
function openExternalUrl(url, { spawnImpl = null, platform = process.platform } = {}) {
const spawn = spawnImpl || require('child_process').spawn;
let command;
let args;
if (platform === 'darwin') {
command = 'open';
args = [url];
} else if (platform === 'win32') {
command = 'rundll32';
args = ['url.dll,FileProtocolHandler', url];
} else {
command = 'xdg-open';
args = [url];
}
const child = spawn(command, args, { stdio: 'ignore', detached: true });
child.on?.('error', () => {});
child.unref?.();
return { command, args };
}
function createHandlers(version, logo) {
function invalidArgument(message) {
const error = new Error(message);
error.code = 'EINVAL';
return error;
}
function emitInteractiveFallback(message) {
console.log(
chalk.yellow(`\nâ ď¸ ${message}. Showing the quick reference instead.\n`)
);
}
const verifyHandler = async ({ options }) => {
const { runQuickTest } = loadAiHelper();
const result = await runQuickTest({
port: options.port,
serverUrl: options.serverUrl,
cwd: process.cwd(),
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(chalk.yellow('\nđ SPAPS Verify\n'));
console.log(result.success ? chalk.green(result.summary) : chalk.yellow(result.summary));
console.log();
(result.results || []).forEach((entry) => {
const mark = entry.success ? chalk.green('â') : chalk.red('â');
const message = entry.message ? `: ${entry.message}` : '';
console.log(` ${mark} ${entry.test}${message}`);
if (!entry.success && entry.fix) {
console.log(chalk.gray(` fix: ${entry.fix}`));
}
});
if (Array.isArray(result.next_steps) && result.next_steps.length > 0) {
console.log();
console.log(chalk.bold('Next'));
result.next_steps.forEach((step) => {
console.log(chalk.cyan(` ${step}`));
});
}
console.log();
};
return {
home: async ({ options }) => {
const { buildHomeView, renderHomeView } = loadHomeView();
const view = await buildHomeView({
port: options.port,
serverUrl: options.serverUrl,
cwd: process.cwd(),
});
if (options.json) {
console.log(JSON.stringify(view, null, 2));
return;
}
renderHomeView(view, { logo });
},
local: async ({ options }) => {
const isJson = options.json;
if (!isJson) console.log(logo);
// Handle stop subcommand
if (options.stop) {
try {
const LocalServer = require('./local-server.js');
const server = new LocalServer({
port: options.port,
runtimeDir: options.runtimeDir,
runtimeSource: options.runtimeSource,
dataSource: options.dataSource,
json: isJson,
});
server.stop();
return;
} catch (error) {
handleError(error, { command: 'local stop' }, { json: isJson });
}
}
// Handle start
try {
const LocalServer = require('./local-server.js');
const server = new LocalServer({
port: options.port,
runtimeDir: options.runtimeDir,
runtimeSource: options.runtimeSource,
dataSource: options.dataSource,
json: isJson,
detach: options.detach,
fresh: options.fresh,
fromBackup: options.fromBackup
});
await server.start();
if (options.open && !isJson) {
const url = `http://localhost:${options.port}/docs`;
openExternalUrl(url);
}
// Set up shutdown handler
const shutdown = async () => {
await server.shutdown();
process.exit(0);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
} catch (error) {
handleError(error, { port: options.port, command: 'local' }, { json: isJson });
}
},
quickstart: async ({ options }) => {
const { getQuickStartInstructions } = loadAiHelper();
const instructions = await getQuickStartInstructions(options.port);
if (options.json) {
console.log(JSON.stringify(instructions, null, 2));
} else {
console.log(chalk.yellow('\nđ SPAPS Quick Start Instructions\n'));
const modeSummary = instructions.server.running
? instructions.auth.local_mode
? 'Mode: local mode active'
: 'Mode: provisioned application required'
: 'Mode: server unreachable';
console.log(modeSummary);
instructions.summary.forEach((line, index) => {
console.log(`${index + 1}. ${line}`);
});
console.log('\nFor JSON output: npx spaps quickstart --json');
}
},
status: async ({ options }) => {
const { getServerStatus } = loadAiHelper();
const status = await getServerStatus(options.port);
if (options.json) {
console.log(JSON.stringify(status));
if (!status.running) process.exitCode = 1;
} else {
if (!status.running) {
console.log(chalk.red('\nâ SPAPS server is not running'));
console.log('Start it with:');
console.log(chalk.cyan(` npx spaps local --port ${options.port}`));
console.log();
process.exitCode = 1;
} else {
console.log(chalk.green('\nâ
SPAPS server is running!\n'));
console.log(' URL:', chalk.cyan(status.url));
console.log(' Docs:', chalk.cyan(status.docs));
if (status.local_mode?.known) {
console.log(
' Mode:',
chalk.cyan(status.local_mode.active ? 'local mode active' : 'application key required')
);
}
console.log();
}
}
},
verify: verifyHandler,
init: async ({ options }) => {
const isJson = options.json;
const envContent = `# SPAPS Local Development\nSPAPS_API_URL=http://localhost:${DEFAULT_PORT}\n# SPAPS_API_KEY=your-api-key-here\n`;
const result = { success: true, command: 'init', files_created: [], files_skipped: [], next_steps: ['npx spaps local', 'npm install @spaps/sdk', 'Start coding!'] };
if (!fs.existsSync('.env.local')) {
fs.writeFileSync('.env.local', envContent);
result.files_created.push('.env.local');
if (!isJson) console.log(chalk.green('â
Created .env.local'));
} else {
result.files_skipped.push('.env.local');
result.message = '.env.local already exists';
if (!isJson) console.log(chalk.yellow('â ď¸ .env.local already exists'));
}
if (isJson) {
console.log(JSON.stringify(result));
} else {
console.log();
console.log(chalk.green('⨠SPAPS initialized!'));
console.log();
console.log('Next steps:');
console.log(chalk.cyan(' 1. Run: npx spaps local'));
console.log(chalk.cyan(' 2. Install SDK: npm install @spaps/sdk'));
console.log(chalk.cyan(' 3. Start coding!'));
}
},
create: async ({ options }) => {
const isJson = options.json;
const { createProjectStarter } = loadProjectScaffolder();
try {
const result = await createProjectStarter({
name: options.name,
template: options.template,
dir: options.dir,
port: options.port,
force: options.force,
version,
});
if (isJson) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(chalk.green(`\n⨠Created ${result.project_name} (${result.template})`));
console.log(chalk.cyan(` ${result.target_dir}`));
console.log(chalk.gray(` provisioning: ${result.provisioning.status}`));
if (result.files_created.length > 0) {
console.log(chalk.green('\nFiles created:'));
result.files_created.forEach((file) => {
console.log(chalk.gray(` ⢠${file}`));
});
}
if (result.files_overwritten.length > 0) {
console.log(chalk.yellow('\nFiles overwritten:'));
result.files_overwritten.forEach((file) => {
console.log(chalk.gray(` ⢠${file}`));
});
}
if (result.warnings.length > 0) {
console.log(chalk.yellow('\nWarnings:'));
result.warnings.forEach((warning) => {
console.log(chalk.gray(` ⢠${warning}`));
});
}
console.log(chalk.green('\nNext steps:'));
result.next_steps.forEach((step, index) => {
console.log(chalk.cyan(` ${index + 1}. ${step}`));
});
console.log();
} catch (error) {
handleError(
error,
{
command: 'create',
name: options.name,
template: options.template,
dir: options.dir,
},
{ json: isJson }
);
}
},
types: () => {
console.log(chalk.yellow('đ SPAPS'));
console.log(chalk.yellow(`đ§ 'spaps types' coming in v0.4.0!`));
},
help: async ({ options }) => {
if (options.interactive) {
if (!hasInteractiveTerminal()) {
emitInteractiveFallback('Interactive help requires a TTY');
showQuickHelp();
return;
}
await loadHelpSystem().showInteractiveHelp();
} else if (options.quick) {
showQuickHelp();
} else {
showQuickHelp();
}
},
docs: async ({ options }) => {
if (options.search) {
const results = loadDocsSystem().searchDocs(options.search);
if (options.json) {
console.log(JSON.stringify({ results }, null, 2));
} else {
console.log(chalk.yellow(`\nđ Search results for "${options.search}":\n`));
if (results.length === 0) {
console.log(chalk.gray(' No results found'));
} else {
results.forEach((result, i) => {
console.log(chalk.green(` ${i + 1}. ${result.title}`));
console.log(chalk.gray(` ${result.preview}`));
console.log();
});
}
console.log(chalk.blue(' Run: npx spaps docs --interactive'));
console.log(chalk.blue(' to browse full documentation\n'));
}
} else if (options.interactive) {
if (!hasInteractiveTerminal()) {
emitInteractiveFallback('Interactive docs require a TTY');
showQuickReference();
return;
}
await loadDocsSystem().showInteractiveDocs();
} else {
showQuickReference();
}
},
tools: async ({ options }) => {
const { buildToolSpec } = loadAiToolSpec();
const spec = await buildToolSpec({
format: options.format || 'openai',
port: options.port,
version,
});
if (options.json) {
console.log(JSON.stringify(spec, null, 2));
} else {
console.log(chalk.yellow('\nđ SPAPS AI Tool Spec (OpenAI-style)\n'));
console.log('Base URL:', spec.base_url);
if (spec.auth && typeof spec.auth.local_mode === 'boolean') {
console.log(
'Auth:',
spec.auth.local_mode ? 'local mode active' : 'application key required'
);
}
console.log('Tools:');
spec.tools.forEach((t, i) => {
console.log(chalk.green(` ${i + 1}. ${t.name}`), '-', t.description);
console.log(chalk.gray(` ${t.method} ${t.path}`));
});
console.log('\nTip: npx spaps tools --json > spaps-tools.json');
}
},
fixtures: async ({ options }) => {
const isJson = options.json;
const {
applyFixtures,
exportStorageState,
initFixtureKernel,
resetFixtures,
} = loadFixtureKernel();
try {
if (options.format && options.format !== 'playwright') {
throw invalidArgument(`Unsupported fixture format "${options.format}". Only "playwright" is currently supported.`);
}
let result;
switch (options.subcommand) {
case 'init':
result = await initFixtureKernel({
dir: options.dir,
port: options.port,
baseUrl: options.baseUrl,
version,
force: options.force,
});
break;
case 'apply':
result = await applyFixtures({
dir: options.dir,
port: options.port,
baseUrl: options.baseUrl,
version,
persona: options.persona,
seed: options.seed,
syncServer: options.syncServer,
});
break;
case 'reset':
result = await resetFixtures({
dir: options.dir,
port: options.port,
baseUrl: options.baseUrl,
version,
seed: options.seed,
syncServer: options.syncServer,
});
break;
case 'storage-state':
if (!options.persona) {
throw invalidArgument('The fixtures storage-state command requires --persona.');
}
result = await exportStorageState({
dir: options.dir,
port: options.port,
baseUrl: options.baseUrl,
version,
persona: options.persona,
seed: options.seed,
syncServer: options.syncServer,
});
break;
default:
throw invalidArgument(
`Unsupported fixtures subcommand "${options.subcommand}". Use init, apply, reset, or storage-state.`
);
}
if (isJson) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(chalk.green(`\n⨠.spaps fixtures ${result.subcommand} complete`));
console.log(chalk.cyan(` ${result.fixture_dir}`));
if (Array.isArray(result.files_created) && result.files_created.length > 0) {
console.log(chalk.green('\nFiles created:'));
result.files_created.forEach((file) => {
console.log(chalk.gray(` ⢠${file}`));
});
}
if (Array.isArray(result.files_overwritten) && result.files_overwritten.length > 0) {
console.log(chalk.yellow('\nFiles overwritten:'));
result.files_overwritten.forEach((file) => {
console.log(chalk.gray(` ⢠${file}`));
});
}
if (Array.isArray(result.removed) && result.removed.length > 0) {
console.log(chalk.yellow('\nRemoved stale artifacts:'));
result.removed.forEach((file) => {
console.log(chalk.gray(` ⢠${file}`));
});
}
if (Array.isArray(result.seeding?.personas) && result.seeding.personas.length > 0) {
console.log(chalk.green('\nSeeded personas:'));
result.seeding.personas.forEach((entry) => {
console.log(chalk.gray(` ⢠${entry.persona} (${entry.request_count} requests)`));
});
}
if (Array.isArray(result.server_sync?.personas) && result.server_sync.personas.length > 0) {
console.log(chalk.green('\nSynced server fixtures:'));
console.log(chalk.gray(` app: ${result.server_sync.application.slug}`));
result.server_sync.personas.forEach((entry) => {
console.log(chalk.gray(` ⢠${entry.code} (${entry.user.id})`));
});
}
if (result.generated?.personas?.length) {
console.log(chalk.green('\nGenerated personas:'));
result.generated.personas.forEach((entry) => {
console.log(chalk.gray(` ⢠${entry.persona}`));
console.log(chalk.gray(` storageState: ${entry.storage_state_path}`));
console.log(chalk.gray(` headers: ${entry.headers_path}`));
});
}
if (result.generated?.bridge?.script_path) {
console.log(chalk.green('\nFrontend bridge:'));
console.log(chalk.gray(` script: ${result.generated.bridge.script_path}`));
console.log(chalk.gray(` include: <script src="${result.generated.bridge.public_url}"></script>`));
}
if (result.storage_state_path) {
console.log(chalk.green('\nStorage state:'));
console.log(chalk.gray(` ${result.storage_state_path}`));
console.log(chalk.gray(` headers: ${result.headers_path}`));
}
if (result.bridge?.script_path) {
console.log(chalk.green('\nFrontend bridge:'));
console.log(chalk.gray(` script: ${result.bridge.script_path}`));
console.log(chalk.gray(` include: <script src="${result.bridge.public_url}"></script>`));
}
console.log(chalk.green('\nNext steps:'));
(result.next_steps || []).forEach((step, index) => {
console.log(chalk.cyan(` ${index + 1}. ${step}`));
});
console.log();
} catch (error) {
handleError(
error,
{
command: 'fixtures',
subcommand: options.subcommand,
dir: options.dir,
persona: options.persona,
},
{ json: isJson }
);
}
},
doctor: async ({ options }) => {
const { runDoctor } = loadDoctor();
await runDoctor({
port: options.port || DEFAULT_PORT,
serverUrl: options.serverUrl || null,
origin: options.origin || null,
stripe: options.stripe || null,
json: options.json,
});
},
test: verifyHandler,
auth: async (...args) => loadAuthCommandHandlers().authHandler(...args),
login: async (...args) => loadAuthCommandHandlers().loginHandler(...args),
logout: async (...args) => loadAuthCommandHandlers().logoutHandler(...args),
whoami: async (...args) => loadAuthCommandHandlers().whoamiHandler(...args),
token: async (...args) => loadAuthCommandHandlers().tokenHandler(...args),
billing: billingHandler,
dayrate: dayrateHandler,
email: emailHandler,
policy: policyHandler,
webhook: webhookHandler,
'issue-reports': issueReportsHandler,
access: capabilityHandler,
journey: capabilityHandler,
graph: capabilityHandler,
explain: capabilityHandler,
contract: capabilityHandler,
};
}
function emitText({ intent, result, isJson, successMessage = null }) {
const { emit } = loadDomainCli();
if (isJson) {
emit({ intent, result, isJson, successMessage });
return;
}
if (!result.ok) {
emit({ intent, result, isJson, successMessage });
return;
}
if (successMessage) {
console.log(successMessage);
}
const output = typeof result.data === 'string' ? result.data : JSON.stringify(result.data, null, 2);
process.stdout.write(output);
if (!output.endsWith('\n')) process.stdout.write('\n');
}
function parseJsonFlag(raw, label) {
if (raw === null || raw === undefined) {
return { ok: true, value: null };
}
try {
return { ok: true, value: JSON.parse(raw) };
} catch {
console.error(`${label}: must be valid JSON`);
process.exitCode = 2;
return { ok: false, value: null };
}
}
function parseBooleanFlag(raw, label) {
if (raw === null || raw === undefined) {
return { ok: true, value: null };
}
if (typeof raw === 'boolean') {
return { ok: true, value: raw };
}
const normalized = String(raw).trim().toLowerCase();
if (['true', '1', 'yes', 'y'].includes(normalized)) {
return { ok: true, value: true };
}
if (['false', '0', 'no', 'n'].includes(normalized)) {
return { ok: true, value: false };
}
console.error(`${label}: must be one of true|false|1|0|yes|no`);
process.exitCode = 2;
return { ok: false, value: null };
}
function requireOption(value, message) {
if (value !== null && value !== undefined && value !== '') {
return true;
}
console.error(message);
process.exitCode = 2;
return false;
}
function assignIfDefined(target, key, value) {
if (value !== null && value !== undefined) {
target[key] = value;
}
}
function splitCsv(raw) {
if (!raw) return [];
return String(raw)
.split(',')
.map((value) => value.trim())
.filter(Boolean);
}
function extractErrorObject(result) {
const candidates = [result?.raw, result?.data];
for (const candidate of candidates) {
if (!candidate || typeof candidate !== 'object') continue;
if (candidate.error && typeof candidate.error === 'object') return candidate.error;
if (typeof candidate.code === 'string' || typeof candidate.message === 'string') {
return candidate;
}
}
return null;
}
function extractErrorMessage(result) {
const error = extractErrorObject(result);
return (
error?.message ||
result?.data?.detail ||
(result?.status ? `HTTP ${result.status}` : 'Request failed')
);
}
function extractErrorCode(result) {
const error = extractErrorObject(result);
return error?.code || (result?.status ? `http_${result.status}` : 'request_failed');
}
function extractRequestId(result) {
const raw = result?.raw;
if (!raw || typeof raw !== 'object') return null;
return raw.request_id || raw.metadata?.request_id || null;
}
function extractSources(data) {
if (Array.isArray(data?.sources)) return data.sources;
if (Array.isArray(data?.decision?.sources)) return data.decision.sources;
return [];
}
function normalizeDiagnostic(item, fallbackStatus = null) {
const diagnostic = {
code: item?.code || (fallbackStatus ? `http_${fallbackStatus}` : 'request_failed'),
message: item?.message || 'Request failed',
status: item?.status ?? fallbackStatus,
};
assignIfDefined(diagnostic, 'details', item?.details);
return diagnostic;
}
function extractPayloadDiagnostics(result) {
const raw = result?.raw;
if (Array.isArray(raw?.diagnostics)) {
return raw.diagnostics.map((item) => normalizeDiagnostic(item, result?.status || null));
}
return [];
}
function extractRemediations(data) {
const actions = Array.isArray(data?.next_actions) ? data.next_actions : [];
return actions.map((action) => {
const remediation = {
kind: action.kind,
label: action.label || null,
method: action.method || null,
path: action.path || null,
cli: action.cli || null,
requires: Array.isArray(action.requires) ? action.requires : [],
};
assignIfDefined(remediation, 'command_template', action.command_template);
assignIfDefined(remediation, 'safe_to_execute', action.safe_to_execute);
assignIfDefined(remediation, 'operator_gate_required', action.operator_gate_required);
return remediation;
});
}
function normalizeRemediation(item) {
const remediation = {
kind: item.kind,
label: item.label || null,
method: item.method || null,
path: item.path || null,
cli: item.cli || null,
requires: Array.isArray(item.requires) ? item.requires : [],
};
assignIfDefined(remediation, 'details', item.details);
assignIfDefined(remediation, 'command_template', item.command_template);
assignIfDefined(remediation, 'safe_to_execute', item.safe_to_execute);
assignIfDefined(remediation, 'operator_gate_required', item.operator_gate_required);
return remediation;
}
function extractPayloadRemediations(result) {
const raw = result?.raw;
if (!Array.isArray(raw?.remediations)) return [];
return raw.remediations
.filter((item) => item && typeof item === 'object' && item.kind)
.map((item) => normalizeRemediation(item));
}
function capabilityEnvelope({ command, result, data = null, success = null, diagnostics = [] }) {
const resolvedData = data === null && result ? result.data : data;
const ok = success === null ? Boolean(result?.ok) : Boolean(success);
const envelopeDiagnostics = Array.isArray(diagnostics) ? diagnostics.slice() : [];
envelopeDiagnostics.push(...extractPayloadDiagnostics(result));
if (result && !result.ok) {
const hasServerDiagnostic = envelopeDiagnostics.length > 0;
if (!hasServerDiagnostic) {
const error = extractErrorObject(result);
const diagnostic = {
code: extractErrorCode(result),
message: extractErrorMessage(result),
status: result.status || null,
};
assignIfDefined(diagnostic, 'details', error?.details);
envelopeDiagnostics.push(diagnostic);
}
}
const payloadRemediations = extractPayloadRemediations(result);
const envelope = {
schema_version: 'spaps.cli.capability.v1',
command,
success: ok,
status: result?.status || null,
data: resolvedData,
diagnostics: envelopeDiagnostics,
remediations: payloadRemediations.length ? payloadRemediations : extractRemediations(resolvedData),
sources: extractSources(resolvedData),
};
assignIfDefined(envelope, 'request_id', extractRequestId(result));
assignIfDefined(envelope, 'timestamp', result?.raw?.timestamp);
return envelope;
}
function emitCapability({ command, result, isJson, exitCodeOnFailure = 10 }) {
if (isJson) {
console.log(JSON.stringify(capabilityEnvelope({ command, result }), null, 2));
} else if (!result.ok) {
console.error(`\u2717 ${command}: ${extractErrorMessage(result)}`);
} else {
const data = result.data || {};
if (typeof data.allowed === 'boolean') {
console.log(`${command}: ${data.allowed ? 'allowed' : data.outcome || 'not allowed'}`);
(data.reasons || []).forEach((reason) => {
console.log(` - ${reason.message || reason.code}`);
});
} else if (typeof data.status === 'string' && typeof data.allowed === 'boolean') {
console.log(`${command}: ${data.status}`);
} else {
console.log(JSON.stringify(data, null, 2));
}
}
if (!result.ok) {
process.exitCode = exitCodeOnFailure;
}
}
function emitCapabilityError({ command, err, isJson, exitCode = 10 }) {
const message = err && err.message ? err.message : String(err);
const code = err && err.code ? err.code : 'ERROR';
if (isJson) {
console.log(JSON.stringify({
schema_version: 'spaps.cli.capability.v1',
command,
success: false,
status: err?.status || null,
data: null,
diagnostics: [{ code, message, status: err?.status || null }],
remediations: code === 'NOT_AUTHENTICATED' || code === 'SESSION_EXPIRED'
? [{ kind: 'authenticate', label: 'Run spaps login', cli: 'spaps login', requires: ['operator'] }]
: [],
sources: [],
}, null, 2));
} else {
const hint = code === 'NOT_AUTHENTICATED' || code === 'SESSION_EXPIRED'
? ' (run `spaps login` first)'
: '';
console.error(`\u2717 ${command}: ${message}${hint}`);
}
process.exitCode = exitCode;
}
function buildAccessDecisionPayload(options, prefix) {
if (!requireOption(options.action, `${prefix}: --action is required`)) return null;
if (!requireOption(options.resourceType, `${prefix}: --resource-type is required`)) return null;
if (!requireOption(options.resourceRef, `${prefix}: --resource-ref is required`)) return null;
const context = parseJsonFlag(options.context, `${prefix}: --context`);
if (!context.ok) return null;
const policyContext = parseJsonFlag(options.policyContext, `${prefix}: --policy-context`);
if (!policyContext.ok) return null;
const usageDimensions = parseJsonFlag(options.usageDimensions, `${prefix}: --usage-dimensions`);
if (!usageDimensions.ok) return null;
const authenticated = parseBooleanFlag(options.authenticated, `${prefix}: --authenticated`);
if (!authenticated.ok) return null;
const actor = {
actor_type: options.actorType || 'user',
};
assignIfDefined(actor, 'actor_ref', options.actorRef);
assignIfDefined(actor, 'user_id', options.userId);
assignIfDefined(actor, 'email', options.email);
assignIfDefined(actor, 'agent_id', options.agentId);
assignIfDefined(actor, 'authenticated', authenticated.value);
const resource = {
resource_type: options.resourceType,
resource_ref: options.resourceRef,
};
assignIfDefined(resource, 'resource_id', options.resourceId);
assignIfDefined(resource, 'resource_key', options.resourceKey);
const controls = {
approval_required: Boolean(options.approvalRequired),
authority_scope: options.authorityScope || 'application',
};
assignIfDefined(controls, 'entitlement_key', options.entitlementKey);
assignIfDefined(controls, 'entitlement_resource_type', options.entitlementResourceType);
assignIfDefined(controls, 'entitlement_resource_id', options.entitlementResourceId);
assignIfDefined(controls, 'policy_name', options.policyName);
assignIfDefined(controls, 'policy_context', policyContext.value);
assignIfDefined(controls, 'usage_feature_key', options.usageFeatureKey);
assignIfDefined(controls, 'usage_dimensions', usageDimensions.value);
assignIfDefined(controls, 'x402_resource_key', options.x402ResourceKey);
assignIfDefined(controls, 'approval_id', options.approvalId);
const payload = {
actor,
action: options.action,
resource,
controls,
context: context.value || {},
};
assignIfDefined(payload, 'idempotency_key', options.idempotencyKey);
assignIfDefined(payload, 'correlation_id', options.correlationId);
return payload;
}
async function dayrateHandler({ options }) {
const { callEndpoint, emit, emitAuthError } = loadDomainCli();
const isJson = Boolean(options.json);
const sub = options.subcommand;
if (sub !== 'config') {
console.error('dayrate: unknown subcommand. Supported: config');
process.exitCode = 2;
return;
}
try {
const result = await callEndpoint({ options, method: 'GET', path: '/api/dayrate/admin/config' });
emit({ intent: 'dayrate.config', result, isJson });
if (!result.ok) process.exitCode = 1;
} catch (err) {
emitAuthError('dayrate.config', err, isJson);
process.exitCode = 1;
}
}
async function billingHandler({ options }) {
const { callEndpoint, emit, emitAuthError } = loadDomainCli();
const isJson = Boolean(options.json);
const sub = options.subcommand;
try {
if (sub === 'status') {
const result = await callEndpoint({
options,
method: 'GET',
path: '/api/admin/billing/status',
});
emit({ intent: 'billing.status', result, isJson });
if (!result.ok) process.exitCode = 1;
return;
}
if (sub === 'verify') {
const result = await callEndpoint({
options,
method: 'GET',
path: '/api/admin/billing/verify',
});
emit({ intent: 'billing.verify', result, isJson });
if (!result.ok || result.data?.valid === false) process.exitCode = 1;
return;
}
if (sub === 'attach') {
if (!requireOption(options.billingAccountId, 'billing attach: --billing-account-id is required')) return;
const result = await callEndpoint({
options,
method: 'POST',
path: '/api/admin/billing/attach',
body: { billing_account_id: options.billingAccountId },
});
emit({ intent: 'billing.attach', result, isJson });
if (!result.ok) process.exitCode = 1;
return;
}
console.error('billing: unknown subcommand. Supported: status, attach, verify');
process.exitCode = 2;
} catch (err) {
emitAuthError(`billing.${sub || 'unknown'}`, err, isJson);
process.exitCode = 1;
}
}
async function emailHandler({ options }) {
const { callEndpoint, emit, emitAuthError } = loadDomainCli();
const isJson = Boolean(options.json);
const sub = options.subcommand;
try {
if (sub === 'send') {
if (!requireOption(options.templateKey, 'email send: --template-key is required')) return;
if (!requireOption(options.to, 'email send: --to is required')) return;
const context = parseJsonFlag(options.context, 'email send: --context');
if (!context.ok) return;
const body = {
template_key: options.templateKey,
to: options.to,
};
assignIfDefined(body, 'context', context.value);
assignIfDefined(body, 'user_id', options.userId);
assignIfDefined(body, 'owner_id', options.ownerId);
assignIfDefined(body, 'subject_override', options.subjectOverride);
assignIfDefined(body, 'body_override', options.bodyOverride);
assignIfDefined(body, 'idempotency_key', options.idempotencyKey);
const result = await callEndpoint({ options, method: 'POST', path: '/api/email/send', body });
emit({ intent: 'email.send', result, isJson });
if (!result.ok) process.exitCode = 1;
return;
}
if (sub === 'get-template') {
if (!requireOption(options.templateKey, 'email get-template: --template-key is required')) return;
const result = await callEndpoint({
options,
method: 'GET',
path: `/api/email/templates/${encodeURIComponent(options.templateKey)}`,
});
emit({ intent: 'email.get-template', result, isJson });
if (!result.ok) process.exitCode = 1;
return;
}
if (sub === 'preview') {
if (!requireOption(options.templateKey, 'email preview: --template-key is required')) return;
const context = parseJsonFlag(options.context, 'email preview: --context');
if (!context.ok) return;
const path = `/api/email/templates/${encodeURIComponent(options.templateKey)}/preview`;
const result = context.value === null
? await callEndpoint({ options, method: 'GET', path })
: await callEndpoint({
options,
method: 'POST',
path,
body: { context: context.value },
});
emitText({ intent: 'email.preview', result, isJson });
if (!result.ok) process.exitCode = 1;
return;
}
if (sub === 'logs') {
const query = {};
if (options.ownerId) query.owner_id = options.ownerId;
if (options.userId) query.user_id = options.userId;
if (options.limit) query.limit = options.limit;
if (options.offset) query.offset = options.offset;
const result = await callEndpoint({ options, method: 'GET', path: '/api/email/logs', query });
emit({ intent: 'email.logs', result, isJson });
if (!result.ok) process.exitCode = 1;
return;
}
if (sub === 'list-templates') {
const result = await callEndpoint({ options, method: 'GET', path: '/api/email/templates' });
emit({ intent: 'email.list-templates', result, isJson });
if (!result.ok) process.exitCode = 1;
return;
}
if (sub === 'create-template') {
if (!requireOption(options.templateKey, 'email create-template: --template-key is required')) return;
if (!requireOption(options.name, 'email create-template: --name is required')) return;
if (!requireOption(options.subject, 'email create-template: --subject is required')) return;
if (!requireOption(options.htmlBody, 'email create-template: --html-body is required')) return;
const variables = parseJsonFlag(options.variables, 'email create-template: --variables');
if (!variables.ok) return;
const sampleContext = parseJsonFlag(options.sampleContext, 'email create-template: --sample-context');
if (!sampleContext.ok) return;
const isActive = parseBooleanFlag(options.isActive, 'email create-template: --is-active');
if (!isActive.ok) return;
const body = {
template_key: options.templateKey,
name: options.name,
subject: options.subject,
html_body: options.htmlBody,
};
assignIfDefined(body, 'description', options.description);
assignIfDefined(body, 'text_body', options.textBody);
assignIfDefined(body, 'from_email', options.fromEmail);
assignIfDefined(body, 'from_name', options.fromName);
assignIfDefined(body, 'reply_to', options.replyTo);
assignIfDefined(body, 'variables', variables.value);
assignIfDefined(body, 'sample_context', sampleContext.value);
assignIfDefined(body, 'is_active', isActive.value);
assignIfDefined(body, 'category', options.category);
const result = await callEndpoint({ options, method: 'POST', path: '/api/email/templates', body });
emit({ intent: 'email.create-template', result, isJson });
if (!result.ok) process.exitCode = 1;
return;
}
if (sub === 'update-template') {
if (!requireOption(options.templateKey, 'email update-template: --template-key is required')) return;
const variables = parseJsonFlag(options.variables, 'email update-template: --variables');
if (!variables.ok) return;
const sampleContext = parseJsonFlag(options.sampleContext, 'email update-template: --sample-context');
if (!sampleContext.ok) return;
const isActive = parseBooleanFlag(options.isActive, 'email update-template: --is-active');
if (!isActive.ok) return;
const body = {};
assignIfDefined(body, 'name', options.name);
assignIfDefined(body, 'description', options.description);
assignIfDefined(body, 'subject', options.subject);
assignIfDefined(body, 'html_body', options.htmlBody);
assignIfDefined(body, 'text_body', options.textBody);
assignIfDefined(body, 'from_email', options.fromEmail);
assignIfDefined(body, 'from_name', options.fromName);
assignIfDefined(body, 'reply_to', options.replyTo);
assignIfDefined(body, 'variables', variables.value);
assignIfDefined(body, 'sample_context', sampleContext.value);
assignIfDefined(body, 'is_active', isActive.value);
assignIfDefined(body, 'category', options.category);
if (Object.keys(body).length === 0) {
console.error('email update-template: provide at least one field to update');
process.exitCode = 2;
return;
}
const result = await callEndpoint({
options,
method: 'PUT',
path: `/api/email/templates/${encodeURIComponent(options.templateKey)}`,
body,
});
emit({ intent: 'email.update-template', result, isJson });
if (!result.ok) process.exitCode = 1;
return;
}
if (sub === 'get-override') {
if (!requireOption(options.templateKey, 'email get-override: --template-key is required')) return;
const result = await callEndpoint({
options,
method: 'GET',
path: `/api/email/templates/${encodeURIComponent(options.templateKey)}/override`,
});
emit({ intent: 'email.get-override', result, isJson });
if (!result.ok) process.exitCode = 1;
return;
}
if (sub === 'set-override') {
if (!requireOption(options.templateKey, 'email set-override: --template-key is required')) return;
const body = {};
assignIfDefined(body, 'subject_override', options.subjectOverride);
assignIfDefined(body, 'body_override', options.bodyOverride);
if (Object.keys(body).length === 0) {
console.error('email set-override: provide --subject-override and/or --body-override');
process.exitCode = 2;
return;
}
const result = await callEndpoint({
options,
method: 'PUT',
path: `/api/email/templates/${encodeURIComponent(options.templateKey)}/override`,
body,
});
emit({ intent: 'email.set-override', result, isJson });
if (!result.ok) process.exitCode = 1;
return;
}
if (sub === 'clear-override') {
if (!requireOption(options.templateKey, 'email clear-override: --template-key is required')) return;
const result = await callEndpoint({
options,
method: 'DELETE',
path: `/api/email/templates/${encodeURIComponent(options.templateKey)}/override`,
});
emit({ intent: 'email.clear-override', result, isJson });
if (!result.ok) process.exitCode = 1;
return;
}
console.error(
'email: unknown subcommand. Supported: send, get-template, preview, logs, list-templates, create-template, update-template, get-override, set-override, clear-override'
);
process.exitCode = 2;
} catch (err) {
emitAuthError(`email.${sub || 'unknown'}`, err, isJson);
process.exitCode = 1;
}
}
async function policyHandler({ options }) {
const { callEndpoint, emit, emitAuthError } = loadDomainCli();
const isJson = Boolean(options.json);
const sub = options.subcommand;
try {
if (sub === 'list') {
const query = {};
if (options.isActive !== null && options.isActive !== undefined) query.is_active = options.isActive;
if (options.limit) query.limit = options.limit;
const result = await callEndpoint({ options, method: 'GET', path: '/api/policies', query });
emit({ intent: 'policy.list', result, isJson });
if (!result.ok) process.exitCode = 1;
return;
}
if (sub === 'create') {
if (!options.name || !options.effect) {
console.error('policy create: --name and --effect are required');
process.exitCode = 2;
return;
}
let conditions = {};
if (options.conditions) {
try { conditions = JSON.parse(options.conditions); }
catch { console.error('policy create: --conditions must be valid JSON'); process.exitCode = 2; return; }
}
const body = {
name: options.name,
effect: options.effect,
conditions,
description: options.description || null,
priority: options.priority || 0,
};
const result = await callEndpoint({ options, method: 'POST', path: '/api/policies', body });
emit({ intent: 'policy.create', result, isJson });
if (!result.ok) process.exitCode = 1;
return;
}
if (sub === 'delete') {
if (!options.id) {
console.error('policy delete: --id is required');
process.exitCode = 2;
return;
}
const result = await callEndpoint({ options, method: 'DELETE', path: `/api/policies/${encodeURIComponent(options.id)}` });
emit({ intent: 'policy.delete', result, isJson });
if (!result.ok) process.exitCode = 1;
return;
}
console.error('policy: unknown subcommand. Supported: list, create, delete');
process.exitCode = 2;
} catch (err) {
emitAuthError(`policy.${sub || 'unknown'}`, err, isJson);
process.exitCode = 1;
}
}
async function webhookHandler({ options }) {
const { callEndpoint, emit, emitAuthError } = loadDomainCli();
const isJson = Boolean(options.json);
const sub = options.subcommand;
try {
if (sub === 'list') {
const result = await callEndpoint({ options, method: 'GET', path: '/api/webhooks' });
emit({ intent: 'webhook.list', result, isJson });
if (!result.ok) process.exitCode = 1;
return;
}
if (sub === 'register') {
if (!options.url || !options.events) {
console.error('webhook register: --url and --events (comma-separated) are required');
process.exitCode = 2;
return;
}
const events = String(options.events).split(',').map((e) => e.trim()).filter(Boolean);
const body = { url: options.url, events };
const result = await callEndpoint({ options, method: 'POST', path: '/api/webhooks', body });
emit({ intent: 'webhook.register', result, isJson });
if (!result.ok) process.exitCode = 1;
return;
}
console.error('webhook: unknown subcommand. Supported: list, register');
process.exitCode = 2;
} catch (err) {
emitAuthError(`webhook.${sub || 'unknown'}`, err, isJson);
process.exitCode = 1;
}
}
async function issueReportsHandler({ options }) {
const { callEndpoint, emit, emitAuthError } = loadDomainCli();
const isJson = Boolean(options.json);
const sub = options.subcommand;
if (sub !== 'list-mine') {
console.error('issue-reports: unknown subcommand. Supported: list-mine');
process.exitCode = 2;
return;
}
try {
const query = {};
if (options.status) query.status = options.status;
if (options.limit) query.limit = options.limit;
if (options.offset) query.offset = options.offset;
const result = await callEndpoint({ options, method: 'GET', path: '/api/v1/issue-reports', query });
emit({ intent: 'issue-reports.list-mine', result, isJson });
if (!result.ok) process.exitCode = 1;
} catch (err) {
emitAuthError('issue-reports.list-mine', err, isJson);
process.exitCode = 1;
}
}
async function capabilityHandler({ name, options }) {
const { callEndpoint } = loadDomainCli();
const isJson = Boolean(options.json);
const command = name === 'access'
? `access.${options.subcommand || 'unknown'}`
: name === 'journey'
? `journey.${options.subcommand || 'unknown'}`
: name === 'graph'
? `graph.${options.subcommand || 'unknown'}`
: name;
try {
if (name === 'access') {
if (options.subcommand !== 'check') {
console.error('access: unknown subcommand. Supported: check');
process.exitCode = 2;
return;
}
const body = buildAccessDecisionPayload(options, 'access check');
if (!body) return;
const result = await callEndpoint({
options,
method: 'POST',
path: '/api/access/decide',
body,
});
emitCapability({ command, result, isJson, exitCodeOnFailure: 10 });
return;
}
if (name === 'journey') {
if (options.subcommand !== 'run') {
console.error('journey: unknown subcommand. Supported: run');
process.exitCode = 2;
return;
}
const access = buildAccessDecisionPayload(options, 'journey run');
if (!access) return;
const operatorLabels = splitCsv(options.operatorLabels)
.filter((label) => label.toLowerCase() !== 'operator-gated');
const body = {
access,
include_command_templates: Boolean(options.includeCommandTemplates),
operator_labels: operatorLabels,
environment: options.environment || 'production',
};
const result = await callEndpoint({
options,
method: 'POST',
path: '/api/actions/prepare',
body,
});
emitCapability({ command, result, isJson, exitCodeOnFailure: 30 });
return;
}
if (name === 'graph') {
let result;
if (options.subcommand === 'nodes') {
const query = {};
assignIfDefined(query, 'node_type', options.nodeType);
assignIfDefined(query, 'status', options.status);
assignIfDefined(query, 'q', options.query);
assignIfDefined(query, 'cursor', options.cursor);
assignIfDefined(query, 'limit', options.limit);
assignIfDefined(query, 'application_id', options.applicationId);
result = await callEndpoint({ options, method: 'GET', path: '/api/graph/nodes', query });
} else if (options.subcommand === 'paths') {
const query = {
from_node_key: options.fromNodeKey,
to_node_key: options.toNodeKey,
};
assignIfDefined(query, 'max_depth', options.maxDepth);
assignIfDefined(query, 'limit', options.limit);
assignIfDefined(query, 'include_stale', options.includeStale);
assignIfDefined(query, 'application_id', options.applicationId);
result = await callEndpoint({ options, method: 'GET', path: '/api/graph/paths', query });
} else if (options.subcommand === 'impact') {
const query = {
node_key: options.nodeKey,
};
assignIfDefined(query, 'max_depth', options.maxDepth);
assignIfDefined(query, 'limit', options.limit);
assignIfDefined(query, 'include_stale', options.includeStale);
assignIfDefined(query, 'application_id', options.applicationId);
result = await callEndpoint({ options, method: 'GET', path: '/api/graph/impact', query });
} else if (options.subcommand === 'refresh') {
const query = {};
assignIfDefined(query, 'application_id', options.applicationId);
assignIfDefined(query, 'correlation_id', options.correlationId);
result = await callEndpoint({ options, method: 'POST', path: '/api/graph/refresh', query });
} else {
console.error('graph: unknown subcommand. Supported: nodes, paths, impact, refresh');
process.exitCode = 2;
return;
}
emitCapability({ command, result, isJson, exitCodeOnFailure: 10 });
return;
}
if (name === 'explain') {
if (!requireOption(options.decisionId, 'explain: <decision-id> is required')) return;
const result = await callEndpoint({
options,
method: 'GET',
path: `/api/graph/explain/${encodeURIComponent(options.decisionId)}`,
});
emitCapability({ command, result, isJson, exitCodeOnFailure: 10 });
return;
}
if (name === 'contract') {
const result = await callEndpoint({
options,
method: 'GET',
path: '/api/contract',
});
emitCapability({ command, result, isJson, exitCodeOnFailure: 20 });
return;
}
console.error('capability: unsupported command');
process.exitCode = 2;
} catch (err) {
emitCapabilityError({
command,
err,
isJson,
exitCode: name === 'contract' ? 20 : name === 'journey' ? 30 : 10,
});
}
}
module.exports = { createHandlers, openExternalUrl };