glab-setup-git-identity 0.6.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 (66) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/workflows/release.yml +372 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.jscpd.json +20 -0
  6. package/.prettierignore +7 -0
  7. package/.prettierrc +10 -0
  8. package/CHANGELOG.md +143 -0
  9. package/LICENSE +24 -0
  10. package/README.md +455 -0
  11. package/bunfig.toml +3 -0
  12. package/deno.json +7 -0
  13. package/docs/case-studies/issue-13/README.md +195 -0
  14. package/docs/case-studies/issue-13/hive-mind-issue-960.json +23 -0
  15. package/docs/case-studies/issue-13/hive-mind-pr-961-diff.txt +773 -0
  16. package/docs/case-studies/issue-13/hive-mind-pr-961.json +126 -0
  17. package/docs/case-studies/issue-21/README.md +384 -0
  18. package/docs/case-studies/issue-21/ci-logs/run-20803315337.txt +1188 -0
  19. package/docs/case-studies/issue-21/ci-logs/run-20885464993.txt +1310 -0
  20. package/docs/case-studies/issue-21/issue-111-data.txt +15 -0
  21. package/docs/case-studies/issue-21/issue-113-data.txt +15 -0
  22. package/docs/case-studies/issue-21/pr-112-data.json +109 -0
  23. package/docs/case-studies/issue-21/pr-112-diff.patch +1336 -0
  24. package/docs/case-studies/issue-21/pr-114-data.json +126 -0
  25. package/docs/case-studies/issue-21/pr-114-diff.patch +879 -0
  26. package/docs/case-studies/issue-3/README.md +338 -0
  27. package/docs/case-studies/issue-3/created-issues.md +32 -0
  28. package/docs/case-studies/issue-3/issue-data.json +29 -0
  29. package/docs/case-studies/issue-3/original-format-release-notes.mjs +212 -0
  30. package/docs/case-studies/issue-3/reference-pr-59-diff.txt +614 -0
  31. package/docs/case-studies/issue-3/reference-pr-59.json +109 -0
  32. package/docs/case-studies/issue-3/release-v0.1.0.json +9 -0
  33. package/docs/case-studies/issue-3/repositories-with-same-script.json +22 -0
  34. package/docs/case-studies/issue-3/research-notes.md +33 -0
  35. package/docs/case-studies/issue-7/BEST-PRACTICES-COMPARISON.md +334 -0
  36. package/docs/case-studies/issue-7/FORMATTER-COMPARISON.md +649 -0
  37. package/docs/case-studies/issue-7/current-repository-analysis.json +70 -0
  38. package/docs/case-studies/issue-7/effect-template-analysis.json +178 -0
  39. package/eslint.config.js +91 -0
  40. package/examples/basic-usage.js +64 -0
  41. package/experiments/test-changeset-scripts.mjs +303 -0
  42. package/experiments/test-failure-detection.mjs +143 -0
  43. package/experiments/test-format-major-changes.mjs +49 -0
  44. package/experiments/test-format-minor-changes.mjs +52 -0
  45. package/experiments/test-format-no-hash.mjs +43 -0
  46. package/experiments/test-format-patch-changes.mjs +46 -0
  47. package/package.json +80 -0
  48. package/scripts/changeset-version.mjs +75 -0
  49. package/scripts/check-changesets.mjs +67 -0
  50. package/scripts/check-version.mjs +129 -0
  51. package/scripts/create-github-release.mjs +93 -0
  52. package/scripts/create-manual-changeset.mjs +89 -0
  53. package/scripts/detect-code-changes.mjs +194 -0
  54. package/scripts/format-github-release.mjs +83 -0
  55. package/scripts/format-release-notes.mjs +219 -0
  56. package/scripts/instant-version-bump.mjs +172 -0
  57. package/scripts/js-paths.mjs +177 -0
  58. package/scripts/merge-changesets.mjs +263 -0
  59. package/scripts/publish-to-npm.mjs +302 -0
  60. package/scripts/setup-npm.mjs +37 -0
  61. package/scripts/validate-changeset.mjs +265 -0
  62. package/scripts/version-and-commit.mjs +284 -0
  63. package/src/cli.js +386 -0
  64. package/src/index.d.ts +255 -0
  65. package/src/index.js +563 -0
  66. package/tests/index.test.js +137 -0
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Version packages and commit to main
5
+ * Usage: node scripts/version-and-commit.mjs --mode <changeset|instant> [--bump-type <type>] [--description <desc>] [--js-root <path>]
6
+ * changeset: Run changeset version
7
+ * instant: Run instant version bump with bump_type (patch|minor|major) and optional description
8
+ *
9
+ * Configuration:
10
+ * - CLI: --js-root <path> to explicitly set JavaScript root
11
+ * - Environment: JS_ROOT=<path>
12
+ *
13
+ * Uses link-foundation libraries:
14
+ * - use-m: Dynamic package loading without package.json dependencies
15
+ * - command-stream: Modern shell command execution with streaming support
16
+ * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files
17
+ *
18
+ * Addresses issues documented in:
19
+ * - Issue #21: Supporting both single and multi-language repository structures
20
+ * - Reference: link-assistant/agent PR #112 (--legacy-peer-deps fix)
21
+ * - Reference: link-assistant/agent PR #114 (configurable package root)
22
+ */
23
+
24
+ import { readFileSync, appendFileSync, readdirSync } from 'fs';
25
+
26
+ import {
27
+ getJsRoot,
28
+ getPackageJsonPath,
29
+ getChangesetDir,
30
+ needsCd,
31
+ parseJsRootConfig,
32
+ } from './js-paths.mjs';
33
+
34
+ // Load use-m dynamically
35
+ const { use } = eval(
36
+ await (await fetch('https://unpkg.com/use-m/use.js')).text()
37
+ );
38
+
39
+ // Import link-foundation libraries
40
+ const { $ } = await use('command-stream');
41
+ const { makeConfig } = await use('lino-arguments');
42
+
43
+ // Parse CLI arguments using lino-arguments
44
+ const config = makeConfig({
45
+ yargs: ({ yargs, getenv }) =>
46
+ yargs
47
+ .option('mode', {
48
+ type: 'string',
49
+ default: getenv('MODE', 'changeset'),
50
+ describe: 'Version mode: changeset or instant',
51
+ choices: ['changeset', 'instant'],
52
+ })
53
+ .option('bump-type', {
54
+ type: 'string',
55
+ default: getenv('BUMP_TYPE', ''),
56
+ describe: 'Version bump type for instant mode: major, minor, or patch',
57
+ })
58
+ .option('description', {
59
+ type: 'string',
60
+ default: getenv('DESCRIPTION', ''),
61
+ describe: 'Description for instant version bump',
62
+ })
63
+ .option('js-root', {
64
+ type: 'string',
65
+ default: getenv('JS_ROOT', ''),
66
+ describe:
67
+ 'JavaScript package root directory (auto-detected if not specified)',
68
+ }),
69
+ });
70
+
71
+ const { mode, bumpType, description, jsRoot: jsRootArg } = config;
72
+
73
+ // Get JavaScript package root (auto-detect or use explicit config)
74
+ const jsRootConfig = jsRootArg || parseJsRootConfig();
75
+ const jsRoot = getJsRoot({ jsRoot: jsRootConfig, verbose: true });
76
+
77
+ // Debug: Log parsed configuration
78
+ console.log('Parsed configuration:', {
79
+ mode,
80
+ bumpType,
81
+ description: description || '(none)',
82
+ jsRoot,
83
+ });
84
+
85
+ // Detect if positional arguments were used (common mistake)
86
+ const args = process.argv.slice(2);
87
+ if (args.length > 0 && !args[0].startsWith('--')) {
88
+ console.error('Error: Positional arguments detected!');
89
+ console.error('Command line arguments:', args);
90
+ console.error('');
91
+ console.error(
92
+ 'This script requires named arguments (--mode, --bump-type, --description, --js-root).'
93
+ );
94
+ console.error('Usage:');
95
+ console.error(' Changeset mode:');
96
+ console.error(
97
+ ' node scripts/version-and-commit.mjs --mode changeset [--js-root <path>]'
98
+ );
99
+ console.error(' Instant mode:');
100
+ console.error(
101
+ ' node scripts/version-and-commit.mjs --mode instant --bump-type <major|minor|patch> [--description <desc>] [--js-root <path>]'
102
+ );
103
+ console.error('');
104
+ console.error('Examples:');
105
+ console.error(
106
+ ' node scripts/version-and-commit.mjs --mode instant --bump-type patch --description "Fix bug"'
107
+ );
108
+ console.error(' node scripts/version-and-commit.mjs --mode changeset');
109
+ console.error(
110
+ ' node scripts/version-and-commit.mjs --mode changeset --js-root js'
111
+ );
112
+ process.exit(1);
113
+ }
114
+
115
+ // Validation: Ensure mode is set correctly
116
+ if (mode !== 'changeset' && mode !== 'instant') {
117
+ console.error(`Invalid mode: "${mode}". Expected "changeset" or "instant".`);
118
+ console.error('Command line arguments:', process.argv.slice(2));
119
+ process.exit(1);
120
+ }
121
+
122
+ // Validation: Ensure bump type is provided for instant mode
123
+ if (mode === 'instant' && !bumpType) {
124
+ console.error('Error: --bump-type is required for instant mode');
125
+ console.error(
126
+ 'Usage: node scripts/version-and-commit.mjs --mode instant --bump-type <major|minor|patch> [--description <desc>] [--js-root <path>]'
127
+ );
128
+ process.exit(1);
129
+ }
130
+
131
+ // Store the original working directory to restore after cd commands
132
+ // IMPORTANT: command-stream's cd is a virtual command that calls process.chdir()
133
+ const originalCwd = process.cwd();
134
+
135
+ /**
136
+ * Append to GitHub Actions output file
137
+ * @param {string} key
138
+ * @param {string} value
139
+ */
140
+ function setOutput(key, value) {
141
+ const outputFile = process.env.GITHUB_OUTPUT;
142
+ if (outputFile) {
143
+ appendFileSync(outputFile, `${key}=${value}\n`);
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Count changeset files (excluding README.md)
149
+ */
150
+ function countChangesets() {
151
+ try {
152
+ const changesetDir = getChangesetDir({ jsRoot });
153
+ const files = readdirSync(changesetDir);
154
+ return files.filter((f) => f.endsWith('.md') && f !== 'README.md').length;
155
+ } catch {
156
+ return 0;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Get package version
162
+ * @param {string} source - 'local' or 'remote'
163
+ */
164
+ async function getVersion(source = 'local') {
165
+ const packageJsonPath = getPackageJsonPath({ jsRoot });
166
+ if (source === 'remote') {
167
+ const result = await $`git show origin/main:${packageJsonPath}`.run({
168
+ capture: true,
169
+ });
170
+ return JSON.parse(result.stdout).version;
171
+ }
172
+ return JSON.parse(readFileSync(packageJsonPath, 'utf8')).version;
173
+ }
174
+
175
+ async function main() {
176
+ try {
177
+ // Configure git
178
+ await $`git config user.name "github-actions[bot]"`;
179
+ await $`git config user.email "github-actions[bot]@users.noreply.github.com"`;
180
+
181
+ // Check if remote main has advanced (handles re-runs after partial success)
182
+ console.log('Checking for remote changes...');
183
+ await $`git fetch origin main`;
184
+
185
+ const localHeadResult = await $`git rev-parse HEAD`.run({ capture: true });
186
+ const localHead = localHeadResult.stdout.trim();
187
+
188
+ const remoteHeadResult = await $`git rev-parse origin/main`.run({
189
+ capture: true,
190
+ });
191
+ const remoteHead = remoteHeadResult.stdout.trim();
192
+
193
+ if (localHead !== remoteHead) {
194
+ console.log(
195
+ `Remote main has advanced (local: ${localHead}, remote: ${remoteHead})`
196
+ );
197
+ console.log('This may indicate a previous attempt partially succeeded.');
198
+
199
+ // Check if the remote version is already the expected bump
200
+ const remoteVersion = await getVersion('remote');
201
+ console.log(`Remote version: ${remoteVersion}`);
202
+
203
+ // Check if there are changesets to process
204
+ const changesetCount = countChangesets();
205
+
206
+ if (changesetCount === 0) {
207
+ console.log('No changesets to process and remote has advanced.');
208
+ console.log(
209
+ 'Assuming version bump was already completed in a previous attempt.'
210
+ );
211
+ setOutput('version_committed', 'false');
212
+ setOutput('already_released', 'true');
213
+ setOutput('new_version', remoteVersion);
214
+ return;
215
+ } else {
216
+ console.log('Rebasing on remote main to incorporate changes...');
217
+ await $`git rebase origin/main`;
218
+ }
219
+ }
220
+
221
+ // Get current version before bump
222
+ const oldVersion = await getVersion();
223
+ console.log(`Current version: ${oldVersion}`);
224
+
225
+ if (mode === 'instant') {
226
+ console.log('Running instant version bump...');
227
+ // Run instant version bump script
228
+ // Pass --js-root to ensure consistent path handling
229
+ // Rely on command-stream's auto-quoting for proper argument handling
230
+ if (description) {
231
+ await $`node scripts/instant-version-bump.mjs --bump-type ${bumpType} --description ${description} --js-root ${jsRoot}`;
232
+ } else {
233
+ await $`node scripts/instant-version-bump.mjs --bump-type ${bumpType} --js-root ${jsRoot}`;
234
+ }
235
+ } else {
236
+ console.log('Running changeset version...');
237
+ // Run changeset version to bump versions and update CHANGELOG
238
+ // IMPORTANT: cd is a virtual command that calls process.chdir(), so we restore after
239
+ if (needsCd({ jsRoot })) {
240
+ await $`cd ${jsRoot} && npm run changeset:version`;
241
+ process.chdir(originalCwd);
242
+ } else {
243
+ await $`npm run changeset:version`;
244
+ }
245
+ }
246
+
247
+ // Get new version after bump
248
+ const newVersion = await getVersion();
249
+ console.log(`New version: ${newVersion}`);
250
+ setOutput('new_version', newVersion);
251
+
252
+ // Check if there are changes to commit
253
+ const statusResult = await $`git status --porcelain`.run({ capture: true });
254
+ const status = statusResult.stdout.trim();
255
+
256
+ if (status) {
257
+ console.log('Changes detected, committing...');
258
+
259
+ // Stage all changes (package.json, package-lock.json, CHANGELOG.md, deleted changesets)
260
+ await $`git add -A`;
261
+
262
+ // Commit with version number as message
263
+ const commitMessage = newVersion;
264
+ const escapedMessage = commitMessage.replace(/"/g, '\\"');
265
+ await $`git commit -m "${escapedMessage}"`;
266
+
267
+ // Push directly to main
268
+ await $`git push origin main`;
269
+
270
+ console.log('\u2705 Version bump committed and pushed to main');
271
+ setOutput('version_committed', 'true');
272
+ } else {
273
+ console.log('No changes to commit');
274
+ setOutput('version_committed', 'false');
275
+ }
276
+ } catch (error) {
277
+ // Restore cwd on error
278
+ process.chdir(originalCwd);
279
+ console.error('Error:', error.message);
280
+ process.exit(1);
281
+ }
282
+ }
283
+
284
+ main();
package/src/cli.js ADDED
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * glab-setup-git-identity CLI
5
+ *
6
+ * Command-line interface for setting up git identity based on GitLab user
7
+ */
8
+
9
+ import { makeConfig } from 'lino-arguments';
10
+ import {
11
+ setupGitIdentity,
12
+ isGlabAuthenticated,
13
+ runGlabAuthLogin,
14
+ runGlabAuthSetupGit,
15
+ verifyGitIdentity,
16
+ defaultAuthOptions,
17
+ } from './index.js';
18
+ import { $ } from 'command-stream';
19
+
20
+ // Parse command-line arguments with environment variable and .lenv support
21
+ const config = makeConfig({
22
+ yargs: ({ yargs, getenv }) =>
23
+ yargs
24
+ .usage('Usage: $0 [options]')
25
+ // Git identity options
26
+ .option('global', {
27
+ alias: 'g',
28
+ type: 'boolean',
29
+ description: 'Set git config globally (default)',
30
+ default: false,
31
+ })
32
+ .option('local', {
33
+ alias: 'l',
34
+ type: 'boolean',
35
+ description: 'Set git config locally (in current repository)',
36
+ default: getenv('GLAB_SETUP_GIT_IDENTITY_LOCAL', false),
37
+ })
38
+ .option('verbose', {
39
+ alias: 'v',
40
+ type: 'boolean',
41
+ description: 'Enable verbose output',
42
+ default: getenv('GLAB_SETUP_GIT_IDENTITY_VERBOSE', false),
43
+ })
44
+ .option('dry-run', {
45
+ alias: 'dry',
46
+ type: 'boolean',
47
+ description:
48
+ 'Dry run mode - show what would be done without making changes',
49
+ default: getenv('GLAB_SETUP_GIT_IDENTITY_DRY_RUN', false),
50
+ })
51
+ .option('verify', {
52
+ type: 'boolean',
53
+ description: 'Verify current git identity configuration',
54
+ default: false,
55
+ })
56
+ // glab auth login options
57
+ .option('hostname', {
58
+ type: 'string',
59
+ description: 'GitLab hostname to authenticate with',
60
+ default: getenv('GLAB_AUTH_HOSTNAME', defaultAuthOptions.hostname),
61
+ })
62
+ .option('token', {
63
+ alias: 't',
64
+ type: 'string',
65
+ description: 'GitLab access token',
66
+ default: getenv('GLAB_AUTH_TOKEN', undefined),
67
+ })
68
+ .option('stdin', {
69
+ type: 'boolean',
70
+ description: 'Read token from standard input',
71
+ default: false,
72
+ })
73
+ .option('git-protocol', {
74
+ alias: 'p',
75
+ type: 'string',
76
+ description: 'Protocol for git operations: ssh, https, or http',
77
+ choices: ['ssh', 'https', 'http'],
78
+ default: getenv(
79
+ 'GLAB_AUTH_GIT_PROTOCOL',
80
+ defaultAuthOptions.gitProtocol
81
+ ),
82
+ })
83
+ .option('api-protocol', {
84
+ type: 'string',
85
+ description: 'Protocol for API calls: https or http',
86
+ choices: ['https', 'http'],
87
+ default: getenv(
88
+ 'GLAB_AUTH_API_PROTOCOL',
89
+ defaultAuthOptions.apiProtocol
90
+ ),
91
+ })
92
+ .option('api-host', {
93
+ type: 'string',
94
+ description: 'Custom API host URL',
95
+ default: getenv('GLAB_AUTH_API_HOST', undefined),
96
+ })
97
+ .option('use-keyring', {
98
+ type: 'boolean',
99
+ description: 'Store token in system keyring',
100
+ default: getenv('GLAB_AUTH_USE_KEYRING', defaultAuthOptions.useKeyring),
101
+ })
102
+ .option('job-token', {
103
+ alias: 'j',
104
+ type: 'string',
105
+ description: 'CI job token for authentication',
106
+ default: getenv('GLAB_AUTH_JOB_TOKEN', undefined),
107
+ })
108
+ .check((argv) => {
109
+ // --global and --local are mutually exclusive
110
+ if (argv.global && argv.local) {
111
+ throw new Error('Arguments global and local are mutually exclusive');
112
+ }
113
+ // --token and --stdin are mutually exclusive
114
+ if (argv.token && argv.stdin) {
115
+ throw new Error('Arguments token and stdin are mutually exclusive');
116
+ }
117
+ // --token and --job-token are mutually exclusive
118
+ if (argv.token && argv.jobToken) {
119
+ throw new Error(
120
+ 'Arguments token and job-token are mutually exclusive'
121
+ );
122
+ }
123
+ return true;
124
+ })
125
+ .example('$0', 'Setup git identity globally using GitLab user')
126
+ .example('$0 --local', 'Setup git identity for current repository only')
127
+ .example(
128
+ '$0 --dry-run',
129
+ 'Show what would be configured without making changes'
130
+ )
131
+ .example('$0 --verify', 'Verify current git identity configuration')
132
+ .example(
133
+ '$0 --hostname gitlab.company.com',
134
+ 'Authenticate with self-hosted GitLab'
135
+ )
136
+ .example('$0 --git-protocol ssh', 'Use SSH protocol for git operations')
137
+ .example(
138
+ 'echo "$TOKEN" | $0 --stdin',
139
+ 'Authenticate using token from stdin'
140
+ )
141
+ .example('$0 --token glpat-xxxxx', 'Authenticate using a token directly')
142
+ .example(
143
+ '$0 --job-token "$CI_JOB_TOKEN"',
144
+ 'Authenticate using CI job token'
145
+ )
146
+ .help('h')
147
+ .alias('h', 'help')
148
+ .version()
149
+ .strict(),
150
+ });
151
+
152
+ /**
153
+ * Run verification commands and display results
154
+ * @param {string} scope - 'global' or 'local'
155
+ * @param {boolean} verbose - Enable verbose logging
156
+ */
157
+ async function runVerify(scope, verbose) {
158
+ const scopeFlag = scope === 'local' ? '--local' : '--global';
159
+
160
+ console.log('Verifying git identity configuration...');
161
+ console.log('');
162
+
163
+ // 1. Run glab auth status
164
+ console.log('1. GitLab CLI authentication status:');
165
+ console.log(' $ glab auth status');
166
+ console.log('');
167
+
168
+ // Run glab auth status - use command-stream with inherited stdio for interactive output
169
+ try {
170
+ await $`glab auth status`.run({ mirror: { stdout: true, stderr: true } });
171
+ } catch {
172
+ // Continue even if not authenticated
173
+ }
174
+
175
+ console.log('');
176
+
177
+ // 2. Get git config user.name
178
+ console.log(`2. Git user.name (${scope}):`);
179
+ console.log(` $ git config ${scopeFlag} user.name`);
180
+
181
+ const identity = await verifyGitIdentity({ scope, verbose });
182
+
183
+ if (identity.username) {
184
+ console.log(` ${identity.username}`);
185
+ } else {
186
+ console.log(' (not set)');
187
+ }
188
+
189
+ console.log('');
190
+
191
+ // 3. Get git config user.email
192
+ console.log(`3. Git user.email (${scope}):`);
193
+ console.log(` $ git config ${scopeFlag} user.email`);
194
+
195
+ if (identity.email) {
196
+ console.log(` ${identity.email}`);
197
+ } else {
198
+ console.log(' (not set)');
199
+ }
200
+
201
+ console.log('');
202
+ console.log('Verification complete!');
203
+ }
204
+
205
+ /**
206
+ * Get auth options from CLI config
207
+ * @returns {Object} Auth options
208
+ */
209
+ function getAuthOptions() {
210
+ return {
211
+ hostname: config.hostname,
212
+ token: config.token,
213
+ gitProtocol: config.gitProtocol,
214
+ apiProtocol: config.apiProtocol,
215
+ apiHost: config.apiHost,
216
+ useKeyring: config.useKeyring,
217
+ jobToken: config.jobToken,
218
+ stdin: config.stdin,
219
+ verbose: config.verbose,
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Handle authentication flow
225
+ * @returns {Promise<boolean>} True if authentication succeeded or already authenticated
226
+ */
227
+ async function ensureAuthenticated() {
228
+ const authenticated = await isGlabAuthenticated({
229
+ hostname: config.hostname,
230
+ verbose: config.verbose,
231
+ });
232
+
233
+ if (authenticated) {
234
+ return handleAlreadyAuthenticated();
235
+ }
236
+
237
+ return handleNotAuthenticated();
238
+ }
239
+
240
+ /**
241
+ * Handle case when already authenticated
242
+ * @returns {Promise<boolean>} True
243
+ */
244
+ async function handleAlreadyAuthenticated() {
245
+ // Ensure git credential helper is configured
246
+ const setupGitSuccess = await runGlabAuthSetupGit({
247
+ hostname: config.hostname,
248
+ verbose: config.verbose,
249
+ });
250
+
251
+ if (!setupGitSuccess && config.verbose) {
252
+ console.log(
253
+ 'Note: Git credential helper may not be configured. Consider running:'
254
+ );
255
+ console.log(
256
+ ' Configure manually or use --force option to overwrite existing config'
257
+ );
258
+ }
259
+
260
+ return true;
261
+ }
262
+
263
+ /**
264
+ * Handle case when not authenticated
265
+ * @returns {Promise<boolean>} True if login succeeded
266
+ */
267
+ async function handleNotAuthenticated() {
268
+ console.log('GitLab CLI is not authenticated. Starting authentication...');
269
+ console.log('');
270
+
271
+ const loginSuccess = await runGlabAuthLogin(getAuthOptions());
272
+
273
+ if (!loginSuccess) {
274
+ console.log('');
275
+ console.log('Authentication failed. Please try running manually:');
276
+ console.log(
277
+ ` glab auth login --hostname ${config.hostname} --git-protocol ${config.gitProtocol}`
278
+ );
279
+ return false;
280
+ }
281
+
282
+ // Setup git credential helper after successful login
283
+ const setupGitSuccess = await runGlabAuthSetupGit({
284
+ hostname: config.hostname,
285
+ verbose: config.verbose,
286
+ });
287
+
288
+ if (!setupGitSuccess) {
289
+ console.log('');
290
+ console.log(
291
+ 'Warning: Failed to setup git credential helper. HTTPS git operations may require manual authentication.'
292
+ );
293
+ }
294
+
295
+ return true;
296
+ }
297
+
298
+ /**
299
+ * Display the setup results
300
+ * @param {Object} result - Setup result with username and email
301
+ * @param {Object} options - Setup options
302
+ */
303
+ function displayResults(result, options) {
304
+ const { scope, dryRun } = options;
305
+
306
+ console.log('');
307
+ console.log(` ${dryRun ? '[DRY MODE] Would configure' : 'Git configured'}:`);
308
+ console.log(` user.name: ${result.username}`);
309
+ console.log(` user.email: ${result.email}`);
310
+ console.log(
311
+ ` Scope: ${scope === 'global' ? 'global (--global)' : 'local (--local)'}`
312
+ );
313
+
314
+ if (!dryRun) {
315
+ console.log('');
316
+ console.log('Git identity setup complete!');
317
+ console.log('');
318
+ console.log('You can verify your configuration with:');
319
+ console.log(' glab auth status');
320
+ console.log(
321
+ ` git config ${scope === 'global' ? '--global' : '--local'} user.name`
322
+ );
323
+ console.log(
324
+ ` git config ${scope === 'global' ? '--global' : '--local'} user.email`
325
+ );
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Main CLI function
331
+ */
332
+ async function main() {
333
+ try {
334
+ const scope = config.local ? 'local' : 'global';
335
+
336
+ // Handle --verify mode
337
+ if (config.verify) {
338
+ await runVerify(scope, config.verbose);
339
+ process.exit(0);
340
+ }
341
+
342
+ // Ensure authenticated
343
+ const authSuccess = await ensureAuthenticated();
344
+ if (!authSuccess) {
345
+ process.exit(1);
346
+ }
347
+
348
+ // Prepare options
349
+ const options = {
350
+ hostname: config.hostname,
351
+ scope,
352
+ dryRun: config.dryRun,
353
+ verbose: config.verbose,
354
+ };
355
+
356
+ if (options.verbose) {
357
+ console.log('Options:', options);
358
+ }
359
+
360
+ if (options.dryRun) {
361
+ console.log('');
362
+ console.log('DRY MODE - No actual changes will be made');
363
+ }
364
+
365
+ // Setup git identity
366
+ const result = await setupGitIdentity(options);
367
+
368
+ // Display results
369
+ displayResults(result, options);
370
+
371
+ process.exit(0);
372
+ } catch (error) {
373
+ console.error('');
374
+ console.error('Error:', error.message);
375
+
376
+ if (config.verbose) {
377
+ console.error('Stack trace:');
378
+ console.error(error.stack);
379
+ }
380
+
381
+ process.exit(1);
382
+ }
383
+ }
384
+
385
+ // Run the CLI
386
+ main();