glab-setup-git-identity 0.6.0 → 0.6.2

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,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 9f3ac16: Fix --version showing 'unknown' and fix auth flow for unauthenticated users
8
+ - Fix --version to explicitly read version from package.json (yargs auto-detection fails when installed globally with bun)
9
+ - Fix isGlabAuthenticated to detect invalid/missing tokens even when glab auth status exits with code 0
10
+ - Suppress noisy glab auth status output by using mirror: false with capture: true in command-stream
11
+ - Add mirror: false to all captured command executions to prevent output leaking to terminal
12
+
13
+ ## 0.6.1
14
+
15
+ ### Patch Changes
16
+
17
+ - 006215b: Fix Docker/server support and glab --jq compatibility
18
+ - Fix README.md manual commands to use pipe to jq instead of --jq flag
19
+ - Add "Authentication in Docker/Server Environments" section to README
20
+ - Enhance CLI with helpful headless auth instructions
21
+ - Fix src/index.js to parse JSON in JavaScript for better glab version compatibility
22
+ - Optimize getGitLabUserInfo to use a single API call
23
+
3
24
  ## 0.6.0
4
25
 
5
26
  ### Minor Changes
package/README.md CHANGED
@@ -13,16 +13,24 @@ A tool to setup git identity based on current GitLab user.
13
13
  Instead of manually running:
14
14
 
15
15
  ```bash
16
- glab auth login --hostname gitlab.com --git-protocol https
16
+ # Authenticate with GitLab (interactive mode - no extra flags)
17
+ glab auth login
18
+
19
+ # Or for non-interactive mode with a token:
20
+ # glab auth login --hostname gitlab.com --git-protocol https --token YOUR_TOKEN
21
+
17
22
  glab auth git-credential # For HTTPS authentication helper
18
23
 
19
- USERNAME=$(glab api user --jq '.username')
20
- EMAIL=$(glab api user --jq '.email')
24
+ # Get user info (requires jq to be installed)
25
+ USERNAME=$(glab api user | jq -r '.username')
26
+ EMAIL=$(glab api user | jq -r '.email')
21
27
 
22
28
  git config --global user.name "$USERNAME"
23
29
  git config --global user.email "$EMAIL"
24
30
  ```
25
31
 
32
+ > **Note for manual commands**: The commands above require `jq` to be installed (`apt install jq` or `brew install jq`). The `glab api` command does not have a built-in `--jq` flag - you must pipe output to the external `jq` tool.
33
+
26
34
  You can simply run:
27
35
 
28
36
  ```bash
@@ -163,7 +171,61 @@ The tool runs `glab auth login` automatically, followed by configuring git to us
163
171
  If automatic authentication fails, you can run the commands manually:
164
172
 
165
173
  ```bash
166
- glab auth login --hostname gitlab.com --git-protocol https
174
+ glab auth login
175
+ ```
176
+
177
+ ### Authentication in Docker/Server Environments (Headless)
178
+
179
+ When running in Docker containers or on remote servers without a browser, `glab auth login` will display a URL to open but fail to launch a browser:
180
+
181
+ ```
182
+ Failed opening a browser at https://gitlab.com/oauth/authorize?...
183
+ Encountered error: exec: "xdg-open": executable file not found in $PATH
184
+ Try entering the URL in your browser manually.
185
+ ```
186
+
187
+ **To complete authentication in headless environments:**
188
+
189
+ 1. **Copy the authorization URL** displayed by glab and open it in your local browser
190
+ 2. Complete the GitLab OAuth flow in your browser
191
+ 3. You'll be redirected to a URL like: `http://localhost:7171/auth/redirect?code=...&state=...`
192
+ 4. **Use `curl` to send the redirect URL back to glab**:
193
+
194
+ ```bash
195
+ # Method 1: Using screen (recommended for Docker)
196
+ # Terminal 1: Start glab auth in screen
197
+ screen -S glab-auth
198
+ glab auth login
199
+ # Press Ctrl+A, D to detach from screen
200
+
201
+ # Terminal 2: After completing OAuth in browser, send the redirect URL
202
+ curl -L "http://localhost:7171/auth/redirect?code=YOUR_CODE&state=YOUR_STATE"
203
+
204
+ # Return to screen to see auth completion
205
+ screen -r glab-auth
206
+ ```
207
+
208
+ ```bash
209
+ # Method 2: Using SSH port forwarding (for remote servers)
210
+ # On your local machine, forward the callback port:
211
+ ssh -L 7171:localhost:7171 user@remote-server
212
+
213
+ # Then on the server, run glab auth login
214
+ # The OAuth redirect will go through the SSH tunnel to your local machine
215
+ ```
216
+
217
+ **Alternatively, use token-based authentication** (recommended for CI/CD and automation):
218
+
219
+ ```bash
220
+ # Generate a Personal Access Token at:
221
+ # https://gitlab.com/-/profile/personal_access_tokens
222
+ # Required scopes: api, write_repository
223
+
224
+ # Then authenticate non-interactively:
225
+ glab auth login --hostname gitlab.com --token YOUR_TOKEN
226
+
227
+ # Or with this tool:
228
+ glab-setup-git-identity --token YOUR_TOKEN
167
229
  ```
