@adobe/aem-cli 16.17.1 → 16.18.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.
@@ -0,0 +1,150 @@
1
+ /*
2
+ * Copyright 2026 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import fse from 'fs-extra';
15
+ import git from 'isomorphic-git';
16
+ import { diff3Merge } from 'node-diff3'; // eslint-disable-line import/no-unresolved
17
+ import { DaClient } from './da-api.js';
18
+ import { getValidToken } from './da-auth.js';
19
+ import { CONTENT_DIR, CONFIG_FILE } from './content-shared.js';
20
+
21
+ /**
22
+ * 3-way merge using diff3 algorithm.
23
+ * Returns { text, conflict }.
24
+ */
25
+ function threeWayMerge(base, local, remote) {
26
+ const baseLines = base.split('\n');
27
+ const localLines = local.split('\n');
28
+ const remoteLines = remote.split('\n');
29
+
30
+ const regions = diff3Merge(localLines, baseLines, remoteLines);
31
+
32
+ const out = [];
33
+ let hasConflict = false;
34
+
35
+ for (const region of regions) {
36
+ if (region.ok) {
37
+ out.push(...region.ok);
38
+ } else {
39
+ out.push('<<<<<<< LOCAL', ...region.conflict.a, '=======', ...region.conflict.b, '>>>>>>> REMOTE');
40
+ hasConflict = true;
41
+ }
42
+ }
43
+
44
+ return { text: out.join('\n'), conflict: hasConflict };
45
+ }
46
+
47
+ export default class MergeCommand {
48
+ constructor(logger) {
49
+ this.log = logger;
50
+ this._dir = process.cwd();
51
+ this._filePath = null;
52
+ }
53
+
54
+ withDirectory(dir) {
55
+ this._dir = dir;
56
+ return this;
57
+ }
58
+
59
+ withToken(token) {
60
+ this._token = token;
61
+ return this;
62
+ }
63
+
64
+ withFilePath(filePath) {
65
+ this._filePath = filePath || null;
66
+ return this;
67
+ }
68
+
69
+ async run() {
70
+ const { log } = this;
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);
78
+
79
+ // Find locally changed files via git
80
+ const matrix = await git.statusMatrix({ fs, dir: contentDir });
81
+ let changedPaths = matrix
82
+ .filter(([, head, workdir]) => head === 1 && workdir === 2)
83
+ .map(([filepath]) => `/${filepath}`);
84
+
85
+ if (this._filePath) {
86
+ const needle = this._filePath.startsWith('/') ? this._filePath : `/${this._filePath}`;
87
+ changedPaths = changedPaths.filter((p) => p === needle);
88
+ if (changedPaths.length === 0) {
89
+ log.info(`No local changes detected for ${needle}`);
90
+ return;
91
+ }
92
+ }
93
+
94
+ if (changedPaths.length === 0) {
95
+ log.info('Nothing to merge. No locally modified files.');
96
+ return;
97
+ }
98
+
99
+ const token = await getValidToken(log, this._token, contentDir);
100
+ const client = new DaClient(token);
101
+
102
+ // Get the HEAD commit to read base blobs
103
+ const [headCommit] = await git.log({ fs, dir: contentDir, depth: 1 });
104
+
105
+ let cleanCount = 0;
106
+ let conflictCount = 0;
107
+
108
+ for (const daPath of changedPaths) {
109
+ const relPath = daPath.slice(1); // strip leading /
110
+ const localPath = path.join(contentDir, ...daPath.split('/').filter(Boolean));
111
+
112
+ // eslint-disable-next-line no-await-in-loop
113
+ const [localBuffer, remoteRes, blobResult] = await Promise.all([
114
+ fse.readFile(localPath),
115
+ client.getSource(org, repo, daPath),
116
+ git.readBlob({
117
+ fs, dir: contentDir, oid: headCommit.oid, filepath: relPath,
118
+ })
119
+ .catch(() => null),
120
+ ]);
121
+
122
+ const localText = localBuffer.toString('utf-8');
123
+ // eslint-disable-next-line no-await-in-loop
124
+ const remoteText = remoteRes ? await remoteRes.text() : '';
125
+ const baseText = blobResult ? Buffer.from(blobResult.blob).toString('utf-8') : '';
126
+
127
+ const { text: merged, conflict } = threeWayMerge(baseText, localText, remoteText);
128
+
129
+ // eslint-disable-next-line no-await-in-loop
130
+ await fse.writeFile(localPath, merged, 'utf-8');
131
+
132
+ if (conflict) {
133
+ log.info(`CONFLICT ${daPath}`);
134
+ conflictCount += 1;
135
+ } else {
136
+ log.info(`merged ${daPath}`);
137
+ cleanCount += 1;
138
+ }
139
+ }
140
+
141
+ log.info('');
142
+ if (conflictCount > 0) {
143
+ log.info(`Merge complete: ${cleanCount} clean, ${conflictCount} with conflicts.`);
144
+ log.info('Resolve conflicts manually, then: aem content add, aem content commit -m "...", aem content push');
145
+ } else {
146
+ log.info(`Merge complete: ${cleanCount} file(s) merged cleanly.`);
147
+ log.info('Review changes, then: aem content add, aem content commit -m "...", aem content push');
148
+ }
149
+ }
150
+ }
@@ -0,0 +1,45 @@
1
+ /*
2
+ * Copyright 2026 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import { getOrCreateLogger } from '../log-common.js';
13
+
14
+ export default function mergeCmd() {
15
+ let executor;
16
+ return {
17
+ set executor(value) {
18
+ executor = value;
19
+ },
20
+ command: 'merge [path]',
21
+ description: 'Merge remote content into local files',
22
+ builder: (yargs) => {
23
+ yargs
24
+ .positional('path', {
25
+ describe: 'File to merge (e.g. /blog/post.html). Merges all modified files if omitted.',
26
+ type: 'string',
27
+ })
28
+ .option('token', {
29
+ describe: 'IMS Bearer token for da.live authentication',
30
+ type: 'string',
31
+ })
32
+ .help();
33
+ },
34
+ handler: async (argv) => {
35
+ if (!executor) {
36
+ const MergeCommand = (await import('./merge.cmd.js')).default;
37
+ executor = new MergeCommand(getOrCreateLogger(argv));
38
+ }
39
+ await executor
40
+ .withToken(argv.token)
41
+ .withFilePath(argv.path)
42
+ .run();
43
+ },
44
+ };
45
+ }
@@ -0,0 +1,291 @@
1
+ /*
2
+ * Copyright 2026 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import fse from 'fs-extra';
15
+ import git from 'isomorphic-git';
16
+ import processQueue from '@adobe/helix-shared-process-queue';
17
+ import { DaClient, getContentType } from './da-api.js';
18
+ import { getValidToken } from './da-auth.js';
19
+ import {
20
+ CONTENT_DIR,
21
+ CONFIG_FILE,
22
+ CONTENT_IO_CONCURRENCY,
23
+ } from './content-shared.js';
24
+ import {
25
+ resolveSyncedOid,
26
+ writeSyncedRef,
27
+ statusMatrixHasUncommitted,
28
+ diffCommitTrees,
29
+ getCommitCommitterTimeMs,
30
+ } from './content-git.js';
31
+
32
+ export default class PushCommand {
33
+ constructor(logger) {
34
+ this.log = logger;
35
+ this._dir = process.cwd();
36
+ this._force = false;
37
+ this._dryRun = false;
38
+ this._pushPath = null;
39
+ }
40
+
41
+ withDirectory(dir) {
42
+ this._dir = dir;
43
+ return this;
44
+ }
45
+
46
+ withToken(token) {
47
+ this._token = token;
48
+ return this;
49
+ }
50
+
51
+ withForce(force) {
52
+ this._force = force;
53
+ return this;
54
+ }
55
+
56
+ withDryRun(dryRun) {
57
+ this._dryRun = dryRun;
58
+ return this;
59
+ }
60
+
61
+ withPath(pushPath) {
62
+ this._pushPath = pushPath || null;
63
+ return this;
64
+ }
65
+
66
+ /**
67
+ * Checks for conflicts between local changes and remote modifications.
68
+ * @param {DaClient} client
69
+ * @param {string} org
70
+ * @param {string} repo
71
+ * @param {string[]} modified
72
+ * @param {string[]} deleted
73
+ * @param {number} lastSyncTime
74
+ * @returns {Promise<boolean>} true if the push should be aborted
75
+ */
76
+ async _checkConflicts(client, org, repo, modified, deleted, lastSyncTime) {
77
+ const { log } = this;
78
+ const conflicts = [];
79
+
80
+ for (const daPath of [...modified, ...deleted]) {
81
+ // eslint-disable-next-line no-await-in-loop
82
+ const remoteLastModified = await client.getRemoteLastModified(org, repo, daPath);
83
+ if (remoteLastModified != null && remoteLastModified > lastSyncTime) {
84
+ conflicts.push({ daPath, remoteDate: new Date(remoteLastModified).toLocaleString() });
85
+ }
86
+ }
87
+
88
+ if (conflicts.length > 0) {
89
+ log.warn('\nConflicts detected — remote files were modified after your last sync:\n');
90
+ for (const { daPath, remoteDate } of conflicts) {
91
+ log.warn(` ✗ ${daPath} (remote modified ${remoteDate})`);
92
+ }
93
+ if (!this._force) {
94
+ log.warn('\nPush aborted. Use --force to overwrite remote changes.');
95
+ process.exitCode = 1;
96
+ return true;
97
+ }
98
+ log.warn('\n--force specified: overwriting remote changes.');
99
+ }
100
+
101
+ return false;
102
+ }
103
+
104
+ /**
105
+ * Uploads added and modified files to da.live.
106
+ * @param {DaClient} client
107
+ * @param {string} org
108
+ * @param {string} repo
109
+ * @param {string} contentDir
110
+ * @param {string[]} targets
111
+ * @returns {Promise<{ pushed: number, errors: number, successfullyPushed: Set<string> }>}
112
+ */
113
+ async _uploadFiles(client, org, repo, contentDir, targets) {
114
+ const { log } = this;
115
+ let pushed = 0;
116
+ let errors = 0;
117
+ const successfullyPushed = new Set();
118
+
119
+ const results = await processQueue(
120
+ targets,
121
+ async (daPath) => {
122
+ const localPath = path.join(contentDir, ...daPath.split('/').filter(Boolean));
123
+ const ext = daPath.split('.').pop();
124
+ try {
125
+ const buffer = await fse.readFile(localPath);
126
+ await client.putSource(org, repo, daPath, buffer, getContentType(ext));
127
+ log.info(` ✓ ${daPath}`);
128
+ return { ok: true, daPath };
129
+ } catch (err) {
130
+ log.warn(` ✗ ${daPath}: ${err.message}`);
131
+ return { ok: false };
132
+ }
133
+ },
134
+ CONTENT_IO_CONCURRENCY,
135
+ );
136
+ for (const r of results) {
137
+ if (r.ok) {
138
+ pushed += 1;
139
+ successfullyPushed.add(r.daPath);
140
+ } else {
141
+ errors += 1;
142
+ }
143
+ }
144
+
145
+ return { pushed, errors, successfullyPushed };
146
+ }
147
+
148
+ /**
149
+ * Deletes files from da.live.
150
+ * @param {DaClient} client
151
+ * @param {string} org
152
+ * @param {string} repo
153
+ * @param {string[]} deleted
154
+ * @returns {Promise<{ pushed: number, errors: number, successfullyDeleted: Set<string> }>}
155
+ */
156
+ async _deleteFiles(client, org, repo, deleted) {
157
+ const { log } = this;
158
+ let pushed = 0;
159
+ let errors = 0;
160
+ const successfullyDeleted = new Set();
161
+
162
+ const results = await processQueue(
163
+ deleted,
164
+ async (daPath) => {
165
+ try {
166
+ await client.deleteSource(org, repo, daPath);
167
+ log.info(` ✓ deleted ${daPath}`);
168
+ return { ok: true, daPath };
169
+ } catch (err) {
170
+ log.warn(` ✗ ${daPath}: ${err.message}`);
171
+ return { ok: false };
172
+ }
173
+ },
174
+ CONTENT_IO_CONCURRENCY,
175
+ );
176
+ for (const r of results) {
177
+ if (r.ok) {
178
+ pushed += 1;
179
+ successfullyDeleted.add(r.daPath);
180
+ } else {
181
+ errors += 1;
182
+ }
183
+ }
184
+
185
+ return { pushed, errors, successfullyDeleted };
186
+ }
187
+
188
+ async run() {
189
+ const { log } = this;
190
+ const contentDir = path.resolve(this._dir, CONTENT_DIR);
191
+ const configPath = path.join(contentDir, CONFIG_FILE);
192
+
193
+ if (!await fse.pathExists(configPath)) {
194
+ throw new Error(`No config found at ${configPath}. Run 'aem content clone' first.`);
195
+ }
196
+ const { org, repo } = await fse.readJson(configPath);
197
+
198
+ const matrix = await git.statusMatrix({ fs, dir: contentDir });
199
+ if (statusMatrixHasUncommitted(matrix)) {
200
+ throw new Error(
201
+ 'Cannot push: you have uncommitted changes in content/. '
202
+ + 'Stage with \'aem content add\' and commit with \'aem content commit -m "..."\'.',
203
+ );
204
+ }
205
+
206
+ const headOid = await git.resolveRef({ fs, dir: contentDir, ref: 'HEAD' });
207
+ const syncedOid = await resolveSyncedOid(fs, contentDir);
208
+ const lastSyncTime = await getCommitCommitterTimeMs(fs, contentDir, syncedOid);
209
+
210
+ let { added, modified, deleted } = await diffCommitTrees(fs, contentDir, syncedOid, headOid);
211
+
212
+ const inScope = (daPath) => !this._pushPath || daPath.startsWith(this._pushPath);
213
+ added = added.filter(inScope);
214
+ modified = modified.filter(inScope);
215
+ deleted = deleted.filter(inScope);
216
+
217
+ if (added.length === 0 && modified.length === 0 && deleted.length === 0) {
218
+ log.info('Nothing to push. No commits ahead of the last da.live sync.');
219
+ return;
220
+ }
221
+
222
+ const token = await getValidToken(log, this._token, this._dir);
223
+
224
+ log.info(`Pushing content to da.live: ${org}/${repo}`);
225
+ log.info(`${added.length} added, ${modified.length} modified, ${deleted.length} deleted`);
226
+
227
+ const client = new DaClient(token);
228
+
229
+ const shouldAbort = await this._checkConflicts(
230
+ client,
231
+ org,
232
+ repo,
233
+ modified,
234
+ deleted,
235
+ lastSyncTime,
236
+ );
237
+ if (shouldAbort) {
238
+ return;
239
+ }
240
+
241
+ if (this._dryRun) {
242
+ log.info('\nDry run — no files were pushed.');
243
+ if (added.length) {
244
+ log.info('\nWould add:');
245
+ for (const p of added) {
246
+ log.info(` + ${p}`);
247
+ }
248
+ }
249
+ if (modified.length) {
250
+ log.info('\nWould update:');
251
+ for (const p of modified) {
252
+ log.info(` ~ ${p}`);
253
+ }
254
+ }
255
+ if (deleted.length) {
256
+ log.info('\nWould delete:');
257
+ for (const p of deleted) {
258
+ log.info(` - ${p}`);
259
+ }
260
+ }
261
+ return;
262
+ }
263
+
264
+ const putTargets = [...added, ...modified];
265
+ const {
266
+ pushed: putPushed,
267
+ errors: putErrors,
268
+ successfullyPushed,
269
+ } = await this._uploadFiles(client, org, repo, contentDir, putTargets);
270
+
271
+ const {
272
+ pushed: deletePushed,
273
+ errors: deleteErrors,
274
+ successfullyDeleted,
275
+ } = await this._deleteFiles(client, org, repo, deleted);
276
+
277
+ const pushed = putPushed + deletePushed;
278
+ const pushErrors = putErrors + deleteErrors;
279
+
280
+ const allPutsOk = putTargets.every((p) => successfullyPushed.has(p));
281
+ const allDeletesOk = deleted.every((p) => successfullyDeleted.has(p));
282
+
283
+ if (allPutsOk && allDeletesOk) {
284
+ await writeSyncedRef(fs, contentDir, headOid);
285
+ } else {
286
+ log.warn('\nSync ref not updated: fix errors and push again to finish syncing this commit.');
287
+ }
288
+
289
+ log.info(`\nDone. ${pushed} file(s) pushed${pushErrors > 0 ? `, ${pushErrors} error(s)` : ''}.`);
290
+ }
291
+ }
@@ -0,0 +1,58 @@
1
+ /*
2
+ * Copyright 2026 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import { getOrCreateLogger } from '../log-common.js';
13
+
14
+ export default function push() {
15
+ let executor;
16
+ return {
17
+ set executor(value) {
18
+ executor = value;
19
+ },
20
+ command: 'push',
21
+ description: 'Push committed content/ changes to da.live (use content add & content commit first)',
22
+ builder: (yargs) => {
23
+ yargs
24
+ .option('token', {
25
+ describe: 'IMS Bearer token for da.live authentication',
26
+ type: 'string',
27
+ })
28
+ .option('path', {
29
+ describe: 'Push only a specific file or subtree (e.g. /blog)',
30
+ type: 'string',
31
+ })
32
+ .option('force', {
33
+ describe: 'Overwrite remote changes even when conflicts are detected',
34
+ type: 'boolean',
35
+ default: false,
36
+ })
37
+ .option('dry-run', {
38
+ alias: 'dryRun',
39
+ describe: 'Show what would be pushed without actually pushing',
40
+ type: 'boolean',
41
+ default: false,
42
+ })
43
+ .help();
44
+ },
45
+ handler: async (argv) => {
46
+ if (!executor) {
47
+ const PushCommand = (await import('./push.cmd.js')).default;
48
+ executor = new PushCommand(getOrCreateLogger(argv));
49
+ }
50
+ await executor
51
+ .withToken(argv.token)
52
+ .withPath(argv.path)
53
+ .withForce(argv.force)
54
+ .withDryRun(argv.dryRun)
55
+ .run();
56
+ },
57
+ };
58
+ }
@@ -0,0 +1,94 @@
1
+ /*
2
+ * Copyright 2026 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import fse from 'fs-extra';
15
+ import git from 'isomorphic-git';
16
+ import { CONTENT_DIR, CONFIG_FILE } from './content-shared.js';
17
+ import { resolveSyncedOid, countCommitsAhead } from './content-git.js';
18
+
19
+ export default class StatusCommand {
20
+ constructor(logger) {
21
+ this.log = logger;
22
+ this._dir = process.cwd();
23
+ }
24
+
25
+ withDirectory(dir) {
26
+ this._dir = dir;
27
+ return this;
28
+ }
29
+
30
+ async run() {
31
+ const { log } = this;
32
+ const contentDir = path.resolve(this._dir, CONTENT_DIR);
33
+ const configPath = path.join(contentDir, CONFIG_FILE);
34
+
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}`);
41
+ log.info(`Local content: ./${CONTENT_DIR}/\n`);
42
+
43
+ const matrix = await git.statusMatrix({ fs, dir: contentDir });
44
+
45
+ const added = [];
46
+ const modified = [];
47
+ const deleted = [];
48
+
49
+ for (const [filepath, head, workdir] of matrix) {
50
+ if (head === 0 && workdir === 2) {
51
+ added.push(filepath);
52
+ } else if (head === 1 && workdir === 2) {
53
+ modified.push(filepath);
54
+ } else if (head === 1 && workdir === 0) {
55
+ deleted.push(filepath);
56
+ }
57
+ }
58
+
59
+ const headOid = await git.resolveRef({ fs, dir: contentDir, ref: 'HEAD' });
60
+ const syncedOid = await resolveSyncedOid(fs, contentDir);
61
+ const ahead = await countCommitsAhead(fs, contentDir, headOid, syncedOid);
62
+
63
+ if (added.length === 0 && modified.length === 0 && deleted.length === 0) {
64
+ log.info('No uncommitted changes in content/.');
65
+ if (ahead === 0) {
66
+ log.info('Nothing to push to da.live.');
67
+ } else {
68
+ log.info(`${ahead} commit(s) not yet pushed to da.live. Run 'aem content push'.`);
69
+ }
70
+ return;
71
+ }
72
+
73
+ if (added.length) {
74
+ log.info('Added (unstaged or not committed):');
75
+ for (const f of added) {
76
+ log.info(` A /${f}`);
77
+ }
78
+ }
79
+ if (modified.length) {
80
+ log.info('Modified (unstaged or not committed):');
81
+ for (const f of modified) {
82
+ log.info(` M /${f}`);
83
+ }
84
+ }
85
+ if (deleted.length) {
86
+ log.info('Deleted (unstaged or not committed):');
87
+ for (const f of deleted) {
88
+ log.info(` D /${f}`);
89
+ }
90
+ }
91
+
92
+ log.info(`\n${added.length} added, ${modified.length} modified, ${deleted.length} deleted`);
93
+ }
94
+ }
@@ -0,0 +1,33 @@
1
+ /*
2
+ * Copyright 2026 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import { getOrCreateLogger } from '../log-common.js';
13
+
14
+ export default function status() {
15
+ let executor;
16
+ return {
17
+ set executor(value) {
18
+ executor = value;
19
+ },
20
+ command: 'status',
21
+ description: 'Show locally added, modified, and deleted content files',
22
+ builder: (yargs) => {
23
+ yargs.help();
24
+ },
25
+ handler: async (argv) => {
26
+ if (!executor) {
27
+ const StatusCommand = (await import('./status.cmd.js')).default;
28
+ executor = new StatusCommand(getOrCreateLogger(argv));
29
+ }
30
+ await executor.run();
31
+ },
32
+ };
33
+ }
@@ -10,10 +10,13 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
  import https from 'https';
13
+ import { existsSync } from 'fs';
14
+ import path from 'path';
13
15
  import EventEmitter from 'events';
14
16
  import express from 'express';
15
17
  import cookieParser from 'cookie-parser';
16
18
  import { getFetch } from '../fetch-utils.js';
19
+ import { CONTENT_DIR } from '../content/content-shared.js';
17
20
  import utils from './utils.js';
18
21
  import packageJson from '../package.cjs';
19
22
 
@@ -155,7 +158,12 @@ export class BaseServer extends EventEmitter {
155
158
  this._addr = this._server.address().address;
156
159
  log.info(`Local AEM dev server up and running: ${this.scheme}://${this.hostname}:${this.port}/`);
157
160
  if (this._project.proxyUrl) {
158
- log.info(`Enabled reverse proxy to ${this._project.proxyUrl}`);
161
+ const contentDir = path.join(this._project.directory, CONTENT_DIR);
162
+ if (existsSync(contentDir)) {
163
+ log.info(`Serving content from local ${CONTENT_DIR}/, proxying missing files from ${this._project.proxyUrl}`);
164
+ } else {
165
+ log.info(`Enabled reverse proxy to ${this._project.proxyUrl}`);
166
+ }
159
167
  }
160
168
  this._server.on('connection', (socket) => {
161
169
  log.debug(`new connection from ${socket.remoteAddress}:${socket.remotePort}`);