@adobe/aem-cli 16.19.13 → 16.19.14

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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [16.19.14](https://github.com/adobe/helix-cli/compare/v16.19.13...v16.19.14) (2026-06-04)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **content:** use org/site terminology and centralize config reading ([#2737](https://github.com/adobe/helix-cli/issues/2737)) ([bb0d357](https://github.com/adobe/helix-cli/commit/bb0d35744b41487245c269de6000a0d9a23d5bdb))
7
+
1
8
  ## [16.19.13](https://github.com/adobe/helix-cli/compare/v16.19.12...v16.19.13) (2026-06-03)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/aem-cli",
3
- "version": "16.19.13",
3
+ "version": "16.19.14",
4
4
  "description": "AEM CLI",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -101,14 +101,14 @@ export default class CloneCommand {
101
101
  throw new Error('Clone root path was not set (internal error).');
102
102
  }
103
103
 
104
- // 1. Resolve owner/repo from git remote
104
+ // 1. Resolve org/site from git remote (GitHub owner/repo → da.live org/site)
105
105
  const originUrl = await GitUtils.getOriginURL(this._dir);
106
106
  if (!originUrl) {
107
107
  throw new Error('No git remote found. Run `aem content clone` inside an AEM project directory.');
108
108
  }
109
- const owner = originUrl.owner.toLowerCase();
110
- const repo = originUrl.repo.toLowerCase();
111
- log.info(`Cloning content from da.live: ${owner}/${repo}${this._rootPath === '/' ? '' : ` @ ${this._rootPath}`}`);
109
+ const org = originUrl.owner.toLowerCase();
110
+ const site = originUrl.repo.toLowerCase();
111
+ log.info(`Cloning content from da.live: ${org}/${site}${this._rootPath === '/' ? '' : ` @ ${this._rootPath}`}`);
112
112
 
113
113
  // 2. Ensure target path is available (do not create content/ until after file count is known)
114
114
  const contentDir = path.resolve(this._dir, CONTENT_DIR);
@@ -126,7 +126,7 @@ export default class CloneCommand {
126
126
  const client = new DaClient(token);
127
127
  log.info('Fetching file list...');
128
128
  const showDiscoveryProgress = process.stdout.isTTY;
129
- const files = await client.listAll(owner, repo, this._rootPath, showDiscoveryProgress
129
+ const files = await client.listAll(org, site, this._rootPath, showDiscoveryProgress
130
130
  ? (n) => {
131
131
  process.stdout.write(`\r ${n} file(s) discovered so far...`);
132
132
  }
@@ -148,15 +148,15 @@ export default class CloneCommand {
148
148
  const downloadResults = await processQueue(
149
149
  files,
150
150
  async (file) => {
151
- const prefix = `/${owner}/${repo}`;
151
+ const prefix = `/${org}/${site}`;
152
152
  if (!file.path.startsWith(prefix)) {
153
- log.warn(` skip (unexpected path, missing owner/repo prefix): ${file.path}`);
153
+ log.warn(` skip (unexpected path, missing org/site prefix): ${file.path}`);
154
154
  return { status: 404 };
155
155
  }
156
156
  const daPath = file.path.slice(prefix.length) || '/';
157
157
  const localPath = path.join(contentDir, ...daPath.split('/').filter(Boolean));
158
158
  try {
159
- const res = await client.getSource(owner, repo, daPath);
159
+ const res = await client.getSource(org, site, daPath);
160
160
  if (!res) {
161
161
  log.warn(` skip (not found): ${daPath}`);
162
162
  return { status: 404 };
@@ -195,7 +195,7 @@ export default class CloneCommand {
195
195
  await git.commit({
196
196
  fs,
197
197
  dir: contentDir,
198
- message: `clone: ${owner}/${repo}${this._rootPath === '/' ? '' : ` (${this._rootPath})`}`,
198
+ message: `clone: ${org}/${site}${this._rootPath === '/' ? '' : ` (${this._rootPath})`}`,
199
199
  author: GIT_AUTHOR,
200
200
  });
201
201
  const headOid = await git.resolveRef({ fs, dir: contentDir, ref: 'HEAD' });
@@ -203,8 +203,8 @@ export default class CloneCommand {
203
203
 
204
204
  // 8. Write config (not tracked by git)
205
205
  await fse.writeJson(path.join(contentDir, CONFIG_FILE), {
206
- owner,
207
- repo,
206
+ org,
207
+ site,
208
208
  rootPath: this._rootPath,
209
209
  }, { spaces: 2 });
210
210
 
@@ -10,6 +10,9 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
+ import path from 'path';
14
+ import fse from 'fs-extra';
15
+
13
16
  export const CONTENT_DIR = 'content';
14
17
  export const CONFIG_FILE = '.da-config.json';
15
18
  export const GIT_AUTHOR = { name: 'aem-cli', email: 'aem-cli@adobe.com' };
@@ -22,6 +25,28 @@ export const LARGE_CLONE_FILE_THRESHOLD = 10000;
22
25
  */
23
26
  export const CONTENT_IO_CONCURRENCY = 10;
24
27
 
28
+ /**
29
+ * Reads and normalizes the content config from a content directory.
30
+ * Accepts both current keys (org/site) and legacy keys (owner/repo) written
31
+ * by earlier versions of clone. org/site take priority when both are present.
32
+ * @param {string} contentDir - absolute path to the content/ directory
33
+ * @returns {Promise<{org: string, site: string, rootPath: string|undefined}>}
34
+ * @throws if the config file is missing or org/site cannot be resolved
35
+ */
36
+ export async function readContentConfig(contentDir) {
37
+ const configPath = path.join(contentDir, CONFIG_FILE);
38
+ if (!await fse.pathExists(configPath)) {
39
+ throw new Error(`No config found at ${configPath}. Run 'aem content clone' first.`);
40
+ }
41
+ const raw = await fse.readJson(configPath);
42
+ const org = (raw.org || raw.owner || '').toLowerCase();
43
+ const site = (raw.site || raw.repo || '').toLowerCase();
44
+ if (!org || !site) {
45
+ throw new Error(`Invalid config: org and site are required in ${configPath}.`);
46
+ }
47
+ return { org, site, rootPath: raw.rootPath };
48
+ }
49
+
25
50
  /**
26
51
  * Normalizes a da.live path: leading slash, no trailing slash except root.
27
52
  * @param {string} input
@@ -39,12 +39,12 @@ export class DaClient {
39
39
  /**
40
40
  * Lists the contents of a directory, following {@link LIST_CONTINUATION_HEADER} until complete.
41
41
  * @param {string} org
42
- * @param {string} repo
42
+ * @param {string} site
43
43
  * @param {string} daPath - path starting with /
44
44
  * @returns {Promise<Array<{path, name, ext?, lastModified}>>}
45
45
  */
46
- async list(org, repo, daPath) {
47
- const url = `${DA_ADMIN}/list/${org}/${repo}${daPath}`;
46
+ async list(org, site, daPath) {
47
+ const url = `${DA_ADMIN}/list/${org}/${site}${daPath}`;
48
48
  const aggregated = [];
49
49
  let continuation = null;
50
50
 
@@ -81,13 +81,13 @@ export class DaClient {
81
81
  /**
82
82
  * Recursively lists all files under a path using a non-recursive queue-based approach.
83
83
  * @param {string} org
84
- * @param {string} repo
84
+ * @param {string} site
85
85
  * @param {string} [daPath='/']
86
86
  * @param {(discoveredCount: number) => void} [onDiscovered] - cumulative file count per discovery
87
87
  * @returns {Promise<Array<{path, name, ext, lastModified}>>}
88
88
  */
89
- async listAll(org, repo, daPath = '/', onDiscovered = undefined) {
90
- const prefix = `/${org}/${repo}`;
89
+ async listAll(org, site, daPath = '/', onDiscovered = undefined) {
90
+ const prefix = `/${org}/${site}`;
91
91
  const files = [];
92
92
  let dirsToProcess = [daPath];
93
93
 
@@ -97,7 +97,7 @@ export class DaClient {
97
97
  await processQueue(
98
98
  dirsToProcess,
99
99
  async (currentPath) => {
100
- const items = await this.list(org, repo, currentPath);
100
+ const items = await this.list(org, site, currentPath);
101
101
  for (const item of items) {
102
102
  if (item.ext !== undefined) {
103
103
  files.push(item);
@@ -119,10 +119,13 @@ export class DaClient {
119
119
 
120
120
  /**
121
121
  * Fetches the raw content of a file.
122
+ * @param {string} org
123
+ * @param {string} site
124
+ * @param {string} daPath
122
125
  * @returns {Promise<Response|null>}
123
126
  */
124
- async getSource(org, repo, daPath) {
125
- const url = `${DA_ADMIN}/source/${org}/${repo}${daPath}`;
127
+ async getSource(org, site, daPath) {
128
+ const url = `${DA_ADMIN}/source/${org}/${site}${daPath}`;
126
129
  const res = await this.fetch(url, { headers: this.authHeader });
127
130
  if (res.status === 401) {
128
131
  throw new Error('Unauthorized: invalid or missing token');
@@ -139,14 +142,14 @@ export class DaClient {
139
142
  /**
140
143
  * Uploads a file via PUT.
141
144
  * @param {string} org
142
- * @param {string} repo
145
+ * @param {string} site
143
146
  * @param {string} daPath
144
147
  * @param {Buffer} buffer
145
148
  * @param {string} contentType
146
149
  * @returns {Promise<object>} API response body
147
150
  */
148
- async putSource(org, repo, daPath, buffer, contentType) {
149
- const url = `${DA_ADMIN}/source/${org}/${repo}${daPath}`;
151
+ async putSource(org, site, daPath, buffer, contentType) {
152
+ const url = `${DA_ADMIN}/source/${org}/${site}${daPath}`;
150
153
  const res = await this.fetch(url, {
151
154
  method: 'PUT',
152
155
  headers: { ...this.authHeader, 'Content-Type': contentType },
@@ -165,8 +168,8 @@ export class DaClient {
165
168
  * Deletes a file or folder. Idempotent: 404 is treated as success.
166
169
  * Throws on transport or server errors so callers don't silently treat them as success.
167
170
  */
168
- async deleteSource(org, repo, daPath) {
169
- const url = `${DA_ADMIN}/source/${org}/${repo}${daPath}`;
171
+ async deleteSource(org, site, daPath) {
172
+ const url = `${DA_ADMIN}/source/${org}/${site}${daPath}`;
170
173
  const res = await this.fetch(url, {
171
174
  method: 'DELETE',
172
175
  headers: this.authHeader,
@@ -183,12 +186,12 @@ export class DaClient {
183
186
  /**
184
187
  * Returns the current lastModified for a file via a HEAD request.
185
188
  * @param {string} org
186
- * @param {string} repo
189
+ * @param {string} site
187
190
  * @param {string} daPath - e.g. /blog/post.html
188
191
  * @returns {Promise<number|null>}
189
192
  */
190
- async getRemoteLastModified(org, repo, daPath) {
191
- const url = `${DA_ADMIN}/source/${org}/${repo}${daPath}`;
193
+ async getRemoteLastModified(org, site, daPath) {
194
+ const url = `${DA_ADMIN}/source/${org}/${site}${daPath}`;
192
195
  const res = await this.fetch(url, { method: 'HEAD', headers: this.authHeader });
193
196
  if (res.status === 401) {
194
197
  throw new Error('Unauthorized: invalid or missing token');
@@ -17,7 +17,7 @@ import { createTwoFilesPatch } from 'diff';
17
17
  import chalk from 'chalk-template';
18
18
  import { DaClient } from './da-api.js';
19
19
  import { getValidToken } from './da-auth.js';
20
- import { CONTENT_DIR, CONFIG_FILE } from './content-shared.js';
20
+ import { CONTENT_DIR, readContentConfig } from './content-shared.js';
21
21
 
22
22
  function printPatch(patch) {
23
23
  for (const line of patch.split('\n')) {
@@ -60,12 +60,7 @@ export default class DiffCommand {
60
60
  async run() {
61
61
  const { log } = this;
62
62
  const contentDir = path.resolve(this._dir, CONTENT_DIR);
63
- const configPath = path.join(contentDir, CONFIG_FILE);
64
-
65
- if (!await fse.pathExists(configPath)) {
66
- throw new Error('No config found. Run \'aem content clone\' first.');
67
- }
68
- const { org, repo } = await fse.readJson(configPath);
63
+ const { org, site } = await readContentConfig(contentDir);
69
64
 
70
65
  const matrix = await git.statusMatrix({ fs, dir: contentDir });
71
66
  let changedPaths = matrix
@@ -98,7 +93,7 @@ export default class DiffCommand {
98
93
  ? await fse.readFile(localPath)
99
94
  : Buffer.from('');
100
95
 
101
- const remoteRes = await client.getSource(org, repo, daPath);
96
+ const remoteRes = await client.getSource(org, site, daPath);
102
97
 
103
98
  const localText = localBuffer.toString('utf-8');
104
99
  const remoteText = remoteRes ? await remoteRes.text() : '';
@@ -16,7 +16,7 @@ import git from 'isomorphic-git';
16
16
  import { diff3Merge } from 'node-diff3'; // eslint-disable-line import/no-unresolved
17
17
  import { DaClient } from './da-api.js';
18
18
  import { getValidToken } from './da-auth.js';
19
- import { CONTENT_DIR, CONFIG_FILE } from './content-shared.js';
19
+ import { CONTENT_DIR, readContentConfig } from './content-shared.js';
20
20
 
21
21
  /**
22
22
  * 3-way merge using diff3 algorithm.
@@ -69,12 +69,7 @@ export default class MergeCommand {
69
69
  async run() {
70
70
  const { log } = this;
71
71
  const contentDir = path.resolve(this._dir, CONTENT_DIR);
72
- const configPath = path.join(contentDir, CONFIG_FILE);
73
-
74
- if (!await fse.pathExists(configPath)) {
75
- throw new Error('No config found. Run \'aem content clone\' first.');
76
- }
77
- const { org, repo } = await fse.readJson(configPath);
72
+ const { org, site } = await readContentConfig(contentDir);
78
73
 
79
74
  // Find locally changed files via git
80
75
  const matrix = await git.statusMatrix({ fs, dir: contentDir });
@@ -112,7 +107,7 @@ export default class MergeCommand {
112
107
  // eslint-disable-next-line no-await-in-loop
113
108
  const [localBuffer, remoteRes, blobResult] = await Promise.all([
114
109
  fse.readFile(localPath),
115
- client.getSource(org, repo, daPath),
110
+ client.getSource(org, site, daPath),
116
111
  git.readBlob({
117
112
  fs, dir: contentDir, oid: headCommit.oid, filepath: relPath,
118
113
  })
@@ -18,8 +18,8 @@ import { DaClient, getContentType } from './da-api.js';
18
18
  import { getValidToken } from './da-auth.js';
19
19
  import {
20
20
  CONTENT_DIR,
21
- CONFIG_FILE,
22
21
  CONTENT_IO_CONCURRENCY,
22
+ readContentConfig,
23
23
  } from './content-shared.js';
24
24
  import {
25
25
  resolveSyncedOid,
@@ -67,19 +67,19 @@ export default class PushCommand {
67
67
  * Checks for conflicts between local changes and remote modifications.
68
68
  * @param {DaClient} client
69
69
  * @param {string} org
70
- * @param {string} repo
70
+ * @param {string} site
71
71
  * @param {string[]} modified
72
72
  * @param {string[]} deleted
73
73
  * @param {number} lastSyncTime
74
74
  * @returns {Promise<boolean>} true if the push should be aborted
75
75
  */
76
- async _checkConflicts(client, org, repo, modified, deleted, lastSyncTime) {
76
+ async _checkConflicts(client, org, site, modified, deleted, lastSyncTime) {
77
77
  const { log } = this;
78
78
  const conflicts = [];
79
79
 
80
80
  for (const daPath of [...modified, ...deleted]) {
81
81
  // eslint-disable-next-line no-await-in-loop
82
- const remoteLastModified = await client.getRemoteLastModified(org, repo, daPath);
82
+ const remoteLastModified = await client.getRemoteLastModified(org, site, daPath);
83
83
  if (remoteLastModified != null && remoteLastModified > lastSyncTime) {
84
84
  conflicts.push({ daPath, remoteDate: new Date(remoteLastModified).toLocaleString() });
85
85
  }
@@ -105,12 +105,12 @@ export default class PushCommand {
105
105
  * Uploads added and modified files to da.live.
106
106
  * @param {DaClient} client
107
107
  * @param {string} org
108
- * @param {string} repo
108
+ * @param {string} site
109
109
  * @param {string} contentDir
110
110
  * @param {string[]} targets
111
111
  * @returns {Promise<{ pushed: number, errors: number }>}
112
112
  */
113
- async _uploadFiles(client, org, repo, contentDir, targets) {
113
+ async _uploadFiles(client, org, site, contentDir, targets) {
114
114
  const { log } = this;
115
115
  let pushed = 0;
116
116
  let errors = 0;
@@ -123,7 +123,7 @@ export default class PushCommand {
123
123
  const ext = daPath.split('.').pop();
124
124
  try {
125
125
  const buffer = await fse.readFile(localPath);
126
- await client.putSource(org, repo, daPath, buffer, getContentType(ext));
126
+ await client.putSource(org, site, daPath, buffer, getContentType(ext));
127
127
  log.info(` ✓ ${daPath}`);
128
128
  return { ok: true };
129
129
  } catch (err) {
@@ -148,11 +148,11 @@ export default class PushCommand {
148
148
  * Deletes files from da.live.
149
149
  * @param {DaClient} client
150
150
  * @param {string} org
151
- * @param {string} repo
151
+ * @param {string} site
152
152
  * @param {string[]} deleted
153
153
  * @returns {Promise<{ pushed: number, errors: number }>}
154
154
  */
155
- async _deleteFiles(client, org, repo, deleted) {
155
+ async _deleteFiles(client, org, site, deleted) {
156
156
  const { log } = this;
157
157
  let pushed = 0;
158
158
  let errors = 0;
@@ -162,7 +162,7 @@ export default class PushCommand {
162
162
  [...deleted],
163
163
  async (daPath) => {
164
164
  try {
165
- await client.deleteSource(org, repo, daPath);
165
+ await client.deleteSource(org, site, daPath);
166
166
  log.info(` ✓ deleted ${daPath}`);
167
167
  return { ok: true };
168
168
  } catch (err) {
@@ -186,12 +186,7 @@ export default class PushCommand {
186
186
  async run() {
187
187
  const { log } = this;
188
188
  const contentDir = path.resolve(this._dir, CONTENT_DIR);
189
- const configPath = path.join(contentDir, CONFIG_FILE);
190
-
191
- if (!await fse.pathExists(configPath)) {
192
- throw new Error(`No config found at ${configPath}. Run 'aem content clone' first.`);
193
- }
194
- const { org, repo } = await fse.readJson(configPath);
189
+ const { org, site } = await readContentConfig(contentDir);
195
190
 
196
191
  const matrix = await git.statusMatrix({ fs, dir: contentDir });
197
192
  if (statusMatrixHasUncommitted(matrix)) {
@@ -228,7 +223,7 @@ export default class PushCommand {
228
223
 
229
224
  const token = await getValidToken(log, this._token, this._dir);
230
225
 
231
- log.info(`Pushing content to da.live: ${org}/${repo}`);
226
+ log.info(`Pushing content to da.live: ${org}/${site}`);
232
227
  log.info(`${added.length} added, ${modified.length} modified, ${deleted.length} deleted`);
233
228
 
234
229
  const client = new DaClient(token);
@@ -236,7 +231,7 @@ export default class PushCommand {
236
231
  const shouldAbort = await this._checkConflicts(
237
232
  client,
238
233
  org,
239
- repo,
234
+ site,
240
235
  modified,
241
236
  deleted,
242
237
  lastSyncTime,
@@ -271,12 +266,12 @@ export default class PushCommand {
271
266
  const {
272
267
  pushed: putPushed,
273
268
  errors: putErrors,
274
- } = await this._uploadFiles(client, org, repo, contentDir, [...added, ...modified]);
269
+ } = await this._uploadFiles(client, org, site, contentDir, [...added, ...modified]);
275
270
 
276
271
  const {
277
272
  pushed: deletePushed,
278
273
  errors: deleteErrors,
279
- } = await this._deleteFiles(client, org, repo, deleted);
274
+ } = await this._deleteFiles(client, org, site, deleted);
280
275
 
281
276
  const pushed = putPushed + deletePushed;
282
277
  const pushErrors = putErrors + deleteErrors;
@@ -11,9 +11,8 @@
11
11
  */
12
12
  import fs from 'fs';
13
13
  import path from 'path';
14
- import fse from 'fs-extra';
15
14
  import git from 'isomorphic-git';
16
- import { CONTENT_DIR, CONFIG_FILE } from './content-shared.js';
15
+ import { CONTENT_DIR, readContentConfig } from './content-shared.js';
17
16
  import { resolveSyncedOid, countCommitsAhead } from './content-git.js';
18
17
 
19
18
  export default class StatusCommand {
@@ -30,14 +29,9 @@ export default class StatusCommand {
30
29
  async run() {
31
30
  const { log } = this;
32
31
  const contentDir = path.resolve(this._dir, CONTENT_DIR);
33
- const configPath = path.join(contentDir, CONFIG_FILE);
32
+ const { org, site } = await readContentConfig(contentDir);
34
33
 
35
- if (!await fse.pathExists(configPath)) {
36
- throw new Error('No config found. Run \'aem content clone\' first.');
37
- }
38
- const { org, repo } = await fse.readJson(configPath);
39
-
40
- log.info(`On da.live: ${org}/${repo}`);
34
+ log.info(`On da.live: ${org}/${site}`);
41
35
  log.info(`Local content: ./${CONTENT_DIR}/\n`);
42
36
 
43
37
  const matrix = await git.statusMatrix({ fs, dir: contentDir });