@adobe/aem-cli 16.11.3 → 16.12.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,30 @@
1
+ # [16.12.0](https://github.com/adobe/helix-cli/compare/v16.11.3...v16.12.0) (2025-09-16)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * address PR review comments ([a14cabc](https://github.com/adobe/helix-cli/commit/a14cabc8aaa21525ded5bd9b4c021baf87da07f3))
7
+ * address PR review comments for git worktree support ([4532cac](https://github.com/adobe/helix-cli/commit/4532cac0327e32b3aca045aa99e4279ba7b15d26)), closes [#2583](https://github.com/adobe/helix-cli/issues/2583)
8
+ * comprehensive test setup improvements for git submodules and worktrees ([2715b95](https://github.com/adobe/helix-cli/commit/2715b9578831544ed0b42e93fdd87b2922f6d953))
9
+ * ensure test directories exist before running git init ([c29542c](https://github.com/adobe/helix-cli/commit/c29542c5cd6c17e7a5e2691d7d4189c07a9d6914))
10
+ * handle case where initGit is called from within target directory ([ad4ccde](https://github.com/adobe/helix-cli/commit/ad4ccde34e97afafdeac0c51b3c8e3ddefee7e3f))
11
+ * handle Windows file locking in git worktree tests ([b271662](https://github.com/adobe/helix-cli/commit/b2716624e3fd28031d01b45cf86556d22e4e0f9f))
12
+ * improve initGit error handling and ensure directory creation ([76514d0](https://github.com/adobe/helix-cli/commit/76514d088bc21bc51a379d292ca42bdde280d184))
13
+ * increase Windows CI test timeout to 60 seconds ([149176a](https://github.com/adobe/helix-cli/commit/149176a2b642aee4efcb6e2d1de0a188cc789e12))
14
+ * inline git init to avoid directory change issues ([72fe216](https://github.com/adobe/helix-cli/commit/72fe216f1355369eb2d82bc4356aa4a3aef07cb4))
15
+ * prevent worktree name collisions in tests ([73b3cbf](https://github.com/adobe/helix-cli/commit/73b3cbfffcd6693163193f2031a344a2d2361d67))
16
+ * properly configure Windows CI tests with increased timeout ([59f3a61](https://github.com/adobe/helix-cli/commit/59f3a6175af1c236c34ad1a54639cf7700fbd230))
17
+ * resolve linting errors in test file ([ed31cca](https://github.com/adobe/helix-cli/commit/ed31cca0f82df047dca5e17b6ad6cba38f32f4c2))
18
+ * revert to using initGit helper for consistent test setup ([b517cb3](https://github.com/adobe/helix-cli/commit/b517cb3e2487b180b4b3cdaf2e3949c3984e461f))
19
+ * split statements to separate lines for linting ([068d91c](https://github.com/adobe/helix-cli/commit/068d91cfc36ef1cc6353ae9ad17b05de6e119734))
20
+ * split test runs to identify hanging test ([8a7dd86](https://github.com/adobe/helix-cli/commit/8a7dd862ca4024a25daac0b37105d9f468a113c3))
21
+ * use process.chdir instead of shell.cd for more reliable directory handling ([a31c327](https://github.com/adobe/helix-cli/commit/a31c327cb96add05d665ee9b00117d2be50150e4))
22
+
23
+
24
+ ### Features
25
+
26
+ * add git worktree support with dynamic port selection ([cb763bd](https://github.com/adobe/helix-cli/commit/cb763bd004344d2a5361d675c8028efa2279ae1a))
27
+
1
28
  ## [16.11.3](https://github.com/adobe/helix-cli/compare/v16.11.2...v16.11.3) (2025-09-13)
2
29
 
3
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/aem-cli",
3
- "version": "16.11.3",
3
+ "version": "16.12.0",
4
4
  "description": "AEM CLI",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -11,7 +11,7 @@
11
11
  "scripts": {
12
12
  "check": "npm run lint && npm run test",
13
13
  "test": "c8 mocha",
14
- "test-ci-win": "npx mocha --reporter xunit test --reporter-options output=junit/test.xml -t 5000",
14
+ "test-ci-win": "mocha --reporter spec test -t 300000 --exit",
15
15
  "lint": "eslint .",
16
16
  "lint:fix": "eslint --fix .",
17
17
  "semantic-release": "semantic-release",
package/src/git-utils.js CHANGED
@@ -144,6 +144,24 @@ export default class GitUtils {
144
144
  * @returns {Promise<string>} current branch or tag
145
145
  */
146
146
  static async getBranch(dir, fallback = this.DEFAULT_BRANCH) {
147
+ // For worktrees, we need to get the actual git directory
148
+ const gitDir = await this.getGitDirectory(dir);
149
+
150
+ // If it's a worktree, try to get the branch from the worktree HEAD file
151
+ if (await this.isGitWorktree(dir)) {
152
+ try {
153
+ // Read the HEAD file in the worktree's git directory
154
+ const headPath = path.join(gitDir, 'HEAD');
155
+ const headContent = await fse.readFile(headPath, 'utf-8');
156
+ const match = headContent.match(/^ref: refs\/heads\/(.+)$/m);
157
+ if (match) {
158
+ return match[1].trim();
159
+ }
160
+ } catch (e) {
161
+ // Fall through to regular processing
162
+ }
163
+ }
164
+
147
165
  // current commit sha
148
166
  const rev = await git.resolveRef({ fs, dir, ref: 'HEAD' });
149
167
  // reverse-lookup tag from commit sha
@@ -194,7 +212,20 @@ export default class GitUtils {
194
212
  */
195
213
  static async getOrigin(dir) {
196
214
  try {
197
- const rmt = (await git.listRemotes({ fs, dir })).find((entry) => entry.remote === 'origin');
215
+ // For worktrees, we need to use the main repository's config
216
+ let gitDir = dir;
217
+ if (await this.isGitWorktree(dir)) {
218
+ // Get the worktree's git directory
219
+ const worktreeGitDir = await this.getGitDirectory(dir);
220
+ // Extract the main repository path from the worktree git dir
221
+ // Worktree git dirs are like: /path/to/repo/.git/worktrees/worktree-name
222
+ const match = worktreeGitDir.match(/^(.+?)\/\.git\/worktrees\//);
223
+ if (match) {
224
+ [, gitDir] = match;
225
+ }
226
+ }
227
+
228
+ const rmt = (await git.listRemotes({ fs, dir: gitDir })).find((entry) => entry.remote === 'origin');
198
229
  return typeof rmt === 'object' ? rmt.url : '';
199
230
  } catch (e) {
200
231
  // don't fail if directory is not a git repository
@@ -264,4 +295,96 @@ export default class GitUtils {
264
295
  }))
265
296
  .then((obj) => obj.object);
266
297
  }
298
+
299
+ /**
300
+ * Checks if the given directory is a git worktree.
301
+ *
302
+ * @param {string} dir working tree directory path
303
+ * @returns {Promise<boolean>} `true` if the directory is a git worktree
304
+ */
305
+ static async isGitWorktree(dir) {
306
+ const gitPath = path.resolve(dir, '.git');
307
+ try {
308
+ // Read the file directly - if it's not a file, readFile will throw
309
+ const content = await fse.readFile(gitPath, 'utf-8');
310
+ return content.includes('/worktrees/');
311
+ } catch (e) {
312
+ // Either doesn't exist, is a directory, or can't be read
313
+ // ignore
314
+ }
315
+ return false;
316
+ }
317
+
318
+ /**
319
+ * Checks if the given directory is a git submodule.
320
+ *
321
+ * @param {string} dir working tree directory path
322
+ * @returns {Promise<boolean>} `true` if the directory is a git submodule
323
+ */
324
+ static async isGitSubmodule(dir) {
325
+ const gitPath = path.resolve(dir, '.git');
326
+ try {
327
+ // Read the file directly - if it's not a file, readFile will throw
328
+ const content = await fse.readFile(gitPath, 'utf-8');
329
+ // Submodules have relative paths to .git/modules
330
+ return content.includes('/.git/modules/') || content.includes('\\.git\\modules\\');
331
+ } catch (e) {
332
+ // Either doesn't exist, is a directory, or can't be read
333
+ // ignore
334
+ }
335
+ return false;
336
+ }
337
+
338
+ /**
339
+ * Gets the actual git directory, resolving through worktree/submodule redirection.
340
+ *
341
+ * @param {string} dir working tree directory path
342
+ * @returns {Promise<string>} path to the actual git directory
343
+ */
344
+ static async getGitDirectory(dir) {
345
+ const gitPath = path.resolve(dir, '.git');
346
+ try {
347
+ // Try to read as a file first (worktree/submodule case)
348
+ const content = await fse.readFile(gitPath, 'utf-8');
349
+ const match = content.match(/^gitdir: (.+)$/m);
350
+ if (match) {
351
+ const targetPath = match[1].trim();
352
+ // If it's a relative path, resolve it relative to the directory
353
+ if (!path.isAbsolute(targetPath)) {
354
+ return path.resolve(dir, targetPath);
355
+ }
356
+ return targetPath;
357
+ }
358
+ } catch (e) {
359
+ // If readFile fails, it's likely a directory - check if it exists
360
+ try {
361
+ const stat = await fse.lstat(gitPath);
362
+ if (stat.isDirectory()) {
363
+ return gitPath;
364
+ }
365
+ } catch (statErr) {
366
+ // ignore
367
+ }
368
+ }
369
+ return gitPath;
370
+ }
371
+
372
+ /**
373
+ * Generates a deterministic port number based on branch name.
374
+ *
375
+ * @param {string} branchName the branch name to hash
376
+ * @param {number} basePort base port number (default: 3000)
377
+ * @param {number} range range of ports to use (default: 1000)
378
+ * @returns {number} port number between basePort and basePort + range - 1
379
+ */
380
+ static hashBranchToPort(branchName, basePort = 3000, range = 1000) {
381
+ let hash = 0;
382
+ for (let i = 0; i < branchName.length; i += 1) {
383
+ // eslint-disable-next-line no-bitwise
384
+ hash = ((hash << 5) - hash) + branchName.charCodeAt(i);
385
+ // eslint-disable-next-line no-bitwise
386
+ hash &= hash; // Convert to 32bit integer
387
+ }
388
+ return basePort + (Math.abs(hash) % range);
389
+ }
267
390
  }
package/src/up.cmd.js CHANGED
@@ -64,7 +64,15 @@ export default class UpCommand extends AbstractServerCommand {
64
64
  try {
65
65
  const stat = await fse.lstat(path.resolve(this.directory, '.git'));
66
66
  if (stat.isFile()) {
67
- throw Error('git submodules are not supported.');
67
+ // Check if it's a submodule or a worktree
68
+ if (await GitUtils.isGitSubmodule(this.directory)) {
69
+ throw Error('git submodules are not supported.');
70
+ }
71
+ // Verify it's actually a worktree
72
+ if (!await GitUtils.isGitWorktree(this.directory)) {
73
+ throw Error('Unsupported git configuration: .git is a file but not a valid worktree or submodule.');
74
+ }
75
+ // It's a worktree - this is allowed
68
76
  }
69
77
  } catch (e) {
70
78
  if (e.code === 'ENOENT') {
@@ -73,6 +81,15 @@ export default class UpCommand extends AbstractServerCommand {
73
81
  throw e;
74
82
  }
75
83
 
84
+ // Check if we're in a worktree and need to adjust the port
85
+ const isWorktree = await GitUtils.isGitWorktree(this.directory);
86
+ if (isWorktree && this._httpPort === 3000) {
87
+ // Only adjust port if using default port
88
+ const branch = await GitUtils.getBranch(this.directory);
89
+ this._httpPort = GitUtils.hashBranchToPort(branch);
90
+ this.log.info(chalk`Git worktree detected. Using port {cyan ${this._httpPort}} for branch {cyan ${branch}}`);
91
+ }
92
+
76
93
  // init dev default file params
77
94
  this._project = new HelixProject()
78
95
  .withCwd(this.directory)
@@ -113,7 +130,7 @@ export default class UpCommand extends AbstractServerCommand {
113
130
 
114
131
  try {
115
132
  await this._project.init();
116
- this.watchGit();
133
+ await this.watchGit();
117
134
  } catch (e) {
118
135
  throw Error(`Unable to start AEM: ${e.message}`);
119
136
  }
@@ -155,10 +172,13 @@ export default class UpCommand extends AbstractServerCommand {
155
172
  /**
156
173
  * Watches the git repository for changes and restarts the server if necessary.
157
174
  */
158
- watchGit() {
175
+ async watchGit() {
159
176
  let timer = null;
160
177
 
161
- this._watcher = chokidar.watch(path.resolve(this._project.directory, '.git'), {
178
+ // Resolve the actual git directory for worktrees
179
+ const gitDir = await GitUtils.getGitDirectory(this._project.directory);
180
+
181
+ this._watcher = chokidar.watch(gitDir, {
162
182
  persistent: true,
163
183
  ignoreInitial: true,
164
184
  });