@automattic/jetpack-cli 1.0.3 → 1.1.1

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.
Files changed (2) hide show
  1. package/bin/jp.js +206 -4
  2. package/package.json +1 -1
package/bin/jp.js CHANGED
@@ -6,7 +6,7 @@ import { dirname, resolve } from 'path';
6
6
  import process from 'process';
7
7
  import { fileURLToPath } from 'url';
8
8
  import chalk from 'chalk';
9
- import dotenv from 'dotenv';
9
+ import * as dotenv from 'dotenv';
10
10
  import prompts from 'prompts';
11
11
  import updateNotifier from 'update-notifier';
12
12
 
@@ -42,6 +42,37 @@ const isMonorepoRoot = dir => {
42
42
  }
43
43
  };
44
44
 
45
+ /**
46
+ * Check if the CLI is running from monorepo source (vs npm installed).
47
+ *
48
+ * @return {boolean} True if running from source
49
+ */
50
+ const isRunningFromSource = () => {
51
+ let dir = __dirname;
52
+ let prevDir;
53
+ while ( dir !== prevDir ) {
54
+ if ( isMonorepoRoot( dir ) ) {
55
+ return true;
56
+ }
57
+ prevDir = dir;
58
+ dir = dirname( dir );
59
+ }
60
+ return false;
61
+ };
62
+
63
+ /**
64
+ * Compute development version by incrementing patch number.
65
+ *
66
+ * @return {string} Development version string (e.g., "1.0.3-alpha" for released "1.0.2")
67
+ */
68
+ const computeDevVersion = () => {
69
+ const [ major, minor, patch ] = packageJson.version.split( '.' ).map( Number );
70
+ return `${ major }.${ minor }.${ patch + 1 }-alpha`;
71
+ };
72
+
73
+ // Version to display - dev version when running from source, package version otherwise
74
+ const displayVersion = isRunningFromSource() ? computeDevVersion() : packageJson.version;
75
+
45
76
  /**
46
77
  * Find monorepo root from a starting directory.
47
78
  *
@@ -81,6 +112,154 @@ const cloneMonorepo = async targetDir => {
81
112
  }
82
113
  };
83
114
 
115
+ /**
116
+ * Get list of git hooks from the .husky directory.
117
+ *
118
+ * @param {string} monorepoRoot - Path to the monorepo root
119
+ * @return {Array<string>} List of hook names
120
+ */
121
+ const getHuskyHooks = monorepoRoot => {
122
+ const huskyDir = resolve( monorepoRoot, '.husky' );
123
+ if ( ! fs.existsSync( huskyDir ) ) {
124
+ return [];
125
+ }
126
+
127
+ // Filter for valid git hook names (lowercase letters and hyphens)
128
+ const hookPattern = /^[a-z][a-z-]*$/;
129
+ return fs.readdirSync( huskyDir ).filter( name => {
130
+ const fullPath = resolve( huskyDir, name );
131
+ return hookPattern.test( name ) && fs.statSync( fullPath ).isFile();
132
+ } );
133
+ };
134
+
135
+ /**
136
+ * Check if a husky hook file exists.
137
+ *
138
+ * @param {string} monorepoRoot - Path to the monorepo root
139
+ * @param {string} hookName - Name of the hook
140
+ * @return {boolean} True if the hook exists
141
+ */
142
+ const huskyHookExists = ( monorepoRoot, hookName ) => {
143
+ return fs.existsSync( resolve( monorepoRoot, '.husky', hookName ) );
144
+ };
145
+
146
+ /**
147
+ * Initialize git hooks that work with Docker.
148
+ *
149
+ * @param {string} monorepoRoot - Path to the monorepo root
150
+ * @throws {Error} If hook installation fails
151
+ */
152
+ const initHooks = monorepoRoot => {
153
+ // Use git rev-parse --git-common-dir to find the hooks directory.
154
+ // In a regular repo this returns ".git"; in a worktree it returns the
155
+ // main repo's .git path. Hooks are shared across all worktrees.
156
+ const gitCommonDirResult = spawnSync( 'git', [ 'rev-parse', '--git-common-dir' ], {
157
+ cwd: monorepoRoot,
158
+ encoding: 'utf8',
159
+ } );
160
+
161
+ if ( gitCommonDirResult.status !== 0 || ! gitCommonDirResult.stdout.trim() ) {
162
+ throw new Error( 'Could not determine git directory. Is this a git repository?' );
163
+ }
164
+
165
+ const hooksDir = resolve( monorepoRoot, gitCommonDirResult.stdout.trim(), 'hooks' );
166
+
167
+ if ( ! fs.existsSync( hooksDir ) ) {
168
+ fs.mkdirSync( hooksDir, { recursive: true } );
169
+ }
170
+
171
+ console.log( chalk.blue( 'Setting up jp git hooks...' ) );
172
+
173
+ // Check if git is configured to use a custom hooks path (e.g., husky)
174
+ const hooksPathResult = spawnSync( 'git', [ 'config', 'core.hooksPath' ], {
175
+ cwd: monorepoRoot,
176
+ encoding: 'utf8',
177
+ } );
178
+
179
+ if ( hooksPathResult.stdout && hooksPathResult.stdout.trim() ) {
180
+ const currentHooksPath = hooksPathResult.stdout.trim();
181
+ console.log( chalk.yellow( ` Detected custom git hooks path: ${ currentHooksPath }` ) );
182
+ console.log( chalk.yellow( ` Resetting to use ${ hooksDir } for jp hooks` ) );
183
+
184
+ const unsetResult = spawnSync( 'git', [ 'config', '--unset', 'core.hooksPath' ], {
185
+ cwd: monorepoRoot,
186
+ } );
187
+
188
+ if ( unsetResult.status !== 0 ) {
189
+ throw new Error( 'Failed to unset core.hooksPath git configuration' );
190
+ }
191
+ }
192
+
193
+ const hooks = getHuskyHooks( monorepoRoot );
194
+ if ( hooks.length === 0 ) {
195
+ console.log( chalk.yellow( ' No hooks found in .husky/' ) );
196
+ return;
197
+ }
198
+
199
+ for ( const hookName of hooks ) {
200
+ const hookPath = resolve( hooksDir, hookName );
201
+ const hookContent = `#!/bin/sh
202
+ # Jetpack CLI git hook
203
+ # Runs the .husky hook in Docker to ensure consistent environment
204
+
205
+ # Exit gracefully if the .husky hook was removed
206
+ if [ ! -f .husky/${ hookName } ]; then
207
+ exit 0
208
+ fi
209
+
210
+ # Check if we're already in the Docker container
211
+ if [ -n "$JETPACK_MONOREPO_ENV" ]; then
212
+ echo "✓ Using jp hooks (running in Docker)"
213
+ sh .husky/${ hookName } "$@"
214
+ exit $?
215
+ fi
216
+
217
+ # Not in Docker - delegate to jp to run in Docker
218
+ echo "✓ Using jp hooks (delegating to Docker)"
219
+ jp git-hook ${ hookName } "$@"
220
+ exit $?
221
+ `;
222
+
223
+ fs.writeFileSync( hookPath, hookContent, { mode: 0o755 } );
224
+ console.log( chalk.green( ` Created ${ hookName } hook` ) );
225
+ }
226
+
227
+ console.log(
228
+ chalk.green( '\n✓ Git hooks installed! Hooks will run automatically in Docker.\n' )
229
+ );
230
+ };
231
+
232
+ /**
233
+ * Run a git hook inside the Docker container.
234
+ *
235
+ * @param {string} monorepoRoot - Path to the monorepo root
236
+ * @param {string} hookName - Name of the hook to run
237
+ * @param {Array} hookArgs - Arguments to pass to the hook
238
+ * @throws {Error} If hook execution fails
239
+ */
240
+ const runGitHook = ( monorepoRoot, hookName, hookArgs ) => {
241
+ console.log( chalk.blue( `Running ${ hookName } hook in Docker...` ) );
242
+
243
+ if ( ! huskyHookExists( monorepoRoot, hookName ) ) {
244
+ throw new Error( `Unknown git hook: ${ hookName }` );
245
+ }
246
+
247
+ // Run the .husky hook directly through the monorepo script
248
+ // TTY detection is handled by the monorepo script itself (reconnects to /dev/tty if available)
249
+ const result = spawnSync(
250
+ resolve( monorepoRoot, 'tools/docker/bin/monorepo' ),
251
+ [ 'sh', `.husky/${ hookName }`, ...hookArgs ],
252
+ {
253
+ stdio: 'inherit',
254
+ cwd: monorepoRoot,
255
+ }
256
+ );
257
+
258
+ if ( result.status !== 0 ) {
259
+ throw new Error( `Git hook ${ hookName } failed with status ${ result.status }` );
260
+ }
261
+ };
262
+
84
263
  /**
85
264
  * Initialize a new Jetpack development environment.
86
265
  *
@@ -109,6 +288,9 @@ const initJetpack = async () => {
109
288
 
110
289
  console.log( chalk.green( '\nJetpack monorepo has been cloned successfully!' ) );
111
290
 
291
+ // Initialize git hooks
292
+ initHooks( targetDir );
293
+
112
294
  console.log( '\nNext steps:' );
113
295
 
114
296
  console.log( '1. cd', response.directory );
@@ -128,7 +310,7 @@ const main = async () => {
128
310
 
129
311
  // Handle version flag
130
312
  if ( args[ 0 ] === '--version' || args[ 0 ] === '-v' ) {
131
- console.log( chalk.green( packageJson.version ) );
313
+ console.log( chalk.green( displayVersion ) );
132
314
  return;
133
315
  }
134
316
 
@@ -154,6 +336,27 @@ const main = async () => {
154
336
  throw new Error( 'Monorepo not found' );
155
337
  }
156
338
 
339
+ // Handle 'init-hooks' command
340
+ if ( args[ 0 ] === 'init-hooks' ) {
341
+ initHooks( monorepoRoot );
342
+ return;
343
+ }
344
+
345
+ // Handle 'git-hook' command
346
+ if ( args[ 0 ] === 'git-hook' ) {
347
+ const hookName = args[ 1 ];
348
+ const hookArgs = args.slice( 2 );
349
+
350
+ if ( ! hookName ) {
351
+ console.error( chalk.red( 'Error: git-hook command requires a hook name' ) );
352
+ console.log( 'Usage: jp git-hook <hook-name> [args...]' );
353
+ throw new Error( 'Missing hook name' );
354
+ }
355
+
356
+ runGitHook( monorepoRoot, hookName, hookArgs );
357
+ return;
358
+ }
359
+
157
360
  // Handle docker commands that must run on the host machine
158
361
  if ( args[ 0 ] === 'docker' ) {
159
362
  const hostCommands = [ 'up', 'down', 'stop', 'clean' ];
@@ -312,8 +515,7 @@ const main = async () => {
312
515
  [ 'pnpm', 'jetpack', ...args ],
313
516
  {
314
517
  stdio: 'inherit',
315
- shell: true,
316
- cwd: monorepoRoot, // Ensure we're in the monorepo root when running commands
518
+ cwd: monorepoRoot,
317
519
  }
318
520
  );
319
521
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/jetpack-cli",
3
- "version": "1.0.3",
3
+ "version": "1.1.1",
4
4
  "description": "Docker-based CLI for Jetpack development",
5
5
  "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/jetpack-cli/#readme",
6
6
  "bugs": {