@forceuser/git-profile-switcher 0.1.4

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.
@@ -0,0 +1,677 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { resolve } from "node:path";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { buildCompletionSuggestions, generateCompletionScript } from "#cli/completion";
8
+ import { parseArgs, getStringFlag, hasFlag } from "#cli/parse";
9
+ import { renderHelp } from "#cli/help";
10
+ import { createPromptSession } from "#cli/prompts";
11
+ import { getAppPaths } from "#config/paths";
12
+ import { applyGitProfileConfig, clearGlobalGitIdentity, findMatchingRule, readActiveGitIdentity, setGlobalGitIdentity, } from "#git/config";
13
+ import { createProfileRepository, normalizePromptColor } from "#profiles/repository";
14
+ import { createProfileExportBundle, mergeProfileStoreData, parseProfileImportBundle, } from "#profiles/transfer";
15
+ import { PROFILE_PROMPT_COLOR_CODES, PROFILE_PROMPT_COLORS, } from "#profiles/types";
16
+ import { getPromptStatus, renderPromptStatus, } from "#prompt/status";
17
+ import { detectShell, installShellAll, installShellCompletion, installShellPrompt, installShellSession, uninstallShellAll, uninstallShellCompletion, uninstallShellPrompt, uninstallShellSession, } from "#shell/integration";
18
+ import { runTui } from "#tui/index";
19
+ const PACKAGE_NAME = "@forceuser/git-profile-switcher";
20
+ const DEFAULT_TRANSFER_FILE_NAME = "gip-profiles.json";
21
+ export async function main(args = process.argv.slice(2)) {
22
+ const command = args[0] ?? "help";
23
+ const rest = args.slice(1);
24
+ const parsed = parseArgs(rest);
25
+ const paths = getAppPaths();
26
+ const repository = createProfileRepository(paths.profilesPath);
27
+ if (command === "help" || command === "--help" || command === "-h") {
28
+ console.log(renderHelp(parsed.positionals[0]));
29
+ return;
30
+ }
31
+ if (hasFlag(parsed, "help")) {
32
+ console.log(renderHelp(command));
33
+ return;
34
+ }
35
+ if (command === "__complete") {
36
+ const separatorIndex = rest.indexOf("--");
37
+ const completionArgs = separatorIndex >= 0 ? rest.slice(separatorIndex + 1) : rest;
38
+ const suggestions = await buildCompletionSuggestions({ repository }, completionArgs);
39
+ console.log(suggestions.join("\n"));
40
+ return;
41
+ }
42
+ if (command === "profile:add") {
43
+ const prompts = createPromptSession();
44
+ try {
45
+ const profileName = parsed.positionals[0] ?? (await prompts.askRequired("Profile name: "));
46
+ const userName = getStringFlag(parsed, "user-name") ?? (await prompts.askRequired("Git user.name: "));
47
+ const userEmail = getStringFlag(parsed, "user-email") ?? (await prompts.askRequired("Git user.email: "));
48
+ const profile = await repository.upsertProfile({ name: profileName, userName, userEmail });
49
+ console.log(`Saved profile ${profile.name}: ${profile.userName} <${profile.userEmail}>`);
50
+ }
51
+ finally {
52
+ prompts.close();
53
+ }
54
+ return;
55
+ }
56
+ if (command === "profile:list") {
57
+ const data = await repository.read();
58
+ if (hasFlag(parsed, "json")) {
59
+ console.log(JSON.stringify(data.profiles, null, 2));
60
+ return;
61
+ }
62
+ if (data.profiles.length === 0) {
63
+ console.log("No profiles yet.");
64
+ return;
65
+ }
66
+ for (const profile of data.profiles) {
67
+ console.log(renderProfileListLine(profile));
68
+ }
69
+ return;
70
+ }
71
+ if (command === "profile:remove") {
72
+ const profileName = requirePositional(parsed.positionals[0], "profile");
73
+ const removed = await repository.removeProfile(profileName);
74
+ console.log(removed ? `Removed profile ${profileName}.` : `Profile not found: ${profileName}`);
75
+ return;
76
+ }
77
+ if (command === "profile:color") {
78
+ const data = await repository.read();
79
+ const profileName = parsed.positionals[0] ??
80
+ (await selectProfile(data.profiles, "Choose profile [1]: ", getActiveProfileName(data)));
81
+ const promptColorArgument = parsePromptColorArgument(parsed.positionals[1]);
82
+ const promptColor = promptColorArgument === undefined
83
+ ? await selectProfilePromptColor(`Prompt color for ${profileName}`)
84
+ : promptColorArgument;
85
+ const profile = await repository.setProfilePromptColor({
86
+ name: profileName,
87
+ promptColor,
88
+ });
89
+ console.log(profile.promptColor
90
+ ? `Set prompt color for ${profile.name} to ${profile.promptColor}.`
91
+ : `Cleared prompt color for ${profile.name}.`);
92
+ return;
93
+ }
94
+ if (command === "rule:add") {
95
+ const data = await repository.read();
96
+ const firstArg = parsed.positionals[0];
97
+ const secondArg = parsed.positionals[1];
98
+ const firstArgIsProfile = data.profiles.some((profile) => profile.name === firstArg);
99
+ let profileName = firstArgIsProfile ? firstArg : undefined;
100
+ let directory = secondArg ?? (firstArgIsProfile ? undefined : firstArg);
101
+ if (!profileName || !directory) {
102
+ const prompts = createPromptSession();
103
+ try {
104
+ profileName =
105
+ profileName ??
106
+ (await selectProfileWithSession(data.profiles, "Choose profile [1]: ", prompts, getActiveProfileName(data)));
107
+ directory = directory ?? (await prompts.askRequired("Directory: "));
108
+ }
109
+ finally {
110
+ prompts.close();
111
+ }
112
+ }
113
+ if (!profileName || !directory) {
114
+ throw new Error("Profile and directory are required.");
115
+ }
116
+ const rule = await repository.addRule({
117
+ profileName,
118
+ directory,
119
+ homeDir: paths.homeDir,
120
+ });
121
+ console.log(`Saved rule ${rule.id}: ${rule.profileName} -> ${rule.directory}`);
122
+ return;
123
+ }
124
+ if (command === "rule:list") {
125
+ const data = await repository.read();
126
+ if (hasFlag(parsed, "json")) {
127
+ console.log(JSON.stringify(data.rules, null, 2));
128
+ return;
129
+ }
130
+ if (data.rules.length === 0) {
131
+ console.log("No directory rules yet.");
132
+ return;
133
+ }
134
+ for (const rule of data.rules) {
135
+ console.log(`${rule.id}\t${rule.profileName}\t${rule.directory}`);
136
+ }
137
+ return;
138
+ }
139
+ if (command === "rule:remove") {
140
+ const ruleId = requirePositional(parsed.positionals[0], "rule-id");
141
+ const removed = await repository.removeRule(ruleId);
142
+ console.log(removed ? `Removed rule ${ruleId}.` : `Rule not found: ${ruleId}`);
143
+ return;
144
+ }
145
+ if (command === "use") {
146
+ const data = await repository.read();
147
+ const profileName = parsed.positionals[0] ??
148
+ (await selectProfile(data.profiles, "Choose profile [1]: ", getActiveProfileName(data)));
149
+ if (hasFlag(parsed, "global")) {
150
+ const profile = findProfile(data.profiles, profileName);
151
+ const result = await setGlobalGitIdentity({
152
+ profile,
153
+ globalGitConfigPath: paths.globalGitConfigPath,
154
+ });
155
+ console.log(`Using global profile ${profile.name}: ${profile.userName} <${profile.userEmail}>`);
156
+ console.log(result.changed
157
+ ? `Updated ${result.globalGitConfigPath}.`
158
+ : `${result.globalGitConfigPath} was already up to date.`);
159
+ return;
160
+ }
161
+ const directory = parsed.positionals[1] ?? process.cwd();
162
+ const rule = await repository.setDirectoryProfile({
163
+ profileName,
164
+ directory,
165
+ homeDir: paths.homeDir,
166
+ });
167
+ const nextData = await repository.read();
168
+ const result = await applyGitProfileConfig({
169
+ data: nextData,
170
+ globalGitConfigPath: paths.globalGitConfigPath,
171
+ generatedGitConfigDir: paths.generatedGitConfigDir,
172
+ });
173
+ console.log(`Using profile ${rule.profileName} for ${rule.directory}`);
174
+ console.log(`Generated ${result.generatedFiles.length} profile config file(s).`);
175
+ console.log(result.changed
176
+ ? `Updated ${result.globalGitConfigPath}.`
177
+ : `${result.globalGitConfigPath} was already up to date.`);
178
+ return;
179
+ }
180
+ if (command === "now") {
181
+ const shell = resolveShell(parsed, undefined) ?? "bash";
182
+ const printExports = hasFlag(parsed, "exports");
183
+ if (hasFlag(parsed, "clear")) {
184
+ console.log(printExports
185
+ ? renderSessionIdentityClear(shell)
186
+ : 'Use shell integration or run `eval "$(gip now --clear --exports)"` to clear the current shell session.');
187
+ return;
188
+ }
189
+ const data = await repository.read();
190
+ const profileName = parsed.positionals[0] ??
191
+ (await selectProfileForShellCommand(data.profiles, getActiveProfileName(data)));
192
+ const profile = findProfile(data.profiles, profileName);
193
+ console.log(printExports
194
+ ? renderSessionIdentityExports(profile, shell)
195
+ : `Selected session profile ${profile.name}. Use shell integration or run \`eval "$(gip now ${profile.name} --exports)"\` to apply it in this shell.`);
196
+ return;
197
+ }
198
+ if (command === "clear") {
199
+ if (hasFlag(parsed, "global")) {
200
+ const result = await clearGlobalGitIdentity(paths.globalGitConfigPath);
201
+ console.log(result.changed
202
+ ? `Cleared global Git user identity in ${result.globalGitConfigPath}.`
203
+ : `Global Git user identity was already clear in ${result.globalGitConfigPath}.`);
204
+ return;
205
+ }
206
+ const directory = parsed.positionals[0] ?? process.cwd();
207
+ const removed = await repository.clearDirectoryProfile({
208
+ directory,
209
+ homeDir: paths.homeDir,
210
+ });
211
+ const data = await repository.read();
212
+ const result = await applyGitProfileConfig({
213
+ data,
214
+ globalGitConfigPath: paths.globalGitConfigPath,
215
+ generatedGitConfigDir: paths.generatedGitConfigDir,
216
+ });
217
+ if (removed.length === 0) {
218
+ console.log(`No profile rule for ${directory}.`);
219
+ }
220
+ else {
221
+ console.log(`Cleared ${removed.length} profile rule(s) for ${removed[0].directory}`);
222
+ }
223
+ console.log(`Generated ${result.generatedFiles.length} profile config file(s).`);
224
+ console.log(result.changed
225
+ ? `Updated ${result.globalGitConfigPath}.`
226
+ : `${result.globalGitConfigPath} was already up to date.`);
227
+ return;
228
+ }
229
+ if (command === "apply") {
230
+ const data = await repository.read();
231
+ const result = await applyGitProfileConfig({
232
+ data,
233
+ globalGitConfigPath: paths.globalGitConfigPath,
234
+ generatedGitConfigDir: paths.generatedGitConfigDir,
235
+ });
236
+ console.log(`Generated ${result.generatedFiles.length} profile config file(s).`);
237
+ console.log(result.changed
238
+ ? `Updated ${result.globalGitConfigPath}.`
239
+ : `${result.globalGitConfigPath} was already up to date.`);
240
+ return;
241
+ }
242
+ if (command === "export") {
243
+ const data = await repository.read();
244
+ const exportData = hasFlag(parsed, "profiles-only") ? { ...data, rules: [] } : data;
245
+ const bundle = createProfileExportBundle(exportData);
246
+ const outputPath = getStringFlag(parsed, "output") ??
247
+ parsed.positionals[0] ??
248
+ join(paths.homeDir, DEFAULT_TRANSFER_FILE_NAME);
249
+ const json = `${JSON.stringify(bundle, null, 2)}\n`;
250
+ if (outputPath === "-") {
251
+ console.log(json.trimEnd());
252
+ return;
253
+ }
254
+ const resolvedOutputPath = resolve(outputPath);
255
+ await mkdir(dirname(resolvedOutputPath), { recursive: true });
256
+ await writeFile(resolvedOutputPath, json, { mode: 0o600 });
257
+ console.log(`Exported ${exportData.profiles.length} profile(s) and ${exportData.rules.length} rule(s) to ${resolvedOutputPath}`);
258
+ return;
259
+ }
260
+ if (command === "import") {
261
+ const inputPath = getStringFlag(parsed, "input") ??
262
+ parsed.positionals[0] ??
263
+ join(paths.homeDir, DEFAULT_TRANSFER_FILE_NAME);
264
+ if (inputPath === "-") {
265
+ throw new Error("Import from stdin is not supported. Use `gip import --input <path>`.");
266
+ }
267
+ const resolvedInputPath = resolve(inputPath);
268
+ const incoming = parseProfileImportBundle(JSON.parse(await readFile(resolvedInputPath, "utf8")));
269
+ const importData = hasFlag(parsed, "profiles-only") ? { ...incoming, rules: [] } : incoming;
270
+ const current = await repository.read();
271
+ const next = hasFlag(parsed, "replace")
272
+ ? importData
273
+ : mergeProfileStoreData(current, importData);
274
+ await repository.save(next);
275
+ console.log(`Imported ${importData.profiles.length} profile(s) and ${importData.rules.length} rule(s) from ${resolvedInputPath}${hasFlag(parsed, "replace") ? " with replace mode" : " with merge mode"}.`);
276
+ if (!hasFlag(parsed, "no-apply")) {
277
+ const result = await applyGitProfileConfig({
278
+ data: next,
279
+ globalGitConfigPath: paths.globalGitConfigPath,
280
+ generatedGitConfigDir: paths.generatedGitConfigDir,
281
+ });
282
+ console.log(`Generated ${result.generatedFiles.length} profile config file(s).`);
283
+ console.log(result.changed
284
+ ? `Updated ${result.globalGitConfigPath}.`
285
+ : `${result.globalGitConfigPath} was already up to date.`);
286
+ }
287
+ return;
288
+ }
289
+ if (command === "doctor") {
290
+ const data = await repository.read();
291
+ const cwd = resolve(parsed.positionals[0] ?? process.cwd());
292
+ const rule = findMatchingRule(data, cwd);
293
+ const identity = readActiveGitIdentity(cwd);
294
+ const report = { cwd, rule, identity };
295
+ if (hasFlag(parsed, "json")) {
296
+ console.log(JSON.stringify(report, null, 2));
297
+ return;
298
+ }
299
+ console.log(`Directory: ${cwd}`);
300
+ console.log(`Git user.name: ${identity.userName ?? "[unset]"}`);
301
+ console.log(`Git user.email: ${identity.userEmail ?? "[unset]"}`);
302
+ console.log(rule
303
+ ? `Managed rule: ${rule.id} (${rule.profileName} -> ${rule.directory})`
304
+ : "Managed rule: [none]");
305
+ return;
306
+ }
307
+ if (command === "prompt") {
308
+ const data = await repository.read();
309
+ const status = getPromptStatus({ data });
310
+ if (hasFlag(parsed, "json")) {
311
+ console.log(JSON.stringify(status, null, 2));
312
+ return;
313
+ }
314
+ const rendered = renderPromptStatus(status, parsePromptFormat(parsed), parsePromptShell());
315
+ if (rendered) {
316
+ console.log(rendered);
317
+ }
318
+ return;
319
+ }
320
+ if (command === "install:prompt" ||
321
+ command === "uninstall:prompt" ||
322
+ command === "install:shell" ||
323
+ command === "uninstall:shell") {
324
+ const shell = resolveShell(parsed, parsed.positionals[0]);
325
+ if (!shell) {
326
+ throw new Error("Could not detect shell. Pass zsh, bash, or fish explicitly.");
327
+ }
328
+ const configPath = getConfigPath(parsed);
329
+ const result = command === "install:prompt"
330
+ ? await installShellPrompt({
331
+ shell,
332
+ configPath: configPath ?? undefined,
333
+ promptFormat: parsePromptFormat(parsed),
334
+ })
335
+ : command === "uninstall:prompt"
336
+ ? await uninstallShellPrompt({ shell, configPath: configPath ?? undefined })
337
+ : command === "install:shell"
338
+ ? await installShellSession({ shell, configPath: configPath ?? undefined })
339
+ : await uninstallShellSession({ shell, configPath: configPath ?? undefined });
340
+ console.log(`${result.changed ? "Updated" : "Unchanged"}: ${result.path}`);
341
+ return;
342
+ }
343
+ if (command === "completion") {
344
+ const shell = resolveShell(parsed, parsed.positionals[0]);
345
+ if (!shell) {
346
+ throw new Error("Could not detect shell. Pass zsh, bash, or fish explicitly.");
347
+ }
348
+ console.log(generateCompletionScript(shell).trimEnd());
349
+ return;
350
+ }
351
+ if (command === "install:completion" || command === "uninstall:completion") {
352
+ const shell = resolveShell(parsed, parsed.positionals[0]);
353
+ if (!shell) {
354
+ throw new Error("Could not detect shell. Pass zsh, bash, or fish explicitly.");
355
+ }
356
+ const configPath = getConfigPath(parsed);
357
+ const result = command === "install:completion"
358
+ ? await installShellCompletion({ shell, configPath: configPath ?? undefined })
359
+ : await uninstallShellCompletion({ shell, configPath: configPath ?? undefined });
360
+ console.log(`${result.changed ? "Updated" : "Unchanged"}: ${result.path}`);
361
+ return;
362
+ }
363
+ if (command === "install" || command === "install:all" || command === "update") {
364
+ const installTarget = parseInstallTarget(command, parsed.positionals);
365
+ const shell = resolveShell(parsed, installTarget.shell);
366
+ if (!shell) {
367
+ throw new Error("Could not detect shell. Pass zsh, bash, or fish explicitly.");
368
+ }
369
+ if (command === "update" || shouldInstallPackage(command, parsed.positionals)) {
370
+ const packageSpec = command === "update" ? `${PACKAGE_NAME}@latest` : readCurrentPackageSpec();
371
+ console.log(`Installing ${packageSpec} globally with npm...`);
372
+ await runNpmInstallGlobal(packageSpec);
373
+ }
374
+ const configPath = getConfigPath(parsed);
375
+ const result = await runInstallTarget({
376
+ target: installTarget.target,
377
+ shell,
378
+ configPath: configPath ?? undefined,
379
+ promptFormat: parsePromptFormat(parsed),
380
+ });
381
+ const verb = command === "update" ? "Updated" : "Installed";
382
+ console.log(`${verb} ${installTarget.target} shell integration for ${shell}: ${result.path}`);
383
+ console.log(result.changed ? "Updated shell config." : "Shell config was already up to date.");
384
+ return;
385
+ }
386
+ if (command === "uninstall:all") {
387
+ const shell = resolveShell(parsed, parsed.positionals[0]);
388
+ if (!shell) {
389
+ throw new Error("Could not detect shell. Pass zsh, bash, or fish explicitly.");
390
+ }
391
+ const result = await uninstallShellAll({
392
+ shell,
393
+ configPath: getConfigPath(parsed) ?? undefined,
394
+ });
395
+ console.log(`${result.changed ? "Updated" : "Unchanged"}: ${result.path}`);
396
+ return;
397
+ }
398
+ if (command === "paths") {
399
+ if (hasFlag(parsed, "json")) {
400
+ console.log(JSON.stringify(paths, null, 2));
401
+ return;
402
+ }
403
+ for (const [key, value] of Object.entries(paths)) {
404
+ console.log(`${key}: ${value}`);
405
+ }
406
+ return;
407
+ }
408
+ if (command === "tui") {
409
+ await runTui({
410
+ repository,
411
+ paths,
412
+ input: process.stdin,
413
+ output: process.stdout,
414
+ });
415
+ return;
416
+ }
417
+ throw new Error(`Unknown command: ${command}\n\n${renderHelp()}`);
418
+ }
419
+ async function selectProfile(profiles, prompt, defaultProfileName) {
420
+ const prompts = createPromptSession();
421
+ try {
422
+ return await selectProfileWithSession(profiles, prompt, prompts, defaultProfileName);
423
+ }
424
+ finally {
425
+ prompts.close();
426
+ }
427
+ }
428
+ async function selectProfileForShellCommand(profiles, defaultProfileName) {
429
+ const prompts = createPromptSession(process.stdin, process.stderr);
430
+ try {
431
+ return await selectProfileWithSession(profiles, "Choose profile [1]: ", prompts, defaultProfileName);
432
+ }
433
+ finally {
434
+ prompts.close();
435
+ }
436
+ }
437
+ function getActiveProfileName(data) {
438
+ return getPromptStatus({ data }).profileName;
439
+ }
440
+ function findProfile(profiles, name) {
441
+ const profile = profiles.find((candidate) => candidate.name === name);
442
+ if (!profile) {
443
+ throw new Error(`Unknown profile: ${name}`);
444
+ }
445
+ return profile;
446
+ }
447
+ function renderProfileListLine(profile) {
448
+ const promptColor = profile.promptColor ? `\tprompt: ${profile.promptColor}` : "";
449
+ return `${profile.name}\t${profile.userName} <${profile.userEmail}>${promptColor}`;
450
+ }
451
+ function renderProfileOption(profile) {
452
+ const swatch = profile.promptColor ? `${renderPromptColorSwatch(profile.promptColor)} ` : "";
453
+ return `${swatch}${profile.name}\t${profile.userName} <${profile.userEmail}>`;
454
+ }
455
+ function parsePromptFormat(parsed) {
456
+ if (hasFlag(parsed, "profile")) {
457
+ return "profile";
458
+ }
459
+ const format = getStringFlag(parsed, "format") ?? "auto";
460
+ if (format === "identity" || format === "profile" || format === "auto") {
461
+ return format;
462
+ }
463
+ throw new Error(`Unsupported prompt format: ${format}. Use identity, profile, or auto.`);
464
+ }
465
+ function parsePromptShell() {
466
+ const shell = process.env.GIP_PROMPT_SHELL;
467
+ if (shell === "bash" || shell === "fish" || shell === "zsh") {
468
+ return shell;
469
+ }
470
+ return null;
471
+ }
472
+ function renderSessionIdentityExports(profile, shell) {
473
+ const values = {
474
+ GIP_PROFILE_NAME: profile.name,
475
+ GIT_AUTHOR_NAME: profile.userName,
476
+ GIT_AUTHOR_EMAIL: profile.userEmail,
477
+ GIT_COMMITTER_NAME: profile.userName,
478
+ GIT_COMMITTER_EMAIL: profile.userEmail,
479
+ GIT_CONFIG_COUNT: "2",
480
+ GIT_CONFIG_KEY_0: "user.name",
481
+ GIT_CONFIG_VALUE_0: profile.userName,
482
+ GIT_CONFIG_KEY_1: "user.email",
483
+ GIT_CONFIG_VALUE_1: profile.userEmail,
484
+ };
485
+ if (shell === "fish") {
486
+ return Object.entries(values)
487
+ .map(([key, value]) => `set -gx ${key} ${quoteFishValue(value)}`)
488
+ .join("\n");
489
+ }
490
+ return Object.entries(values)
491
+ .map(([key, value]) => `export ${key}=${quoteShellValue(value)}`)
492
+ .join("\n");
493
+ }
494
+ function renderSessionIdentityClear(shell) {
495
+ const names = [
496
+ "GIP_PROFILE_NAME",
497
+ "GIT_AUTHOR_NAME",
498
+ "GIT_AUTHOR_EMAIL",
499
+ "GIT_COMMITTER_NAME",
500
+ "GIT_COMMITTER_EMAIL",
501
+ "GIT_CONFIG_COUNT",
502
+ "GIT_CONFIG_KEY_0",
503
+ "GIT_CONFIG_VALUE_0",
504
+ "GIT_CONFIG_KEY_1",
505
+ "GIT_CONFIG_VALUE_1",
506
+ ];
507
+ if (shell === "fish") {
508
+ return names.map((name) => `set -e ${name}`).join("\n");
509
+ }
510
+ return `unset ${names.join(" ")}`;
511
+ }
512
+ function quoteShellValue(value) {
513
+ return `'${value.replaceAll("'", "'\\''")}'`;
514
+ }
515
+ function quoteFishValue(value) {
516
+ return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
517
+ }
518
+ function parsePromptColorArgument(value) {
519
+ if (value === undefined) {
520
+ return undefined;
521
+ }
522
+ const normalized = value.trim().toLowerCase();
523
+ if (normalized === "none" || normalized === "no-color" || normalized === "default") {
524
+ return null;
525
+ }
526
+ const promptColor = normalizePromptColor(normalized);
527
+ if (!promptColor) {
528
+ throw new Error(`Unsupported prompt color: ${value}. Use one of: ${PROFILE_PROMPT_COLORS.join(", ")}, or no-color.`);
529
+ }
530
+ return promptColor;
531
+ }
532
+ function parseInstallTarget(command, positionals) {
533
+ if (command === "install:all" || command === "update") {
534
+ return { target: "all", shell: positionals[0] };
535
+ }
536
+ const first = positionals[0];
537
+ if (first === "all" || first === "completion" || first === "prompt" || first === "shell") {
538
+ return { target: first, shell: positionals[1] };
539
+ }
540
+ return { target: "all", shell: first };
541
+ }
542
+ function shouldInstallPackage(command, positionals) {
543
+ if (command !== "install") {
544
+ return false;
545
+ }
546
+ const first = positionals[0];
547
+ return first !== "all" && first !== "completion" && first !== "prompt" && first !== "shell";
548
+ }
549
+ async function runInstallTarget(input) {
550
+ if (input.target === "completion") {
551
+ return await installShellCompletion({
552
+ shell: input.shell,
553
+ configPath: input.configPath,
554
+ });
555
+ }
556
+ if (input.target === "prompt") {
557
+ return await installShellPrompt({
558
+ shell: input.shell,
559
+ configPath: input.configPath,
560
+ promptFormat: input.promptFormat,
561
+ });
562
+ }
563
+ if (input.target === "shell") {
564
+ return await installShellSession({
565
+ shell: input.shell,
566
+ configPath: input.configPath,
567
+ });
568
+ }
569
+ return await installShellAll({
570
+ shell: input.shell,
571
+ configPath: input.configPath,
572
+ promptFormat: input.promptFormat,
573
+ });
574
+ }
575
+ function resolveShell(parsed, positionalShell) {
576
+ return parseShell(getStringFlag(parsed, "shell") ?? positionalShell) ?? detectShell();
577
+ }
578
+ function getConfigPath(parsed) {
579
+ return getStringFlag(parsed, "config") ?? getStringFlag(parsed, "path");
580
+ }
581
+ function readCurrentPackageSpec() {
582
+ const version = readPackageVersion();
583
+ return version ? `${PACKAGE_NAME}@${version}` : PACKAGE_NAME;
584
+ }
585
+ function readPackageVersion() {
586
+ for (const packageJsonPath of getPackageJsonCandidates()) {
587
+ if (!existsSync(packageJsonPath)) {
588
+ continue;
589
+ }
590
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
591
+ if (parsed.name === PACKAGE_NAME) {
592
+ return parsed.version ?? null;
593
+ }
594
+ }
595
+ return null;
596
+ }
597
+ function getPackageJsonCandidates() {
598
+ const here = dirname(fileURLToPath(import.meta.url));
599
+ return [
600
+ join(here, "..", "..", "package.json"),
601
+ join(here, "..", "..", "..", "package.json"),
602
+ join(process.cwd(), "package.json"),
603
+ ];
604
+ }
605
+ function runNpmInstallGlobal(packageSpec) {
606
+ return new Promise((resolvePromise, reject) => {
607
+ const child = spawn("npm", ["install", "-g", packageSpec], {
608
+ stdio: "inherit",
609
+ });
610
+ child.on("error", reject);
611
+ child.on("close", (status) => {
612
+ if (status === 0) {
613
+ resolvePromise();
614
+ return;
615
+ }
616
+ reject(new Error(`npm install -g ${packageSpec} exited with ${status}`));
617
+ });
618
+ });
619
+ }
620
+ async function selectProfileWithSession(profiles, prompt, prompts, defaultProfileName) {
621
+ const defaultIndex = getProfileDefaultIndex(profiles, defaultProfileName);
622
+ return (await prompts.selectOne({
623
+ prompt,
624
+ emptyMessage: "No profiles yet. Add one with `gip profile:add`.",
625
+ options: profiles,
626
+ renderOption: renderProfileOption,
627
+ getValue: (profile) => profile.name,
628
+ defaultIndex,
629
+ })).name;
630
+ }
631
+ function getProfileDefaultIndex(profiles, defaultProfileName) {
632
+ const index = defaultProfileName
633
+ ? profiles.findIndex((profile) => profile.name === defaultProfileName)
634
+ : -1;
635
+ return index >= 0 ? index : 0;
636
+ }
637
+ async function selectProfilePromptColor(prompt) {
638
+ const prompts = createPromptSession();
639
+ try {
640
+ const options = ["", ...PROFILE_PROMPT_COLORS];
641
+ const choice = await prompts.selectOne({
642
+ prompt,
643
+ emptyMessage: "No prompt colors available.",
644
+ options,
645
+ renderOption: (color) => (color ? renderPromptColorLabel(color) : "[no color]"),
646
+ getValue: (color) => color || "no-color",
647
+ defaultIndex: 0,
648
+ });
649
+ return choice === "" ? null : choice;
650
+ }
651
+ finally {
652
+ prompts.close();
653
+ }
654
+ }
655
+ function renderPromptColorLabel(promptColor) {
656
+ return `${renderPromptColorSwatch(promptColor)} ${promptColor}`;
657
+ }
658
+ function renderPromptColorSwatch(promptColor) {
659
+ return `\x1b[${PROFILE_PROMPT_COLOR_CODES[promptColor]}m■\x1b[0m`;
660
+ }
661
+ function requirePositional(value, name) {
662
+ if (!value) {
663
+ throw new Error(`Missing required argument: ${name}`);
664
+ }
665
+ return value;
666
+ }
667
+ function parseShell(value) {
668
+ if (value === "zsh" || value === "bash" || value === "fish") {
669
+ return value;
670
+ }
671
+ return null;
672
+ }
673
+ main().catch((error) => {
674
+ const message = error instanceof Error ? error.message : String(error);
675
+ console.error(message);
676
+ process.exitCode = 1;
677
+ });