168
230
 
169
231
  ### Successful Run
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glab-setup-git-identity",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "A tool to setup git identity based on current GitLab user",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/cli.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * Command-line interface for setting up git identity based on GitLab user
7
7
  */
8
8
 
9
+ import { createRequire } from 'node:module';
9
10
  import { makeConfig } from 'lino-arguments';
10
11
  import {
11
12
  setupGitIdentity,
@@ -17,6 +18,11 @@ import {
17
18
  } from './index.js';
18
19
  import { $ } from 'command-stream';
19
20
 
21
+ // Read version from package.json explicitly (yargs auto-detection may fail when installed globally)
22
+ const require = createRequire(import.meta.url);
23
+ const packageJson = require('../package.json');
24
+ const packageVersion = packageJson.version;
25
+
20
26
  // Parse command-line arguments with environment variable and .lenv support
21
27
  const config = makeConfig({
22
28
  yargs: ({ yargs, getenv }) =>
@@ -145,7 +151,7 @@ const config = makeConfig({
145
151
  )
146
152
  .help('h')
147
153
  .alias('h', 'help')
148
- .version()
154
+ .version(packageVersion)
149
155
  .strict(),
150
156
  });
151
157
 
@@ -260,6 +266,40 @@ async function handleAlreadyAuthenticated() {
260
266
  return true;
261
267
  }
262
268
 
269
+ /**
270
+ * Print headless authentication instructions
271
+ */
272
+ function printHeadlessAuthInstructions() {
273
+ console.log('');
274
+ console.log('=== Authentication in Docker/Server Environments ===');
275
+ console.log('');
276
+ console.log(
277
+ 'If you see a browser URL but cannot open it, follow these steps:'
278
+ );
279
+ console.log('');
280
+ console.log('1. Copy the authorization URL displayed above');
281
+ console.log('2. Open it in your local browser and complete the OAuth flow');
282
+ console.log(
283
+ '3. You will be redirected to: http://localhost:7171/auth/redirect?code=...&state=...'
284
+ );
285
+ console.log('4. Use curl to send that redirect URL back to glab:');
286
+ console.log('');
287
+ console.log(
288
+ ' curl -L "http://localhost:7171/auth/redirect?code=YOUR_CODE&state=YOUR_STATE"'
289
+ );
290
+ console.log('');
291
+ console.log('Alternatively, use token-based authentication:');
292
+ console.log('');
293
+ console.log('1. Generate a Personal Access Token at:');
294
+ console.log(` https://${config.hostname}/-/profile/personal_access_tokens`);
295
+ console.log(' Required scopes: api, write_repository');
296
+ console.log('');
297
+ console.log('2. Re-run with your token:');
298
+ console.log(` glab-setup-git-identity --token YOUR_TOKEN`);
299
+ console.log('');
300
+ console.log('================================================');
301
+ }
302
+
263
303
  /**
264
304
  * Handle case when not authenticated
265
305
  * @returns {Promise<boolean>} True if login succeeded
@@ -268,14 +308,27 @@ async function handleNotAuthenticated() {
268
308
  console.log('GitLab CLI is not authenticated. Starting authentication...');
269
309
  console.log('');
270
310
 
311
+ // Print headless instructions before attempting auth
312
+ // This helps users in Docker/server environments know what to do
313
+ printHeadlessAuthInstructions();
314
+ console.log('');
315
+
271
316
  const loginSuccess = await runGlabAuthLogin(getAuthOptions());
272
317
 
273
318
  if (!loginSuccess) {
274
319
  console.log('');
275
- console.log('Authentication failed. Please try running manually:');
320
+ console.log('Authentication failed. Please try one of the following:');
321
+ console.log('');
322
+ console.log('Option 1: Interactive login');
323
+ console.log(' glab auth login');
324
+ console.log('');
325
+ console.log('Option 2: Token-based login (recommended for headless)');
276
326
  console.log(
277
- ` glab auth login --hostname ${config.hostname} --git-protocol ${config.gitProtocol}`
327
+ ` glab auth login --hostname ${config.hostname} --token YOUR_TOKEN`
278
328
  );
329
+ console.log('');
330
+ console.log('Option 3: Use this tool with a token');
331
+ console.log(` glab-setup-git-identity --token YOUR_TOKEN`);
279
332
  return false;
280
333
  }
281
334
 
package/src/index.js CHANGED
@@ -62,7 +62,10 @@ export async function getGlabPath(options = {}) {
62
62
  const command = process.platform === 'win32' ? 'where' : 'which';
63
63
 
64
64
  try {
65
- const result = await $`${command} glab`.run({ capture: true });
65
+ const result = await $`${command} glab`.run({
66
+ capture: true,
67
+ mirror: false,
68
+ });
66
69
 
67
70
  if (result.code !== 0 || !result.stdout) {
68
71
  throw new Error(
@@ -238,7 +241,7 @@ export async function runGlabAuthSetupGit(options = {}) {
238
241
  try {
239
242
  const existingResult =
240
243
  await $`git config --global --get credential.${credentialUrl}.helper`.run(
241
- { capture: true }
244
+ { capture: true, mirror: false }
242
245
  );
243
246
 
244
247
  if (existingResult.code === 0 && existingResult.stdout && !force) {
@@ -262,6 +265,7 @@ export async function runGlabAuthSetupGit(options = {}) {
262
265
  try {
263
266
  await $`git config --global credential.${credentialUrl}.helper ""`.run({
264
267
  capture: true,
268
+ mirror: false,
265
269
  });
266
270
  } catch {
267
271
  // Ignore errors if not set
@@ -272,7 +276,7 @@ export async function runGlabAuthSetupGit(options = {}) {
272
276
 
273
277
  const result =
274
278
  await $`git config --global --add credential.${credentialUrl}.helper ${credentialHelper}`.run(
275
- { capture: true }
279
+ { capture: true, mirror: false }
276
280
  );
277
281
 
278
282
  if (result.code !== 0) {
@@ -312,13 +316,29 @@ export async function isGlabAuthenticated(options = {}) {
312
316
  }
313
317
 
314
318
  try {
315
- const result = await $`glab ${args}`.run({ capture: true });
319
+ const result = await $`glab ${args}`.run({
320
+ capture: true,
321
+ mirror: false,
322
+ });
316
323
 
317
324
  if (result.code !== 0) {
318
325
  log.debug(`GitLab CLI is not authenticated: ${result.stderr}`);
319
326
  return false;
320
327
  }
321
328
 
329
+ // glab auth status may exit with code 0 even when not properly authenticated.
330
+ // Check stderr for indicators of auth failure (e.g., "No token provided", "401 Unauthorized").
331
+ const output = (result.stderr || '') + (result.stdout || '');
332
+ if (
333
+ /no token provided/i.test(output) ||
334
+ /401\s*unauthorized/i.test(output)
335
+ ) {
336
+ log.debug(
337
+ `GitLab CLI reports success but token is missing or invalid: ${output.trim()}`
338
+ );
339
+ return false;
340
+ }
341
+
322
342
  log.debug('GitLab CLI is authenticated');
323
343
  return true;
324
344
  } catch (error) {
@@ -330,6 +350,9 @@ export async function isGlabAuthenticated(options = {}) {
330
350
  /**
331
351
  * Get GitLab username from authenticated user
332
352
  *
353
+ * Note: This function parses the JSON response in JavaScript rather than using
354
+ * glab's --jq flag, as the --jq flag is not available in all glab versions.
355
+ *
333
356
  * @param {Object} options - Options
334
357
  * @param {string} options.hostname - GitLab hostname (optional)
335
358
  * @param {boolean} options.verbose - Enable verbose logging
@@ -342,18 +365,34 @@ export async function getGitLabUsername(options = {}) {
342
365
 
343
366
  log.debug('Getting GitLab username...');
344
367
 
345
- const args = ['api', 'user', '--jq', '.username'];
368
+ const args = ['api', 'user'];
346
369
  if (hostname) {
347
370
  args.push('--hostname', hostname);
348
371
  }
349
372
 
350
- const result = await $`glab ${args}`.run({ capture: true });
373
+ const result = await $`glab ${args}`.run({ capture: true, mirror: false });
351
374
 
352
375
  if (result.code !== 0) {
353
376
  throw new Error(`Failed to get GitLab username: ${result.stderr}`);
354
377
  }
355
378
 
356
- const username = result.stdout.trim();
379
+ // Parse JSON response in JavaScript (glab's --jq flag is not available in all versions)
380
+ let userData;
381
+ try {
382
+ userData = JSON.parse(result.stdout.trim());
383
+ } catch (parseError) {
384
+ throw new Error(
385
+ `Failed to parse GitLab user data: ${parseError.message}. Raw output: ${result.stdout}`
386
+ );
387
+ }
388
+
389
+ const username = userData.username;
390
+ if (!username) {
391
+ throw new Error(
392
+ 'No username found in GitLab user data. Please ensure your GitLab account has a username.'
393
+ );
394
+ }
395
+
357
396
  log.debug(`GitLab username: ${username}`);
358
397
 
359
398
  return username;
@@ -362,6 +401,9 @@ export async function getGitLabUsername(options = {}) {
362
401
  /**
363
402
  * Get primary email from GitLab user
364
403
  *
404
+ * Note: This function parses the JSON response in JavaScript rather than using
405
+ * glab's --jq flag, as the --jq flag is not available in all glab versions.
406
+ *
365
407
  * @param {Object} options - Options
366
408
  * @param {string} options.hostname - GitLab hostname (optional)
367
409
  * @param {boolean} options.verbose - Enable verbose logging
@@ -374,18 +416,28 @@ export async function getGitLabEmail(options = {}) {
374
416
 
375
417
  log.debug('Getting GitLab primary email...');
376
418
 
377
- const args = ['api', 'user', '--jq', '.email'];
419
+ const args = ['api', 'user'];
378
420
  if (hostname) {
379
421
  args.push('--hostname', hostname);
380
422
  }
381
423
 
382
- const result = await $`glab ${args}`.run({ capture: true });
424
+ const result = await $`glab ${args}`.run({ capture: true, mirror: false });
383
425
 
384
426
  if (result.code !== 0) {
385
427
  throw new Error(`Failed to get GitLab email: ${result.stderr}`);
386
428
  }
387
429
 
388
- const email = result.stdout.trim();
430
+ // Parse JSON response in JavaScript (glab's --jq flag is not available in all versions)
431
+ let userData;
432
+ try {
433
+ userData = JSON.parse(result.stdout.trim());
434
+ } catch (parseError) {
435
+ throw new Error(
436
+ `Failed to parse GitLab user data: ${parseError.message}. Raw output: ${result.stdout}`
437
+ );
438
+ }
439
+
440
+ const email = userData.email;
389
441
 
390
442
  if (!email) {
391
443
  throw new Error(
@@ -401,6 +453,10 @@ export async function getGitLabEmail(options = {}) {
401
453
  /**
402
454
  * Get GitLab user information (username and primary email)
403
455
  *
456
+ * Note: This function makes a single API call and parses both username and email
457
+ * from the response, which is more efficient than calling getGitLabUsername and
458
+ * getGitLabEmail separately.
459
+ *
404
460
  * @param {Object} options - Options
405
461
  * @param {string} options.hostname - GitLab hostname (optional)
406
462
  * @param {boolean} options.verbose - Enable verbose logging
@@ -408,10 +464,49 @@ export async function getGitLabEmail(options = {}) {
408
464
  * @returns {Promise<{username: string, email: string}>} User information
409
465
  */
410
466
  export async function getGitLabUserInfo(options = {}) {
411
- const [username, email] = await Promise.all([
412
- getGitLabUsername(options),
413
- getGitLabEmail(options),
414
- ]);
467
+ const { hostname, verbose = false, logger = console } = options;
468
+ const log = createDefaultLogger({ verbose, logger });
469
+
470
+ log.debug('Getting GitLab user information...');
471
+
472
+ const args = ['api', 'user'];
473
+ if (hostname) {
474
+ args.push('--hostname', hostname);
475
+ }
476
+
477
+ const result = await $`glab ${args}`.run({ capture: true, mirror: false });
478
+
479
+ if (result.code !== 0) {
480
+ throw new Error(`Failed to get GitLab user info: ${result.stderr}`);
481
+ }
482
+
483
+ // Parse JSON response in JavaScript (glab's --jq flag is not available in all versions)
484
+ let userData;
485
+ try {
486
+ userData = JSON.parse(result.stdout.trim());
487
+ } catch (parseError) {
488
+ throw new Error(
489
+ `Failed to parse GitLab user data: ${parseError.message}. Raw output: ${result.stdout}`
490
+ );
491
+ }
492
+
493
+ const username = userData.username;
494
+ const email = userData.email;
495
+
496
+ if (!username) {
497
+ throw new Error(
498
+ 'No username found in GitLab user data. Please ensure your GitLab account has a username.'
499
+ );
500
+ }
501
+
502
+ if (!email) {
503
+ throw new Error(
504
+ 'No email found on GitLab account. Please set a primary email in your GitLab settings.'
505
+ );
506
+ }
507
+
508
+ log.debug(`GitLab username: ${username}`);
509
+ log.debug(`GitLab primary email: ${email}`);
415
510
 
416
511
  return { username, email };
417
512
  }
@@ -437,6 +532,7 @@ export async function setGitConfig(key, value, options = {}) {
437
532
 
438
533
  const result = await $`git config ${scopeFlag} ${key} ${value}`.run({
439
534
  capture: true,
535
+ mirror: false,
440
536
  });
441
537
 
442
538
  if (result.code !== 0) {
@@ -464,7 +560,10 @@ export async function getGitConfig(key, options = {}) {
464
560
 
465
561
  log.debug(`Getting git config ${key} (${scope})`);
466
562
 
467
- const result = await $`git config ${scopeFlag} ${key}`.run({ capture: true });
563
+ const result = await $`git config ${scopeFlag} ${key}`.run({
564
+ capture: true,
565
+ mirror: false,
566
+ });
468
567
 
469
568
  if (result.code !== 0) {
470
569
  log.debug(`Git config ${key} not set`);
@@ -4,15 +4,19 @@
4
4
  */
5
5
 
6
6
  import { describe, it, expect } from 'test-anywhere';
7
+ import { createRequire } from 'node:module';
7
8
  import {
8
9
  defaultAuthOptions,
9
10
  getGitConfig,
10
11
  setGitConfig,
11
12
  verifyGitIdentity,
12
13
  getGlabPath,
14
+ isGlabAuthenticated,
13
15
  runGlabAuthSetupGit,
14
16
  } from '../src/index.js';
15
17
 
18
+ const require = createRequire(import.meta.url);
19
+
16
20
  describe('defaultAuthOptions', () => {
17
21
  it('should have correct default hostname', () => {
18
22
  expect(defaultAuthOptions.hostname).toBe('gitlab.com');
@@ -131,7 +135,65 @@ describe('runGlabAuthSetupGit', () => {
131
135
  });
132
136
  });
133
137
 
134
- // Note: Tests for isGlabAuthenticated, getGitLabUsername, getGitLabEmail,
138
+ describe('CLI --version', () => {
139
+ it('should output the version from package.json', async () => {
140
+ const pkg = require('../package.json');
141
+ const { execSync } = await import('node:child_process');
142
+ const output = execSync('node src/cli.js --version', {
143
+ encoding: 'utf8',
144
+ }).trim();
145
+ expect(output).toBe(pkg.version);
146
+ });
147
+ });
148
+
149
+ describe('isGlabAuthenticated', () => {
150
+ it('should be a function', () => {
151
+ expect(typeof isGlabAuthenticated).toBe('function');
152
+ });
153
+
154
+ it('should return false when glab has no valid token', async () => {
155
+ // In this test environment, glab is not properly authenticated
156
+ // so isGlabAuthenticated should return false
157
+ try {
158
+ const result = await isGlabAuthenticated();
159
+ expect(typeof result).toBe('boolean');
160
+ } catch {
161
+ // If glab is not installed, it should handle gracefully
162
+ expect(true).toBe(true);
163
+ }
164
+ });
165
+
166
+ it('should not produce visible output when checking auth status', async () => {
167
+ // Ensure isGlabAuthenticated does not leak glab output to the console
168
+ const originalStdoutWrite = process.stdout.write;
169
+ const originalStderrWrite = process.stderr.write;
170
+ let capturedOutput = '';
171
+
172
+ process.stdout.write = (chunk) => {
173
+ capturedOutput += chunk.toString();
174
+ return true;
175
+ };
176
+ process.stderr.write = (chunk) => {
177
+ capturedOutput += chunk.toString();
178
+ return true;
179
+ };
180
+
181
+ try {
182
+ await isGlabAuthenticated({ verbose: false });
183
+ } catch {
184
+ // ignore errors
185
+ } finally {
186
+ process.stdout.write = originalStdoutWrite;
187
+ process.stderr.write = originalStderrWrite;
188
+ }
189
+
190
+ // Should not contain glab auth status output
191
+ expect(capturedOutput.includes('No token provided')).toBe(false);
192
+ expect(capturedOutput.includes('401 Unauthorized')).toBe(false);
193
+ });
194
+ });
195
+
196
+ // Note: Tests for getGitLabUsername, getGitLabEmail,
135
197
  // getGitLabUserInfo, runGlabAuthLogin, and setupGitIdentity require
136
198
  // an authenticated glab CLI environment and are better suited for
137
199
  // integration tests or manual testing.