@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 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 content --html-mount / # serves at /* (root)
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.17.1",
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
+ }