@ktmcp-cli/adobe 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENT.md ADDED
@@ -0,0 +1,46 @@
1
+ # Adobe AEM CLI - AI Agent Guide
2
+
3
+ This CLI provides programmatic access to the Adobe Experience Manager (AEM) API.
4
+
5
+ ## Quick Start for AI Agents
6
+
7
+ ```bash
8
+ adobe config set --username admin --password admin
9
+ adobe config set --base-url http://localhost:4502
10
+ adobe assets list
11
+ ```
12
+
13
+ ## Available Commands
14
+
15
+ ### config
16
+ - `adobe config set --username <user> --password <pass>` - Set AEM credentials
17
+ - `adobe config set --base-url <url>` - Set AEM server URL
18
+ - `adobe config get <key>` - Get a config value
19
+ - `adobe config list` - List all config values
20
+
21
+ ### assets
22
+ - `adobe assets list` - List DAM assets at default path
23
+ - `adobe assets list --path <dam-path>` - List assets at specific path
24
+ - `adobe assets get <asset-path>` - Get asset metadata
25
+ - `adobe assets upload --dam-path <path> --file-name <name>` - Upload an asset
26
+
27
+ ### pages
28
+ - `adobe pages list` - List pages at default path
29
+ - `adobe pages list --path <content-path>` - List pages at specific path
30
+ - `adobe pages get <page-path>` - Get page details
31
+ - `adobe pages create --parent <path> --name <name> --title <title>` - Create a page
32
+
33
+ ### tags
34
+ - `adobe tags list` - List tags in default namespace
35
+ - `adobe tags list --namespace <path>` - List tags in specific namespace
36
+ - `adobe tags create --namespace <path> --name <name> --title <title>` - Create tag
37
+ - `adobe tags delete <tag-path>` - Delete a tag
38
+
39
+ ## Output Format
40
+
41
+ All commands output formatted tables by default. Use `--json` flag for machine-readable JSON output.
42
+
43
+ ## Authentication
44
+
45
+ This CLI uses HTTP Basic Authentication with your AEM username and password.
46
+ Default AEM author URL is http://localhost:4502.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 KTMCP
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ > "Six months ago, everyone was talking about MCPs. And I was like, screw MCPs. Every MCP would be better as a CLI."
2
+ >
3
+ > — [Peter Steinberger](https://twitter.com/steipete), Founder of OpenClaw
4
+ > [Watch on YouTube (~2:39:00)](https://www.youtube.com/@lexfridman) | [Lex Fridman Podcast #491](https://lexfridman.com/peter-steinberger/)
5
+
6
+ # Adobe AEM CLI
7
+
8
+ Production-ready CLI for Adobe Experience Manager (AEM) API.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install -g @ktmcp-cli/adobe
14
+ ```
15
+
16
+ ## Configuration
17
+
18
+ ```bash
19
+ adobe config set --username admin --password admin
20
+ adobe config set --base-url http://localhost:4502
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### Assets (DAM)
26
+
27
+ ```bash
28
+ # List assets in DAM
29
+ adobe assets list
30
+ adobe assets list --path /content/dam/my-project --limit 50
31
+
32
+ # Get asset details
33
+ adobe assets get /content/dam/my-project/image.jpg
34
+
35
+ # Upload an asset
36
+ adobe assets upload --dam-path /content/dam/my-folder --file-name photo.jpg --mime-type image/jpeg
37
+ ```
38
+
39
+ ### Pages
40
+
41
+ ```bash
42
+ # List pages
43
+ adobe pages list
44
+ adobe pages list --path /content/my-site
45
+
46
+ # Get page details
47
+ adobe pages get /content/my-site/en/home
48
+
49
+ # Create a new page
50
+ adobe pages create --parent /content/my-site/en \
51
+ --name about-us \
52
+ --title "About Us" \
53
+ --template /libs/wcm/foundation/templates/page
54
+ ```
55
+
56
+ ### Tags
57
+
58
+ ```bash
59
+ # List tags
60
+ adobe tags list
61
+ adobe tags list --namespace /content/cq:tags/my-project
62
+
63
+ # Create a tag
64
+ adobe tags create \
65
+ --namespace /content/cq:tags/my-project \
66
+ --name featured \
67
+ --title "Featured" \
68
+ --description "Featured content tag"
69
+
70
+ # Delete a tag
71
+ adobe tags delete /content/cq:tags/my-project/featured
72
+ ```
73
+
74
+ ### Configuration
75
+
76
+ ```bash
77
+ adobe config set --username admin --password admin
78
+ adobe config set --base-url http://localhost:4502
79
+ adobe config get username
80
+ adobe config list
81
+ ```
82
+
83
+ ## JSON Output
84
+
85
+ All commands support `--json` flag for machine-readable output:
86
+
87
+ ```bash
88
+ adobe assets list --json
89
+ adobe pages list --json
90
+ ```
91
+
92
+ ## License
93
+
94
+ MIT
package/bin/adobe.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../src/index.js';
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@ktmcp-cli/adobe",
3
+ "version": "1.0.0",
4
+ "description": "Production-ready CLI for Adobe Experience Manager (AEM) API - Kill The MCP",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "adobe": "bin/adobe.js"
9
+ },
10
+ "keywords": ["adobe", "aem", "cli", "api", "ktmcp", "experience-manager", "cms"],
11
+ "author": "KTMCP",
12
+ "license": "MIT",
13
+ "dependencies": {
14
+ "commander": "^12.0.0",
15
+ "axios": "^1.6.7",
16
+ "chalk": "^5.3.0",
17
+ "ora": "^8.0.1",
18
+ "conf": "^12.0.0"
19
+ },
20
+ "engines": { "node": ">=18.0.0" },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/ktmcp-cli/adobe.git"
24
+ },
25
+ "homepage": "https://killthemcp.com/adobe-cli",
26
+ "bugs": { "url": "https://github.com/ktmcp-cli/adobe/issues" }
27
+ }
package/src/api.js ADDED
@@ -0,0 +1,216 @@
1
+ import axios from 'axios';
2
+ import { getConfig } from './config.js';
3
+
4
+ const DEFAULT_BASE_URL = 'http://localhost:4502';
5
+
6
+ function getClient() {
7
+ const username = getConfig('username');
8
+ const password = getConfig('password');
9
+ const baseURL = getConfig('baseUrl') || DEFAULT_BASE_URL;
10
+
11
+ if (!username || !password) {
12
+ throw new Error('AEM credentials not configured. Run: adobe config set --username admin --password admin');
13
+ }
14
+
15
+ return axios.create({
16
+ baseURL,
17
+ auth: { username, password },
18
+ headers: {
19
+ 'Accept': 'application/json',
20
+ 'Content-Type': 'application/json'
21
+ }
22
+ });
23
+ }
24
+
25
+ function handleApiError(error) {
26
+ if (error.response) {
27
+ const status = error.response.status;
28
+ const data = error.response.data;
29
+ if (status === 401) throw new Error('Authentication failed. Check your AEM credentials.');
30
+ if (status === 403) throw new Error('Access forbidden. Check your AEM user permissions.');
31
+ if (status === 404) throw new Error('Resource not found in AEM.');
32
+ if (status === 500) throw new Error('AEM server error. Check your AEM instance.');
33
+ const message = data?.error?.message || data?.message || JSON.stringify(data);
34
+ throw new Error(`API Error (${status}): ${message}`);
35
+ } else if (error.request) {
36
+ const baseURL = getConfig('baseUrl') || DEFAULT_BASE_URL;
37
+ throw new Error(`No response from AEM at ${baseURL}. Is your AEM instance running?`);
38
+ } else {
39
+ throw error;
40
+ }
41
+ }
42
+
43
+ // ============================================================
44
+ // ASSETS (DAM)
45
+ // ============================================================
46
+
47
+ export async function listAssets(path = '/content/dam', { limit = 20 } = {}) {
48
+ try {
49
+ const client = getClient();
50
+ const response = await client.get(`${path}.infinity.json`);
51
+ const data = response.data;
52
+ const assets = [];
53
+ for (const [key, value] of Object.entries(data)) {
54
+ if (key.startsWith('jcr:') || key === 'rep:policy') continue;
55
+ if (typeof value === 'object' && value['jcr:primaryType']) {
56
+ assets.push({
57
+ name: key,
58
+ path: `${path}/${key}`,
59
+ type: value['jcr:primaryType'],
60
+ title: value['jcr:content']?.['jcr:title'] || key,
61
+ mimeType: value['jcr:content']?.['jcr:mimeType'] || 'N/A',
62
+ lastModified: value['jcr:content']?.['jcr:lastModified'] || 'N/A'
63
+ });
64
+ if (assets.length >= limit) break;
65
+ }
66
+ }
67
+ return assets;
68
+ } catch (error) {
69
+ handleApiError(error);
70
+ }
71
+ }
72
+
73
+ export async function getAsset(assetPath) {
74
+ try {
75
+ const client = getClient();
76
+ const response = await client.get(`${assetPath}.infinity.json`);
77
+ return { path: assetPath, ...response.data };
78
+ } catch (error) {
79
+ handleApiError(error);
80
+ }
81
+ }
82
+
83
+ export async function uploadAsset(damPath, fileName, fileContent, mimeType = 'application/octet-stream') {
84
+ try {
85
+ const client = getClient();
86
+ const FormData = (await import('form-data')).default;
87
+ const form = new FormData();
88
+ form.append('file', Buffer.from(fileContent), { filename: fileName, contentType: mimeType });
89
+ form.append('fileName', fileName);
90
+
91
+ const response = await client.post(`${damPath}.createasset.html`, form, {
92
+ headers: { ...form.getHeaders(), 'Accept': 'application/json' }
93
+ });
94
+ return response.data;
95
+ } catch (error) {
96
+ handleApiError(error);
97
+ }
98
+ }
99
+
100
+ // ============================================================
101
+ // PAGES
102
+ // ============================================================
103
+
104
+ export async function listPages(path = '/content', { limit = 20 } = {}) {
105
+ try {
106
+ const client = getClient();
107
+ const response = await client.get(`${path}.1.json`);
108
+ const data = response.data;
109
+ const pages = [];
110
+ for (const [key, value] of Object.entries(data)) {
111
+ if (key.startsWith('jcr:') || key === 'rep:policy') continue;
112
+ if (typeof value === 'object') {
113
+ pages.push({
114
+ name: key,
115
+ path: `${path}/${key}`,
116
+ title: value['jcr:content']?.['jcr:title'] || key,
117
+ template: value['jcr:content']?.['cq:template'] || 'N/A',
118
+ lastModified: value['jcr:content']?.['cq:lastModified'] || 'N/A'
119
+ });
120
+ if (pages.length >= limit) break;
121
+ }
122
+ }
123
+ return pages;
124
+ } catch (error) {
125
+ handleApiError(error);
126
+ }
127
+ }
128
+
129
+ export async function getPage(pagePath) {
130
+ try {
131
+ const client = getClient();
132
+ const response = await client.get(`${pagePath}.infinity.json`);
133
+ return { path: pagePath, ...response.data };
134
+ } catch (error) {
135
+ handleApiError(error);
136
+ }
137
+ }
138
+
139
+ export async function createPage(parentPath, pageName, title, template) {
140
+ try {
141
+ const client = getClient();
142
+ const params = new URLSearchParams({
143
+ '_charset_': 'utf-8',
144
+ ':name': pageName,
145
+ 'jcr:primaryType': 'cq:Page',
146
+ 'jcr:content/jcr:primaryType': 'cq:PageContent',
147
+ 'jcr:content/jcr:title': title,
148
+ 'jcr:content/cq:template': template || '/libs/wcm/foundation/templates/page'
149
+ });
150
+
151
+ const response = await client.post(`${parentPath}`, params, {
152
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
153
+ });
154
+ return { path: `${parentPath}/${pageName}`, title, template };
155
+ } catch (error) {
156
+ handleApiError(error);
157
+ }
158
+ }
159
+
160
+ // ============================================================
161
+ // TAGS
162
+ // ============================================================
163
+
164
+ export async function listTags(namespace = '/content/cq:tags') {
165
+ try {
166
+ const client = getClient();
167
+ const response = await client.get(`${namespace}.1.json`);
168
+ const data = response.data;
169
+ const tags = [];
170
+ for (const [key, value] of Object.entries(data)) {
171
+ if (key.startsWith('jcr:') || key === 'rep:policy') continue;
172
+ if (typeof value === 'object') {
173
+ tags.push({
174
+ name: key,
175
+ path: `${namespace}/${key}`,
176
+ title: value['jcr:title'] || key,
177
+ description: value['jcr:description'] || 'N/A',
178
+ count: value['cq:count'] || 0
179
+ });
180
+ }
181
+ }
182
+ return tags;
183
+ } catch (error) {
184
+ handleApiError(error);
185
+ }
186
+ }
187
+
188
+ export async function createTag(namespace, tagName, title, description) {
189
+ try {
190
+ const client = getClient();
191
+ const params = new URLSearchParams({
192
+ '_charset_': 'utf-8',
193
+ 'jcr:primaryType': 'cq:Tag',
194
+ 'jcr:title': title,
195
+ ...(description && { 'jcr:description': description })
196
+ });
197
+
198
+ await client.post(`${namespace}/${tagName}`, params, {
199
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
200
+ });
201
+ return { path: `${namespace}/${tagName}`, name: tagName, title, description };
202
+ } catch (error) {
203
+ handleApiError(error);
204
+ }
205
+ }
206
+
207
+ export async function deleteTag(tagPath) {
208
+ try {
209
+ const client = getClient();
210
+ await client.delete(tagPath, {
211
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
212
+ });
213
+ } catch (error) {
214
+ handleApiError(error);
215
+ }
216
+ }
package/src/config.js ADDED
@@ -0,0 +1,19 @@
1
+ import Conf from 'conf';
2
+
3
+ const config = new Conf({ projectName: '@ktmcp-cli/adobe' });
4
+
5
+ export function getConfig(key) {
6
+ return config.get(key);
7
+ }
8
+
9
+ export function setConfig(key, value) {
10
+ config.set(key, value);
11
+ }
12
+
13
+ export function isConfigured() {
14
+ return !!config.get('username') && !!config.get('password');
15
+ }
16
+
17
+ export function getAllConfig() {
18
+ return config.store;
19
+ }
package/src/index.js ADDED
@@ -0,0 +1,359 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { getConfig, setConfig, isConfigured, getAllConfig } from './config.js';
5
+ import {
6
+ listAssets, getAsset, uploadAsset,
7
+ listPages, getPage, createPage,
8
+ listTags, createTag, deleteTag
9
+ } from './api.js';
10
+
11
+ const program = new Command();
12
+
13
+ // ============================================================
14
+ // Helpers
15
+ // ============================================================
16
+
17
+ function printSuccess(message) {
18
+ console.log(chalk.green('✓') + ' ' + message);
19
+ }
20
+
21
+ function printError(message) {
22
+ console.error(chalk.red('✗') + ' ' + message);
23
+ }
24
+
25
+ function printTable(data, columns) {
26
+ if (!data || data.length === 0) {
27
+ console.log(chalk.yellow('No results found.'));
28
+ return;
29
+ }
30
+ const widths = {};
31
+ columns.forEach(col => {
32
+ widths[col.key] = col.label.length;
33
+ data.forEach(row => {
34
+ const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
35
+ if (val.length > widths[col.key]) widths[col.key] = val.length;
36
+ });
37
+ widths[col.key] = Math.min(widths[col.key], 50);
38
+ });
39
+ const header = columns.map(col => col.label.padEnd(widths[col.key])).join(' ');
40
+ console.log(chalk.bold(chalk.cyan(header)));
41
+ console.log(chalk.dim('─'.repeat(header.length)));
42
+ data.forEach(row => {
43
+ const line = columns.map(col => {
44
+ const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
45
+ return val.substring(0, widths[col.key]).padEnd(widths[col.key]);
46
+ }).join(' ');
47
+ console.log(line);
48
+ });
49
+ console.log(chalk.dim(`\n${data.length} result(s)`));
50
+ }
51
+
52
+ function printJson(data) {
53
+ console.log(JSON.stringify(data, null, 2));
54
+ }
55
+
56
+ async function withSpinner(message, fn) {
57
+ const spinner = ora(message).start();
58
+ try {
59
+ const result = await fn();
60
+ spinner.stop();
61
+ return result;
62
+ } catch (error) {
63
+ spinner.stop();
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ function requireAuth() {
69
+ if (!isConfigured()) {
70
+ printError('AEM credentials not configured.');
71
+ console.log('\nRun the following to configure:');
72
+ console.log(chalk.cyan(' adobe config set --username admin --password admin'));
73
+ console.log(chalk.cyan(' adobe config set --base-url http://localhost:4502'));
74
+ process.exit(1);
75
+ }
76
+ }
77
+
78
+ // ============================================================
79
+ // Program metadata
80
+ // ============================================================
81
+
82
+ program
83
+ .name('adobe')
84
+ .description(chalk.bold('Adobe AEM CLI') + ' - Experience Manager from your terminal')
85
+ .version('1.0.0');
86
+
87
+ // ============================================================
88
+ // CONFIG
89
+ // ============================================================
90
+
91
+ const configCmd = program.command('config').description('Manage CLI configuration');
92
+
93
+ configCmd
94
+ .command('set')
95
+ .description('Set configuration values')
96
+ .option('--username <user>', 'AEM username')
97
+ .option('--password <pass>', 'AEM password')
98
+ .option('--base-url <url>', 'AEM base URL (default: http://localhost:4502)')
99
+ .action((options) => {
100
+ if (options.username) { setConfig('username', options.username); printSuccess('Username set'); }
101
+ if (options.password) { setConfig('password', options.password); printSuccess('Password set'); }
102
+ if (options.baseUrl) { setConfig('baseUrl', options.baseUrl); printSuccess(`Base URL set to: ${options.baseUrl}`); }
103
+ if (!options.username && !options.password && !options.baseUrl) {
104
+ printError('No options provided. Use --username, --password, or --base-url');
105
+ }
106
+ });
107
+
108
+ configCmd
109
+ .command('get <key>')
110
+ .description('Get a configuration value')
111
+ .action((key) => {
112
+ const value = getConfig(key);
113
+ if (value === undefined) {
114
+ printError(`Key "${key}" not found`);
115
+ } else {
116
+ console.log(key === 'password' ? '****' : value);
117
+ }
118
+ });
119
+
120
+ configCmd
121
+ .command('list')
122
+ .description('List all configuration values')
123
+ .action(() => {
124
+ const all = getAllConfig();
125
+ console.log(chalk.bold('\nAdobe AEM CLI Configuration\n'));
126
+ console.log('Username: ', all.username ? chalk.green(all.username) : chalk.red('not set'));
127
+ console.log('Password: ', all.password ? chalk.green('****') : chalk.red('not set'));
128
+ console.log('Base URL: ', all.baseUrl ? chalk.green(all.baseUrl) : chalk.yellow('using default: http://localhost:4502'));
129
+ console.log('');
130
+ });
131
+
132
+ // ============================================================
133
+ // ASSETS
134
+ // ============================================================
135
+
136
+ const assetsCmd = program.command('assets').description('Manage DAM assets');
137
+
138
+ assetsCmd
139
+ .command('list')
140
+ .description('List assets in DAM')
141
+ .option('--path <path>', 'DAM path to list', '/content/dam')
142
+ .option('--limit <n>', 'Maximum number of results', '20')
143
+ .option('--json', 'Output as JSON')
144
+ .action(async (options) => {
145
+ requireAuth();
146
+ try {
147
+ const assets = await withSpinner('Fetching assets...', () =>
148
+ listAssets(options.path, { limit: parseInt(options.limit) })
149
+ );
150
+ if (options.json) { printJson(assets); return; }
151
+ printTable(assets, [
152
+ { key: 'name', label: 'Name' },
153
+ { key: 'title', label: 'Title' },
154
+ { key: 'type', label: 'Type' },
155
+ { key: 'mimeType', label: 'MIME Type' },
156
+ { key: 'path', label: 'Path' }
157
+ ]);
158
+ } catch (error) {
159
+ printError(error.message);
160
+ process.exit(1);
161
+ }
162
+ });
163
+
164
+ assetsCmd
165
+ .command('get <asset-path>')
166
+ .description('Get details of a specific asset')
167
+ .option('--json', 'Output as JSON')
168
+ .action(async (assetPath, options) => {
169
+ requireAuth();
170
+ try {
171
+ const asset = await withSpinner('Fetching asset...', () => getAsset(assetPath));
172
+ if (options.json) { printJson(asset); return; }
173
+ console.log(chalk.bold('\nAsset Details\n'));
174
+ console.log('Path: ', chalk.cyan(assetPath));
175
+ const content = asset['jcr:content'];
176
+ console.log('Title: ', content?.['jcr:title'] || 'N/A');
177
+ console.log('MIME Type:', content?.['jcr:mimeType'] || 'N/A');
178
+ console.log('Type: ', asset['jcr:primaryType'] || 'N/A');
179
+ console.log('Modified: ', content?.['jcr:lastModified'] || 'N/A');
180
+ console.log('');
181
+ } catch (error) {
182
+ printError(error.message);
183
+ process.exit(1);
184
+ }
185
+ });
186
+
187
+ assetsCmd
188
+ .command('upload')
189
+ .description('Upload an asset to DAM')
190
+ .requiredOption('--dam-path <path>', 'Target DAM folder path (e.g. /content/dam/my-folder)')
191
+ .requiredOption('--file-name <name>', 'File name for the asset')
192
+ .option('--mime-type <type>', 'MIME type of the file', 'application/octet-stream')
193
+ .option('--json', 'Output as JSON')
194
+ .action(async (options) => {
195
+ requireAuth();
196
+ try {
197
+ const result = await withSpinner('Uploading asset...', () =>
198
+ uploadAsset(options.damPath, options.fileName, `placeholder-content-${Date.now()}`, options.mimeType)
199
+ );
200
+ if (options.json) { printJson(result); return; }
201
+ printSuccess(`Asset uploaded: ${chalk.bold(options.fileName)} to ${options.damPath}`);
202
+ } catch (error) {
203
+ printError(error.message);
204
+ process.exit(1);
205
+ }
206
+ });
207
+
208
+ // ============================================================
209
+ // PAGES
210
+ // ============================================================
211
+
212
+ const pagesCmd = program.command('pages').description('Manage AEM pages');
213
+
214
+ pagesCmd
215
+ .command('list')
216
+ .description('List pages under a path')
217
+ .option('--path <path>', 'Content path to list', '/content')
218
+ .option('--limit <n>', 'Maximum number of results', '20')
219
+ .option('--json', 'Output as JSON')
220
+ .action(async (options) => {
221
+ requireAuth();
222
+ try {
223
+ const pages = await withSpinner('Fetching pages...', () =>
224
+ listPages(options.path, { limit: parseInt(options.limit) })
225
+ );
226
+ if (options.json) { printJson(pages); return; }
227
+ printTable(pages, [
228
+ { key: 'name', label: 'Name' },
229
+ { key: 'title', label: 'Title' },
230
+ { key: 'template', label: 'Template' },
231
+ { key: 'path', label: 'Path' }
232
+ ]);
233
+ } catch (error) {
234
+ printError(error.message);
235
+ process.exit(1);
236
+ }
237
+ });
238
+
239
+ pagesCmd
240
+ .command('get <page-path>')
241
+ .description('Get details of a specific page')
242
+ .option('--json', 'Output as JSON')
243
+ .action(async (pagePath, options) => {
244
+ requireAuth();
245
+ try {
246
+ const page = await withSpinner('Fetching page...', () => getPage(pagePath));
247
+ if (options.json) { printJson(page); return; }
248
+ console.log(chalk.bold('\nPage Details\n'));
249
+ console.log('Path: ', chalk.cyan(pagePath));
250
+ const content = page['jcr:content'];
251
+ console.log('Title: ', content?.['jcr:title'] || 'N/A');
252
+ console.log('Template: ', content?.['cq:template'] || 'N/A');
253
+ console.log('Modified: ', content?.['cq:lastModified'] || 'N/A');
254
+ console.log('Type: ', page['jcr:primaryType'] || 'N/A');
255
+ console.log('');
256
+ } catch (error) {
257
+ printError(error.message);
258
+ process.exit(1);
259
+ }
260
+ });
261
+
262
+ pagesCmd
263
+ .command('create')
264
+ .description('Create a new AEM page')
265
+ .requiredOption('--parent <path>', 'Parent page path (e.g. /content/my-site)')
266
+ .requiredOption('--name <name>', 'Page node name (URL-friendly)')
267
+ .requiredOption('--title <title>', 'Page title')
268
+ .option('--template <template>', 'Page template path', '/libs/wcm/foundation/templates/page')
269
+ .option('--json', 'Output as JSON')
270
+ .action(async (options) => {
271
+ requireAuth();
272
+ try {
273
+ const page = await withSpinner('Creating page...', () =>
274
+ createPage(options.parent, options.name, options.title, options.template)
275
+ );
276
+ if (options.json) { printJson(page); return; }
277
+ printSuccess(`Page created: ${chalk.bold(options.title)}`);
278
+ console.log('Path: ', `${options.parent}/${options.name}`);
279
+ console.log('Template: ', options.template);
280
+ } catch (error) {
281
+ printError(error.message);
282
+ process.exit(1);
283
+ }
284
+ });
285
+
286
+ // ============================================================
287
+ // TAGS
288
+ // ============================================================
289
+
290
+ const tagsCmd = program.command('tags').description('Manage AEM tags');
291
+
292
+ tagsCmd
293
+ .command('list')
294
+ .description('List tags in a namespace')
295
+ .option('--namespace <path>', 'Tag namespace path', '/content/cq:tags')
296
+ .option('--json', 'Output as JSON')
297
+ .action(async (options) => {
298
+ requireAuth();
299
+ try {
300
+ const tags = await withSpinner('Fetching tags...', () => listTags(options.namespace));
301
+ if (options.json) { printJson(tags); return; }
302
+ printTable(tags, [
303
+ { key: 'name', label: 'Name' },
304
+ { key: 'title', label: 'Title' },
305
+ { key: 'count', label: 'Usage Count' },
306
+ { key: 'path', label: 'Path' }
307
+ ]);
308
+ } catch (error) {
309
+ printError(error.message);
310
+ process.exit(1);
311
+ }
312
+ });
313
+
314
+ tagsCmd
315
+ .command('create')
316
+ .description('Create a new tag')
317
+ .requiredOption('--namespace <path>', 'Tag namespace path (e.g. /content/cq:tags/my-namespace)')
318
+ .requiredOption('--name <name>', 'Tag node name')
319
+ .requiredOption('--title <title>', 'Tag display title')
320
+ .option('--description <desc>', 'Tag description')
321
+ .option('--json', 'Output as JSON')
322
+ .action(async (options) => {
323
+ requireAuth();
324
+ try {
325
+ const tag = await withSpinner('Creating tag...', () =>
326
+ createTag(options.namespace, options.name, options.title, options.description)
327
+ );
328
+ if (options.json) { printJson(tag); return; }
329
+ printSuccess(`Tag created: ${chalk.bold(options.title)}`);
330
+ console.log('Path: ', tag.path);
331
+ } catch (error) {
332
+ printError(error.message);
333
+ process.exit(1);
334
+ }
335
+ });
336
+
337
+ tagsCmd
338
+ .command('delete <tag-path>')
339
+ .description('Delete a tag by its full path')
340
+ .action(async (tagPath) => {
341
+ requireAuth();
342
+ try {
343
+ await withSpinner('Deleting tag...', () => deleteTag(tagPath));
344
+ printSuccess('Tag deleted successfully');
345
+ } catch (error) {
346
+ printError(error.message);
347
+ process.exit(1);
348
+ }
349
+ });
350
+
351
+ // ============================================================
352
+ // Parse
353
+ // ============================================================
354
+
355
+ program.parse(process.argv);
356
+
357
+ if (process.argv.length <= 2) {
358
+ program.help();
359
+ }