@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 +46 -0
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/bin/adobe.js +2 -0
- package/package.json +27 -0
- package/src/api.js +216 -0
- package/src/config.js +19 -0
- package/src/index.js +359 -0
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
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
|
+
}
|