@swell/cli
Version:
Swell's command line interface/utility
160 lines (158 loc) • 6.48 kB
JavaScript
import { Args, Flags } from '@oclif/core';
import { FetchError } from 'node-fetch';
import { default as localConfig } from '../../lib/config.js';
import { SwellCommand } from '../../swell-command.js';
export default class Content extends SwellCommand {
static summary = 'Inspect content view extensions deployed in your store.';
static description = `
View content view extensions available in your Swell store.
Without a content ID, this command lists all available content views.
With a content ID, it retrieves the configuration for that specific content view in JSON format.
`;
static args = {
'content-id': Args.string({
description: 'Optional content view ID. Example: custom.admin.products, app.myapp.products',
required: false,
}),
};
static flags = {
live: Flags.boolean({
description: 'Use the live environment instead of test (default: test).',
default: false,
}),
yes: Flags.boolean({
char: 'y',
description: 'Accept all prompts automatically (for agent compatibility; no functional effect).',
default: false,
}),
};
static examples = [
'swell inspect content',
'swell inspect content custom.admin.products',
'swell inspect content app.honest_reviews.reviews',
'swell inspect content app.myapp.products --live',
];
async run() {
const { args, flags } = await this.parse(Content);
const { 'content-id': contentId } = args;
const { live } = flags;
if (!live) {
await this.api.setEnv('test');
}
// Detail mode: show configuration for a specific content view
if (contentId) {
await this.showContentDetail(contentId);
return;
}
const store = localConfig.getDefaultStore();
// List mode: show all content views
await this.listContent(store, live);
}
async catch(error) {
if (error instanceof FetchError) {
const message = `Could not connect to Swell API. Please try again later: ${error.message}`;
return this.error(message, { exit: 2, code: error.code });
}
return this.error(error.message, { exit: 1 });
}
async listContent(store, live) {
const { results: apps } = await this.api.get({
adminPath: '/apps',
});
const appsById = Object.fromEntries(apps.map((app) => [app.id, app]));
const { results: contentViews } = await this.api.getAll({
adminPath: '/data/:content',
});
// Sort by resolved ID in reverse order (custom.* first)
const sortedContentViews = [...contentViews].sort((a, b) => {
const resolvedA = this.resolveContentId(a, appsById);
const resolvedB = this.resolveContentId(b, appsById);
return resolvedB.localeCompare(resolvedA);
});
this.log(`Content views extensions available in '${store}' ${live ? '[live]' : '[test]'}:`);
this.log();
this.showContentViews(sortedContentViews, appsById);
// Show next step hints
this.log();
this.log('Examples:');
this.log(' swell inspect content custom.admin.products');
this.log(' swell inspect content app.honest_reviews.accounts');
}
showContentViews(contentViews, appsById) {
const maxNameWidth = Math.max(...contentViews.map((cv) => cv.name.length), 15);
for (const cv of contentViews) {
this.showContentView(cv, maxNameWidth, appsById);
}
}
showContentView(contentView, nameWidth, appsById) {
const resolvedId = this.resolveContentId(contentView, appsById);
const paddedName = contentView.name.padEnd(nameWidth + 2);
this.log(`${paddedName}${resolvedId}`);
}
resolveContentId(contentView, appsById) {
const { id, name } = contentView;
// Parse ID: app.{appId}.{name} or custom.{source}.{name}
const match = id.match(/^(app|custom)\.([^.]+)\.(.+)$/);
if (!match)
return id; // fallback to original
const [, type, identifier] = match;
if (type === 'custom') {
return id; // custom IDs are already resolved
}
// type === 'app': resolve app ID to slug
const appId = identifier;
const app = appsById[appId];
if (!app) {
return id; // fallback if app not found
}
const appSlug = this.getAppSlugId(app);
return `app.${appSlug}.${name}`;
}
async showContentDetail(resolvedId) {
// Reverse resolve to get unresolved ID for API
const unresolvedId = await this.reverseResolveContentId(resolvedId);
// Fetch content view
const content = await this.api.get({
adminPath: `/data/:content/${unresolvedId}`,
});
if (!content) {
this.throwContentNotFound(resolvedId);
}
this.showContent(content);
}
async reverseResolveContentId(resolvedId) {
// Parse ID: app.{slug}.{name} or custom.{source}.{name}
const match = resolvedId.match(/^(app|custom)\.([^.]+)\.(.+)$/);
if (!match) {
throw new Error(`Invalid content ID format: ${resolvedId}`);
}
const [, type, identifier, name] = match;
if (type === 'custom') {
return resolvedId; // custom IDs don't need reverse resolution
}
// Check if identifier is already an app ID (ObjectID format)
if (/^[\da-f]{24}$/.test(identifier)) {
return resolvedId; // already unresolved format
}
// type === 'app': resolve slug to app ID
const appSlug = identifier;
const app = await this.getAppBySlug(appSlug);
return `app.${app.id}.${name}`;
}
async getAppBySlug(slug) {
const app = await this.api.get({ adminPath: `/apps/${slug}` });
if (!app) {
throw new Error(`App '${slug}' not found.\nList available content: swell inspect content`);
}
return app;
}
getAppSlugId(app) {
return app.public_id || app.private_id.replace(/^_/, '');
}
throwContentNotFound(id) {
throw new Error(`No content view found for '${id}'.\nList available content: swell inspect content`);
}
showContent(content) {
this.log(JSON.stringify(content, null, 2));
}
}