@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.
- package/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/package.json +6 -1
- package/src/cli.js +3 -0
- package/src/content/add.cmd.js +133 -0
- package/src/content/add.js +47 -0
- package/src/content/clone.cmd.js +214 -0
- package/src/content/clone.js +74 -0
- package/src/content/commit.cmd.js +57 -0
- package/src/content/commit.js +40 -0
- package/src/content/content-git.js +181 -0
- package/src/content/content-metadata-html.js +321 -0
- package/src/content/content-shared.js +44 -0
- package/src/content/content.js +38 -0
- package/src/content/da-api.js +198 -0
- package/src/content/da-auth.js +209 -0
- package/src/content/diff.cmd.js +123 -0
- package/src/content/diff.js +45 -0
- package/src/content/merge.cmd.js +150 -0
- package/src/content/merge.js +45 -0
- package/src/content/push.cmd.js +291 -0
- package/src/content/push.js +58 -0
- package/src/content/status.cmd.js +94 -0
- package/src/content/status.js +33 -0
- package/src/server/BaseServer.js +9 -1
- package/src/server/HelixProject.js +19 -0
- package/src/server/HelixServer.js +82 -0
- package/src/server/MetadataSheetSupport.js +221 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [16.18.0](https://github.com/adobe/helix-cli/compare/v16.17.1...v16.18.0) (2026-04-16)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* aem content ([#2689](https://github.com/adobe/helix-cli/issues/2689)) ([5524b6d](https://github.com/adobe/helix-cli/commit/5524b6d63949a1d45efe35479a31ab55e62f78ef))
|
|
7
|
+
|
|
1
8
|
## [16.17.1](https://github.com/adobe/helix-cli/compare/v16.17.0...v16.17.1) (2026-04-08)
|
|
2
9
|
|
|
3
10
|
|
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ The `--html-folder` option enables serving HTML files without extensions, useful
|
|
|
80
80
|
|
|
81
81
|
```
|
|
82
82
|
$ aem up --html-folder drafts # serves at /drafts/*
|
|
83
|
-
$ aem up --html-folder
|
|
83
|
+
$ aem up --html-folder html --html-mount / # serves at /* (root)
|
|
84
84
|
$ aem up --html-folder drafts --html-mount /preview # serves at /preview/*
|
|
85
85
|
```
|
|
86
86
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/aem-cli",
|
|
3
|
-
"version": "16.
|
|
3
|
+
"version": "16.18.0",
|
|
4
4
|
"description": "AEM CLI",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -48,12 +48,14 @@
|
|
|
48
48
|
"@adobe/helix-shared-config": "11.1.20",
|
|
49
49
|
"@adobe/helix-shared-git": "3.0.23",
|
|
50
50
|
"@adobe/helix-shared-indexer": "2.2.7",
|
|
51
|
+
"@adobe/helix-shared-process-queue": "3.1.6",
|
|
51
52
|
"camelcase": "9.0.0",
|
|
52
53
|
"chalk-template": "1.1.2",
|
|
53
54
|
"chokidar": "5.0.0",
|
|
54
55
|
"compression": "1.8.1",
|
|
55
56
|
"cookie": "1.1.1",
|
|
56
57
|
"cookie-parser": "1.4.7",
|
|
58
|
+
"diff": "8.0.3",
|
|
57
59
|
"dotenv": "17.3.1",
|
|
58
60
|
"express": "5.2.1",
|
|
59
61
|
"faye-websocket": "0.11.4",
|
|
@@ -61,13 +63,16 @@
|
|
|
61
63
|
"glob": "13.0.6",
|
|
62
64
|
"glob-to-regexp": "0.4.1",
|
|
63
65
|
"hast-util-select": "6.0.4",
|
|
66
|
+
"hast-util-to-html": "9.0.5",
|
|
64
67
|
"http-proxy-agent": "8.0.0",
|
|
65
68
|
"https-proxy-agent": "8.0.0",
|
|
66
69
|
"ignore": "7.0.5",
|
|
67
70
|
"ini": "6.0.0",
|
|
68
71
|
"isomorphic-git": "1.37.4",
|
|
69
72
|
"jose": "6.2.2",
|
|
73
|
+
"mime": "4.1.0",
|
|
70
74
|
"livereload-js": "4.0.2",
|
|
75
|
+
"node-diff3": "3.2.0",
|
|
71
76
|
"node-fetch": "3.3.2",
|
|
72
77
|
"open": "11.0.0",
|
|
73
78
|
"progress": "2.0.3",
|
package/src/cli.js
CHANGED
|
@@ -112,6 +112,9 @@ export default class CLI {
|
|
|
112
112
|
this._commands[cmd] = (await import(`./${cmd}.js`)).default();
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
+
if (!this._commands.content) {
|
|
116
|
+
this._commands.content = (await import('./content/content.js')).default();
|
|
117
|
+
}
|
|
115
118
|
}
|
|
116
119
|
return this;
|
|
117
120
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Paths for git add are relative to the content repo root. Accept optional
|
|
20
|
+
* `content/…` (or `{CONTENT_DIR}/…`) so shells can tab-complete from the project root.
|
|
21
|
+
* @param {string} raw
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
export function normalizePathForContentAdd(raw) {
|
|
25
|
+
let s = String(raw).trim().replace(/\\/g, '/');
|
|
26
|
+
while (s.startsWith('./')) {
|
|
27
|
+
s = s.slice(2);
|
|
28
|
+
}
|
|
29
|
+
const prefix = `${CONTENT_DIR}/`;
|
|
30
|
+
if (s === CONTENT_DIR || s === `${CONTENT_DIR}/`) {
|
|
31
|
+
return '.';
|
|
32
|
+
}
|
|
33
|
+
if (s.startsWith(prefix)) {
|
|
34
|
+
const rest = s.slice(prefix.length);
|
|
35
|
+
return rest.length > 0 ? rest : '.';
|
|
36
|
+
}
|
|
37
|
+
return s.length > 0 ? s : '.';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether a repo-relative path is covered by an `aem content add` path (`.` = whole tree).
|
|
42
|
+
* @param {string} filepath
|
|
43
|
+
* @param {string} scope normalized path from {@link normalizePathForContentAdd}
|
|
44
|
+
*/
|
|
45
|
+
export function filepathInContentAddScope(filepath, scope) {
|
|
46
|
+
if (scope === '.' || scope === '') {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
const s = scope.replace(/\/$/, '');
|
|
50
|
+
return filepath === s || filepath.startsWith(`${s}/`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* isomorphic-git `add` only walks paths that exist on disk, so deletions are never staged.
|
|
55
|
+
* Stage removals for tracked files missing from the workdir (same idea as `git add -u` for
|
|
56
|
+
* those scopes).
|
|
57
|
+
* @param {import('isomorphic-git').FsClient} fsClient
|
|
58
|
+
* @param {string} dir content repo root
|
|
59
|
+
* @param {string[]} scopes normalized add paths
|
|
60
|
+
* @returns {Promise<number>} number of index entries removed
|
|
61
|
+
*/
|
|
62
|
+
export async function stageDeletionsForContentAddScopes(fsClient, dir, scopes) {
|
|
63
|
+
const matrix = await git.statusMatrix({ fs: fsClient, dir });
|
|
64
|
+
let n = 0;
|
|
65
|
+
for (const [filepath, head, workdir, stage] of matrix) {
|
|
66
|
+
const unstagedDelete = head === 1 && workdir === 0 && stage !== 0;
|
|
67
|
+
const inScope = scopes.some((sc) => filepathInContentAddScope(filepath, sc));
|
|
68
|
+
if (unstagedDelete && inScope) {
|
|
69
|
+
// eslint-disable-next-line no-await-in-loop
|
|
70
|
+
await git.remove({ fs: fsClient, dir, filepath });
|
|
71
|
+
n += 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return n;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isNotFoundError(err) {
|
|
78
|
+
return err?.code === 'NotFoundError' || err?.name === 'NotFoundError';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default class AddCommand {
|
|
82
|
+
constructor(logger) {
|
|
83
|
+
this.log = logger;
|
|
84
|
+
this._dir = process.cwd();
|
|
85
|
+
/** @type {string[]} */
|
|
86
|
+
this._paths = ['.'];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
withDirectory(dir) {
|
|
90
|
+
this._dir = dir;
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {string[]} paths paths relative to content/ (default ["."])
|
|
96
|
+
*/
|
|
97
|
+
withPaths(paths) {
|
|
98
|
+
this._paths = paths && paths.length > 0 ? paths : ['.'];
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async run() {
|
|
103
|
+
const { log } = this;
|
|
104
|
+
const contentDir = path.resolve(this._dir, CONTENT_DIR);
|
|
105
|
+
const configPath = path.join(contentDir, CONFIG_FILE);
|
|
106
|
+
|
|
107
|
+
if (!await fse.pathExists(configPath)) {
|
|
108
|
+
throw new Error(`No config found at ${configPath}. Run 'aem content clone' first.`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const normalized = this._paths.map((p) => normalizePathForContentAdd(p));
|
|
112
|
+
|
|
113
|
+
for (const fp of normalized) {
|
|
114
|
+
try {
|
|
115
|
+
// eslint-disable-next-line no-await-in-loop
|
|
116
|
+
await git.add({ fs, dir: contentDir, filepath: fp });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (!isNotFoundError(err)) {
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
// Deleted directory/file: `add` cannot lstat the path; stage removal if it was tracked.
|
|
122
|
+
// eslint-disable-next-line no-await-in-loop
|
|
123
|
+
const staged = await stageDeletionsForContentAddScopes(fs, contentDir, [fp]);
|
|
124
|
+
if (staged === 0) {
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// `git add .` never visits missing files — stage tracked deletions under the requested paths.
|
|
130
|
+
await stageDeletionsForContentAddScopes(fs, contentDir, normalized);
|
|
131
|
+
log.info(`Staged: ${normalized.join(', ')}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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 add() {
|
|
15
|
+
let executor;
|
|
16
|
+
return {
|
|
17
|
+
set executor(value) {
|
|
18
|
+
executor = value;
|
|
19
|
+
},
|
|
20
|
+
command: 'add [files..]',
|
|
21
|
+
description: 'Stage changes in content/ (like git add)',
|
|
22
|
+
builder: (yargs) => {
|
|
23
|
+
yargs
|
|
24
|
+
.positional('files', {
|
|
25
|
+
describe:
|
|
26
|
+
'Paths under content/ to stage, or the same with a content/ prefix for tab completion (default: all)',
|
|
27
|
+
type: 'string',
|
|
28
|
+
array: true,
|
|
29
|
+
})
|
|
30
|
+
.help();
|
|
31
|
+
},
|
|
32
|
+
handler: async (argv) => {
|
|
33
|
+
if (!executor) {
|
|
34
|
+
const AddCommand = (await import('./add.cmd.js')).default;
|
|
35
|
+
executor = new AddCommand(getOrCreateLogger(argv));
|
|
36
|
+
}
|
|
37
|
+
const { files } = argv;
|
|
38
|
+
let paths = [];
|
|
39
|
+
if (Array.isArray(files)) {
|
|
40
|
+
paths = files;
|
|
41
|
+
} else if (files) {
|
|
42
|
+
paths = [files];
|
|
43
|
+
}
|
|
44
|
+
await executor.withPaths(paths).run();
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
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 readline from 'readline';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import fse from 'fs-extra';
|
|
16
|
+
import git from 'isomorphic-git';
|
|
17
|
+
import processQueue from '@adobe/helix-shared-process-queue';
|
|
18
|
+
import GitUtils from '../git-utils.js';
|
|
19
|
+
import { prompt } from '../cli-util.js';
|
|
20
|
+
import { DaClient } from './da-api.js';
|
|
21
|
+
import { getValidToken } from './da-auth.js';
|
|
22
|
+
import {
|
|
23
|
+
CONTENT_DIR,
|
|
24
|
+
CONFIG_FILE,
|
|
25
|
+
GIT_AUTHOR,
|
|
26
|
+
LARGE_CLONE_FILE_THRESHOLD,
|
|
27
|
+
CONTENT_IO_CONCURRENCY,
|
|
28
|
+
} from './content-shared.js';
|
|
29
|
+
import { writeSyncedRef, ensureGitIgnored } from './content-git.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {*} log logger with .warn
|
|
33
|
+
* @param {number} fileCount
|
|
34
|
+
* @param {boolean} assumeYes
|
|
35
|
+
*/
|
|
36
|
+
async function confirmLargeCloneIfNeeded(log, fileCount, assumeYes) {
|
|
37
|
+
if (fileCount <= LARGE_CLONE_FILE_THRESHOLD) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
log.warn(
|
|
41
|
+
`This clone lists ${fileCount} files (more than ${LARGE_CLONE_FILE_THRESHOLD.toLocaleString()}). `
|
|
42
|
+
+ 'Downloading may take a long time and use substantial disk space.',
|
|
43
|
+
);
|
|
44
|
+
if (assumeYes) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Large clone (${fileCount} files) needs confirmation. Re-run with --yes, or clone a smaller path with --path.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
53
|
+
try {
|
|
54
|
+
const answer = await prompt(rl, 'Do you really want to proceed? [y/N] ');
|
|
55
|
+
if (!/^y(es)?$/i.test(String(answer).trim())) {
|
|
56
|
+
throw new Error('Clone cancelled.');
|
|
57
|
+
}
|
|
58
|
+
} finally {
|
|
59
|
+
rl.close();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default class CloneCommand {
|
|
64
|
+
constructor(logger) {
|
|
65
|
+
this.log = logger;
|
|
66
|
+
this._dir = process.cwd();
|
|
67
|
+
this._force = false;
|
|
68
|
+
this._assumeYes = false;
|
|
69
|
+
this._rootPath = null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
withDirectory(dir) {
|
|
73
|
+
this._dir = dir;
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
withToken(token) {
|
|
78
|
+
this._token = token;
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
withForce(force) {
|
|
83
|
+
this._force = force;
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
withAssumeYes(yes) {
|
|
88
|
+
this._assumeYes = !!yes;
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
withRootPath(daPath) {
|
|
93
|
+
this._rootPath = daPath;
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async run() {
|
|
98
|
+
const { log } = this;
|
|
99
|
+
|
|
100
|
+
if (this._rootPath == null) {
|
|
101
|
+
throw new Error('Clone root path was not set (internal error).');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 1. Resolve org/repo from git remote
|
|
105
|
+
const originUrl = await GitUtils.getOriginURL(this._dir);
|
|
106
|
+
if (!originUrl) {
|
|
107
|
+
throw new Error('No git remote found. Run `aem content clone` inside an AEM project directory.');
|
|
108
|
+
}
|
|
109
|
+
const org = originUrl.owner;
|
|
110
|
+
const { repo } = originUrl;
|
|
111
|
+
log.info(`Cloning content from da.live: ${org}/${repo}${this._rootPath === '/' ? '' : ` @ ${this._rootPath}`}`);
|
|
112
|
+
|
|
113
|
+
// 2. Ensure target path is available (do not create content/ until after file count is known)
|
|
114
|
+
const contentDir = path.resolve(this._dir, CONTENT_DIR);
|
|
115
|
+
if (await fse.pathExists(contentDir)) {
|
|
116
|
+
if (!this._force) {
|
|
117
|
+
throw new Error(`'${CONTENT_DIR}' already exists. Use --force to overwrite.`);
|
|
118
|
+
}
|
|
119
|
+
await fse.remove(contentDir);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 3. Resolve token
|
|
123
|
+
const token = await getValidToken(log, this._token, this._dir);
|
|
124
|
+
|
|
125
|
+
// 4. Fetch file list (no local content dir required yet)
|
|
126
|
+
const client = new DaClient(token);
|
|
127
|
+
log.info('Fetching file list...');
|
|
128
|
+
const showDiscoveryProgress = process.stdout.isTTY;
|
|
129
|
+
const files = await client.listAll(org, repo, this._rootPath, showDiscoveryProgress
|
|
130
|
+
? (n) => {
|
|
131
|
+
process.stdout.write(`\r ${n} file(s) discovered so far...`);
|
|
132
|
+
}
|
|
133
|
+
: undefined);
|
|
134
|
+
if (showDiscoveryProgress) {
|
|
135
|
+
process.stdout.write('\n');
|
|
136
|
+
}
|
|
137
|
+
log.info(`Found ${files.length} file(s).`);
|
|
138
|
+
|
|
139
|
+
await confirmLargeCloneIfNeeded(log, files.length, this._assumeYes);
|
|
140
|
+
|
|
141
|
+
// 5. Prepare content directory and project .gitignore
|
|
142
|
+
await fse.ensureDir(contentDir);
|
|
143
|
+
await ensureGitIgnored(this._dir, CONTENT_DIR);
|
|
144
|
+
|
|
145
|
+
log.info('Downloading...');
|
|
146
|
+
|
|
147
|
+
// 6. Download files (bounded concurrency)
|
|
148
|
+
const downloadResults = await processQueue(
|
|
149
|
+
files,
|
|
150
|
+
async (file) => {
|
|
151
|
+
const prefix = `/${org}/${repo}`;
|
|
152
|
+
if (!file.path.startsWith(prefix)) {
|
|
153
|
+
log.warn(` skip (unexpected path, missing org/repo prefix): ${file.path}`);
|
|
154
|
+
return { status: 404 };
|
|
155
|
+
}
|
|
156
|
+
const daPath = file.path.slice(prefix.length) || '/';
|
|
157
|
+
const localPath = path.join(contentDir, ...daPath.split('/').filter(Boolean));
|
|
158
|
+
try {
|
|
159
|
+
const res = await client.getSource(org, repo, daPath);
|
|
160
|
+
if (!res) {
|
|
161
|
+
log.warn(` skip (not found): ${daPath}`);
|
|
162
|
+
return { status: 404 };
|
|
163
|
+
}
|
|
164
|
+
const buffer = await res.buffer();
|
|
165
|
+
await fse.ensureDir(path.dirname(localPath));
|
|
166
|
+
await fse.writeFile(localPath, buffer);
|
|
167
|
+
log.info(` ✓ ${daPath}`);
|
|
168
|
+
return { status: 200, daPath };
|
|
169
|
+
} catch (err) {
|
|
170
|
+
log.warn(` ✗ ${daPath}: ${err.message}`);
|
|
171
|
+
return { status: err.status || 500 };
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
CONTENT_IO_CONCURRENCY,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const downloaded = [];
|
|
178
|
+
let errors = 0;
|
|
179
|
+
for (const r of downloadResults) {
|
|
180
|
+
if (r.status === 200) {
|
|
181
|
+
downloaded.push(r.daPath);
|
|
182
|
+
} else if (r.status !== 404) {
|
|
183
|
+
errors += 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 7. Init git repo and commit as baseline
|
|
188
|
+
await git.init({ fs, dir: contentDir, defaultBranch: 'main' });
|
|
189
|
+
await fse.writeFile(path.join(contentDir, '.gitignore'), `${CONFIG_FILE}\n`);
|
|
190
|
+
for (const daPath of downloaded) {
|
|
191
|
+
// eslint-disable-next-line no-await-in-loop
|
|
192
|
+
await git.add({ fs, dir: contentDir, filepath: daPath.replace(/^\//, '') });
|
|
193
|
+
}
|
|
194
|
+
await git.add({ fs, dir: contentDir, filepath: '.gitignore' });
|
|
195
|
+
await git.commit({
|
|
196
|
+
fs,
|
|
197
|
+
dir: contentDir,
|
|
198
|
+
message: `clone: ${org}/${repo}${this._rootPath === '/' ? '' : ` (${this._rootPath})`}`,
|
|
199
|
+
author: GIT_AUTHOR,
|
|
200
|
+
});
|
|
201
|
+
const headOid = await git.resolveRef({ fs, dir: contentDir, ref: 'HEAD' });
|
|
202
|
+
await writeSyncedRef(fs, contentDir, headOid);
|
|
203
|
+
|
|
204
|
+
// 8. Write config (not tracked by git)
|
|
205
|
+
await fse.writeJson(path.join(contentDir, CONFIG_FILE), {
|
|
206
|
+
org,
|
|
207
|
+
repo,
|
|
208
|
+
rootPath: this._rootPath,
|
|
209
|
+
}, { spaces: 2 });
|
|
210
|
+
|
|
211
|
+
log.info(`\nDone. ${downloaded.length} file(s) downloaded${errors > 0 ? `, ${errors} error(s)` : ''}.`);
|
|
212
|
+
log.info(`Content saved to ./${CONTENT_DIR}/`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
import { normalizeDaPath, LARGE_CLONE_FILE_THRESHOLD } from './content-shared.js';
|
|
14
|
+
|
|
15
|
+
export default function clone() {
|
|
16
|
+
let executor;
|
|
17
|
+
return {
|
|
18
|
+
set executor(value) {
|
|
19
|
+
executor = value;
|
|
20
|
+
},
|
|
21
|
+
command: 'clone',
|
|
22
|
+
description: 'Clone da.live content locally into content/',
|
|
23
|
+
builder: (yargs) => {
|
|
24
|
+
yargs
|
|
25
|
+
.option('path', {
|
|
26
|
+
describe: 'da.live folder to clone (e.g. /ca/fr_ca). Omit only when using --all.',
|
|
27
|
+
type: 'string',
|
|
28
|
+
})
|
|
29
|
+
.option('all', {
|
|
30
|
+
describe: 'Clone the entire site content (large). Use instead of --path.',
|
|
31
|
+
type: 'boolean',
|
|
32
|
+
default: false,
|
|
33
|
+
})
|
|
34
|
+
.option('token', {
|
|
35
|
+
describe: 'IMS Bearer token for da.live authentication',
|
|
36
|
+
type: 'string',
|
|
37
|
+
})
|
|
38
|
+
.option('force', {
|
|
39
|
+
describe: 'Overwrite existing content/ without prompting',
|
|
40
|
+
type: 'boolean',
|
|
41
|
+
default: false,
|
|
42
|
+
})
|
|
43
|
+
.option('yes', {
|
|
44
|
+
alias: 'y',
|
|
45
|
+
describe: `Proceed without prompting when the clone has more than ${LARGE_CLONE_FILE_THRESHOLD.toLocaleString()} files`,
|
|
46
|
+
type: 'boolean',
|
|
47
|
+
default: false,
|
|
48
|
+
})
|
|
49
|
+
.check((argv) => {
|
|
50
|
+
if (argv.all && argv.path !== undefined && argv.path !== '') {
|
|
51
|
+
return 'Do not use --path together with --all.';
|
|
52
|
+
}
|
|
53
|
+
if (!argv.all && (argv.path === undefined || argv.path === '')) {
|
|
54
|
+
return 'Missing --path. Example: aem content clone --path /ca/fr_ca. Use --all to clone all the content.';
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
})
|
|
58
|
+
.help();
|
|
59
|
+
},
|
|
60
|
+
handler: async (argv) => {
|
|
61
|
+
if (!executor) {
|
|
62
|
+
const CloneCommand = (await import('./clone.cmd.js')).default;
|
|
63
|
+
executor = new CloneCommand(getOrCreateLogger(argv));
|
|
64
|
+
}
|
|
65
|
+
const rootPath = argv.all ? '/' : normalizeDaPath(argv.path);
|
|
66
|
+
await executor
|
|
67
|
+
.withToken(argv.token)
|
|
68
|
+
.withForce(argv.force)
|
|
69
|
+
.withAssumeYes(argv.yes)
|
|
70
|
+
.withRootPath(rootPath)
|
|
71
|
+
.run();
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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, GIT_AUTHOR } from './content-shared.js';
|
|
17
|
+
|
|
18
|
+
export default class CommitCommand {
|
|
19
|
+
constructor(logger) {
|
|
20
|
+
this.log = logger;
|
|
21
|
+
this._dir = process.cwd();
|
|
22
|
+
this._message = '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
withDirectory(dir) {
|
|
26
|
+
this._dir = dir;
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
withMessage(message) {
|
|
31
|
+
this._message = message || '';
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async run() {
|
|
36
|
+
const { log } = this;
|
|
37
|
+
const contentDir = path.resolve(this._dir, CONTENT_DIR);
|
|
38
|
+
const configPath = path.join(contentDir, CONFIG_FILE);
|
|
39
|
+
|
|
40
|
+
if (!await fse.pathExists(configPath)) {
|
|
41
|
+
throw new Error(`No config found at ${configPath}. Run 'aem content clone' first.`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const msg = String(this._message).trim();
|
|
45
|
+
if (!msg) {
|
|
46
|
+
throw new Error('Commit message is required. Use -m "your message".');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const oid = await git.commit({
|
|
50
|
+
fs,
|
|
51
|
+
dir: contentDir,
|
|
52
|
+
message: msg,
|
|
53
|
+
author: GIT_AUTHOR,
|
|
54
|
+
});
|
|
55
|
+
log.info(`Committed ${oid.slice(0, 7)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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 commit() {
|
|
15
|
+
let executor;
|
|
16
|
+
return {
|
|
17
|
+
set executor(value) {
|
|
18
|
+
executor = value;
|
|
19
|
+
},
|
|
20
|
+
command: 'commit',
|
|
21
|
+
description: 'Commit staged changes in content/ (like git commit)',
|
|
22
|
+
builder: (yargs) => {
|
|
23
|
+
yargs
|
|
24
|
+
.option('m', {
|
|
25
|
+
alias: 'message',
|
|
26
|
+
describe: 'Commit message',
|
|
27
|
+
type: 'string',
|
|
28
|
+
demandOption: true,
|
|
29
|
+
})
|
|
30
|
+
.help();
|
|
31
|
+
},
|
|
32
|
+
handler: async (argv) => {
|
|
33
|
+
if (!executor) {
|
|
34
|
+
const CommitCommand = (await import('./commit.cmd.js')).default;
|
|
35
|
+
executor = new CommitCommand(getOrCreateLogger(argv));
|
|
36
|
+
}
|
|
37
|
+
await executor.withMessage(argv.m).run();
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|