@automattic/jetpack-cli 1.0.3 → 1.1.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.
Files changed (2) hide show
  1. package/bin/jp.js +194 -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,142 @@ 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
+ const hooksDir = resolve( monorepoRoot, '.git/hooks' );
154
+
155
+ if ( ! fs.existsSync( hooksDir ) ) {
156
+ throw new Error( 'Git hooks directory not found. Is this a git repository?' );
157
+ }
158
+
159
+ console.log( chalk.blue( 'Setting up jp git hooks...' ) );
160
+
161
+ // Check if git is configured to use a custom hooks path (e.g., husky)
162
+ const hooksPathResult = spawnSync( 'git', [ 'config', 'core.hooksPath' ], {
163
+ cwd: monorepoRoot,
164
+ encoding: 'utf8',
165
+ } );
166
+
167
+ if ( hooksPathResult.stdout && hooksPathResult.stdout.trim() ) {
168
+ const currentHooksPath = hooksPathResult.stdout.trim();
169
+ console.log( chalk.yellow( ` Detected custom git hooks path: ${ currentHooksPath }` ) );
170
+ console.log( chalk.yellow( ' Resetting to use .git/hooks/ for jp hooks' ) );
171
+
172
+ const unsetResult = spawnSync( 'git', [ 'config', '--unset', 'core.hooksPath' ], {
173
+ cwd: monorepoRoot,
174
+ } );
175
+
176
+ if ( unsetResult.status !== 0 ) {
177
+ throw new Error( 'Failed to unset core.hooksPath git configuration' );
178
+ }
179
+ }
180
+
181
+ const hooks = getHuskyHooks( monorepoRoot );
182
+ if ( hooks.length === 0 ) {
183
+ console.log( chalk.yellow( ' No hooks found in .husky/' ) );
184
+ return;
185
+ }
186
+
187
+ for ( const hookName of hooks ) {
188
+ const hookPath = resolve( hooksDir, hookName );
189
+ const hookContent = `#!/bin/sh
190
+ # Jetpack CLI git hook
191
+ # Runs the .husky hook in Docker to ensure consistent environment
192
+
193
+ # Exit gracefully if the .husky hook was removed
194
+ if [ ! -f .husky/${ hookName } ]; then
195
+ exit 0
196
+ fi
197
+
198
+ # Check if we're already in the Docker container
199
+ if [ -n "$JETPACK_MONOREPO_ENV" ]; then
200
+ echo "✓ Using jp hooks (running in Docker)"
201
+ sh .husky/${ hookName } "$@"
202
+ exit $?
203
+ fi
204
+
205
+ # Not in Docker - delegate to jp to run in Docker
206
+ echo "✓ Using jp hooks (delegating to Docker)"
207
+ jp git-hook ${ hookName } "$@"
208
+ exit $?
209
+ `;
210
+
211
+ fs.writeFileSync( hookPath, hookContent, { mode: 0o755 } );
212
+ console.log( chalk.green( ` Created ${ hookName } hook` ) );
213
+ }
214
+
215
+ console.log(
216
+ chalk.green( '\n✓ Git hooks installed! Hooks will run automatically in Docker.\n' )
217
+ );
218
+ };
219
+
220
+ /**
221
+ * Run a git hook inside the Docker container.
222
+ *
223
+ * @param {string} monorepoRoot - Path to the monorepo root
224
+ * @param {string} hookName - Name of the hook to run
225
+ * @param {Array} hookArgs - Arguments to pass to the hook
226
+ * @throws {Error} If hook execution fails
227
+ */
228
+ const runGitHook = ( monorepoRoot, hookName, hookArgs ) => {
229
+ console.log( chalk.blue( `Running ${ hookName } hook in Docker...` ) );
230
+
231
+ if ( ! huskyHookExists( monorepoRoot, hookName ) ) {
232
+ throw new Error( `Unknown git hook: ${ hookName }` );
233
+ }
234
+
235
+ // Run the .husky hook directly through the monorepo script
236
+ // TTY detection is handled by the monorepo script itself (reconnects to /dev/tty if available)
237
+ const result = spawnSync(
238
+ resolve( monorepoRoot, 'tools/docker/bin/monorepo' ),
239
+ [ 'sh', `.husky/${ hookName }`, ...hookArgs ],
240
+ {
241
+ stdio: 'inherit',
242
+ cwd: monorepoRoot,
243
+ }
244
+ );
245
+
246
+ if ( result.status !== 0 ) {
247
+ throw new Error( `Git hook ${ hookName } failed with status ${ result.status }` );
248
+ }
249
+ };
250
+
84
251
  /**
85
252
  * Initialize a new Jetpack development environment.
86
253
  *
@@ -109,6 +276,9 @@ const initJetpack = async () => {
109
276
 
110
277
  console.log( chalk.green( '\nJetpack monorepo has been cloned successfully!' ) );
111
278
 
279
+ // Initialize git hooks
280
+ initHooks( targetDir );
281
+
112
282
  console.log( '\nNext steps:' );
113
283
 
114
284
  console.log( '1. cd', response.directory );
@@ -128,7 +298,7 @@ const main = async () => {
128
298
 
129
299
  // Handle version flag
130
300
  if ( args[ 0 ] === '--version' || args[ 0 ] === '-v' ) {
131
- console.log( chalk.green( packageJson.version ) );
301
+ console.log( chalk.green( displayVersion ) );
132
302
  return;
133
303
  }
134
304
 
@@ -154,6 +324,27 @@ const main = async () => {
154
324
  throw new Error( 'Monorepo not found' );
155
325
  }
156
326
 
327
+ // Handle 'init-hooks' command
328
+ if ( args[ 0 ] === 'init-hooks' ) {
329
+ initHooks( monorepoRoot );
330
+ return;
331
+ }
332
+
333
+ // Handle 'git-hook' command
334
+ if ( args[ 0 ] === 'git-hook' ) {
335
+ const hookName = args[ 1 ];
336
+ const hookArgs = args.slice( 2 );
337
+
338
+ if ( ! hookName ) {
339
+ console.error( chalk.red( 'Error: git-hook command requires a hook name' ) );
340
+ console.log( 'Usage: jp git-hook <hook-name> [args...]' );
341
+ throw new Error( 'Missing hook name' );
342
+ }
343
+
344
+ runGitHook( monorepoRoot, hookName, hookArgs );
345
+ return;
346
+ }
347
+
157
348
  // Handle docker commands that must run on the host machine
158
349
  if ( args[ 0 ] === 'docker' ) {
159
350
  const hostCommands = [ 'up', 'down', 'stop', 'clean' ];
@@ -312,8 +503,7 @@ const main = async () => {
312
503
  [ 'pnpm', 'jetpack', ...args ],
313
504
  {
314
505
  stdio: 'inherit',
315
- shell: true,
316
- cwd: monorepoRoot, // Ensure we're in the monorepo root when running commands
506
+ cwd: monorepoRoot,
317
507
  }
318
508
  );
319
509
 
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.0",
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": {