@gitset-dev/cli 1.2.1 → 2.0.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 (41) hide show
  1. package/{LICENSE.md → LICENSE} +1 -1
  2. package/README.md +80 -159
  3. package/index.js +230 -0
  4. package/lib/ai/capabilities.js +81 -0
  5. package/lib/ai/errors.js +115 -0
  6. package/lib/ai/index.js +98 -0
  7. package/lib/ai/providers/anthropic.js +62 -0
  8. package/lib/ai/providers/gemini.js +77 -0
  9. package/lib/ai/providers/mock.js +41 -0
  10. package/lib/ai/providers/openai-compatible.js +72 -0
  11. package/lib/ai/retry.js +31 -0
  12. package/lib/cli-commit.js +143 -0
  13. package/lib/cli-config.js +207 -0
  14. package/lib/cli-issue.js +144 -0
  15. package/lib/cli-pr.js +168 -0
  16. package/lib/cli-readme.js +153 -0
  17. package/lib/commit-local.js +67 -0
  18. package/lib/config.js +134 -0
  19. package/lib/generate-local.js +59 -0
  20. package/lib/labels-ai.js +41 -0
  21. package/lib/prompts/defaults.js +191 -0
  22. package/lib/prompts/index.js +79 -0
  23. package/lib/setup-wizard.js +115 -0
  24. package/npm-shrinkwrap.json +566 -0
  25. package/package.json +35 -35
  26. package/src/commands/dependabot-resolver.js +314 -0
  27. package/src/commands/gitignore.js +110 -0
  28. package/src/commands/init.js +38 -0
  29. package/src/commands/labelspack.js +94 -0
  30. package/src/commands/license.js +208 -0
  31. package/src/commands/release.js +180 -0
  32. package/src/commands/repo.js +176 -0
  33. package/src/commands/status.js +56 -0
  34. package/src/commands/template.js +92 -0
  35. package/src/commands/tree.js +77 -0
  36. package/src/utils/dependabot-analyzer.js +101 -0
  37. package/src/utils/labels.js +102 -0
  38. package/src/utils/theme.js +70 -0
  39. package/src/utils/ui.js +173 -0
  40. package/.github/workflows/npm_publish.yml +0 -38
  41. package/src/index.js +0 -505
@@ -0,0 +1,314 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+ const DependabotAnalyzer = require('../utils/dependabot-analyzer');
5
+ const { log, askQuestion, selectOption, colors } = require('../utils/ui');
6
+
7
+ function execCommand(cmd) {
8
+ try {
9
+ return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
10
+ } catch (err) {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ function getLocalVersion(dependencyName, manifestPath) {
16
+ try {
17
+ const fullPath = path.resolve(process.cwd(), manifestPath);
18
+ if (!fs.existsSync(fullPath)) return null;
19
+
20
+ const content = fs.readFileSync(fullPath, 'utf8');
21
+
22
+ if (manifestPath.endsWith('package.json')) {
23
+ const pkg = JSON.parse(content);
24
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
25
+ const version = deps[dependencyName];
26
+ return version ? version.replace(/^[\^~]/, '') : null;
27
+ }
28
+
29
+ if (manifestPath.endsWith('package-lock.json')) {
30
+ const lock = JSON.parse(content);
31
+
32
+ if (lock.packages) {
33
+ const pkgKey = `node_modules/${dependencyName}`;
34
+ if (lock.packages[pkgKey]) {
35
+ return lock.packages[pkgKey].version;
36
+ }
37
+ }
38
+
39
+ if (lock.dependencies && lock.dependencies[dependencyName]) {
40
+ return lock.dependencies[dependencyName].version;
41
+ }
42
+ return null;
43
+ }
44
+
45
+ if (manifestPath.endsWith('requirements.txt')) {
46
+ const regex = new RegExp(`^${dependencyName}==(.*)$`, 'm');
47
+ const match = content.match(regex);
48
+ return match ? match[1] : null;
49
+ }
50
+
51
+ return null;
52
+ } catch (e) {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ async function fetchAlerts(owner, repo, token) {
58
+ const url = `https://api.github.com/repos/${owner}/${repo}/dependabot/alerts?state=open&per_page=100`;
59
+ try {
60
+ const response = await fetch(url, {
61
+ headers: {
62
+ 'Authorization': `token ${token}`,
63
+ 'Accept': 'application/vnd.github+json',
64
+ 'X-GitHub-Api-Version': '2022-11-28'
65
+ }
66
+ });
67
+
68
+ if (!response.ok) {
69
+ if (response.status === 401) throw new Error('Unauthorized (check token scopes)');
70
+ if (response.status === 403) throw new Error('Forbidden (check permissions/dependabot access)');
71
+ if (response.status === 404) throw new Error('Repo not found or no access');
72
+ throw new Error(`API Error: ${response.status} ${response.statusText}`);
73
+ }
74
+
75
+ return await response.json();
76
+ } catch (e) {
77
+ throw e;
78
+ }
79
+ }
80
+
81
+ async function commandDependabotResolver(config, args) {
82
+ const token = config?.github_token;
83
+ const ghStatus = execCommand('gh auth status');
84
+
85
+ if (!token && !ghStatus) {
86
+ log('✗ No GitHub authentication found.', 'red');
87
+ log(' Run "gh auth login" (GitHub CLI) and try again.', 'yellow');
88
+ return 1;
89
+ }
90
+
91
+ const analyzer = new DependabotAnalyzer();
92
+
93
+ let owner, repo;
94
+ try {
95
+ const remoteUrl = execCommand('git config --get remote.origin.url');
96
+ if (!remoteUrl) throw new Error('No remote origin found');
97
+
98
+ const match = remoteUrl.match(/[:/]([^/]+)\/([^/.]+)(?:\.git)?$/);
99
+ if (match) {
100
+ owner = match[1];
101
+ repo = match[2];
102
+ }
103
+ } catch (e) {
104
+ log('✗ Could not detect repository info from git config', 'red');
105
+ return;
106
+ }
107
+
108
+ if (!owner || !repo) {
109
+ log('✗ Invalid repository info detected', 'red');
110
+ return;
111
+ }
112
+
113
+ const shouldResolve = args.includes('--resolve');
114
+ const subcommand = shouldResolve ? 'resolve' : 'list';
115
+
116
+ if (subcommand === 'list' || subcommand === 'resolve') {
117
+ log(`\n→ Fetching Dependabot alerts for ${owner}/${repo}...\n`, 'cyan');
118
+
119
+ try {
120
+ let alerts = [];
121
+ let tokenError = null;
122
+
123
+ if (token) {
124
+ try {
125
+ alerts = await fetchAlerts(owner, repo, token);
126
+ } catch (e) {
127
+ tokenError = e;
128
+
129
+ if (e.message.includes('Unauthorized') || e.message.includes('Forbidden')) {
130
+ log(`⚠ Stored token failed (${e.message}).`, 'yellow');
131
+ log('→ Falling back to local "gh" CLI...', 'cyan');
132
+ } else {
133
+ throw e;
134
+ }
135
+ }
136
+ }
137
+
138
+ if (!token || (tokenError && (tokenError.message.includes('Unauthorized') || tokenError.message.includes('Forbidden')))) {
139
+ try {
140
+ const alertsJson = execCommand(`gh api "/repos/${owner}/${repo}/dependabot/alerts?state=open&per_page=100"`);
141
+ if (!alertsJson) {
142
+ throw new Error('Failed to fetch alerts via gh CLI (check "gh auth status" and scopes)');
143
+ }
144
+ alerts = JSON.parse(alertsJson);
145
+ } catch (ghError) {
146
+ if (tokenError) {
147
+ throw new Error(`Both methods failed.\n Token: ${tokenError.message}\n CLI: ${ghError.message}\n (Note: Dependabot alerts require 'security_events' scope)`);
148
+ } else {
149
+ throw ghError;
150
+ }
151
+ }
152
+ }
153
+
154
+ if (!alerts || alerts.length === 0) {
155
+ log('✓ No open Dependabot alerts found!', 'green');
156
+ return;
157
+ }
158
+
159
+ const analyzedAlerts = [];
160
+
161
+ for (const alert of alerts) {
162
+ const depName = alert.dependency.package.name;
163
+ const ecosystem = alert.dependency.package.ecosystem;
164
+ const manifestPath = alert.dependency.manifest_path;
165
+
166
+ let patchedVersion = null;
167
+
168
+ if (alert.security_vulnerability && alert.security_vulnerability.first_patched_version) {
169
+ patchedVersion = alert.security_vulnerability.first_patched_version.identifier;
170
+ }
171
+
172
+ else if (alert.security_advisory && alert.security_advisory.vulnerabilities) {
173
+ const v = alert.security_advisory.vulnerabilities.find(v => v.package.name === depName);
174
+ if (v && v.first_patched_version) {
175
+ patchedVersion = v.first_patched_version.identifier;
176
+ }
177
+ }
178
+
179
+ else if (alert.security_advisory && alert.security_advisory.patched_versions && alert.security_advisory.patched_versions.length > 0) {
180
+ patchedVersion = alert.security_advisory.patched_versions[0].identifier;
181
+ }
182
+
183
+ if (!patchedVersion) {
184
+ continue;
185
+ }
186
+
187
+ const localVersion = getLocalVersion(depName, manifestPath);
188
+
189
+ let risk = { level: 'UNKNOWN', reason: 'Local version not found' };
190
+ if (localVersion) {
191
+ risk = analyzer.calculateRisk(localVersion, patchedVersion, ecosystem);
192
+ }
193
+
194
+ analyzedAlerts.push({
195
+ id: alert.number,
196
+ depName,
197
+ localVersion: localVersion || '?',
198
+ patchedVersion,
199
+ severity: alert.security_advisory.severity,
200
+ risk,
201
+ manifestPath,
202
+ ecosystem,
203
+ url: alert.html_url
204
+ });
205
+ }
206
+
207
+ log('ID | Severity | Dependency | Version Delta | Risk', 'blue');
208
+ log('-----|----------|------------------|---------------------|----------------', 'blue');
209
+
210
+ analyzedAlerts.forEach(a => {
211
+ const delta = `${a.localVersion} -> ${a.patchedVersion}`;
212
+ const riskColor = a.risk.level === 'RISK_NONE' ? 'green' :
213
+ a.risk.level === 'RISK_LOW_BEHAVIORAL' ? 'cyan' :
214
+ a.risk.level === 'RISK_MODERATE_API' ? 'yellow' : 'red';
215
+
216
+ const paint = colors[riskColor] || ((s) => s);
217
+ console.log(
218
+ `${a.id.toString().padEnd(4)} | ` +
219
+ `${a.severity.padEnd(8)} | ` +
220
+ `${a.depName.padEnd(16)} | ` +
221
+ `${delta.padEnd(19)} | ` +
222
+ paint(a.risk.level)
223
+ );
224
+ });
225
+ log('');
226
+
227
+ if (subcommand === 'resolve') {
228
+ const isDryRun = args.includes('--dry-run');
229
+ const autoResolvable = analyzedAlerts.filter(a => a.risk.level === 'RISK_NONE');
230
+
231
+ if (autoResolvable.length === 0) {
232
+ log('No RISK_NONE alerts found for auto-resolution.', 'yellow');
233
+ return;
234
+ }
235
+
236
+ log(`Found ${autoResolvable.length} auto-resolvable alerts (RISK_NONE).`, 'green');
237
+
238
+ if (isDryRun) {
239
+ log('\n=== DRY RUN: Proposed Actions ===', 'pink');
240
+ autoResolvable.forEach(alert => {
241
+ console.log(`\n[Alert #${alert.id}] ${alert.depName}`);
242
+ console.log(` Update: ${alert.localVersion} -> ${alert.patchedVersion}`);
243
+ console.log(` Action: Update ${alert.manifestPath} locally`);
244
+ });
245
+ return;
246
+ }
247
+
248
+ const confirm = await askQuestion('Do you want to apply these updates locally? (y/n): ');
249
+
250
+ if (confirm.toLowerCase() === 'y') {
251
+ log('\n→ Applying updates...', 'cyan');
252
+
253
+ for (const alert of autoResolvable) {
254
+ log(`\n[${alert.depName}] Updating to ${alert.patchedVersion}...`, 'blue');
255
+
256
+ try {
257
+ if (alert.ecosystem === 'npm') {
258
+ const pkgPath = path.resolve(process.cwd(), 'package.json');
259
+ let saveDev = false;
260
+ if (fs.existsSync(pkgPath)) {
261
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
262
+ if (pkg.devDependencies && pkg.devDependencies[alert.depName]) {
263
+ saveDev = true;
264
+ }
265
+ }
266
+
267
+ try {
268
+ execCommand(`npm install ${alert.depName}@${alert.patchedVersion} ${saveDev ? '--save-dev' : ''}`);
269
+ log(` ✓ Updated ${alert.depName}`, 'green');
270
+ } catch (err) {
271
+ log(` ✗ npm install failed: ${err.message}`, 'red');
272
+ throw err;
273
+ }
274
+ } else {
275
+ const fullPath = path.resolve(process.cwd(), alert.manifestPath);
276
+ if (fs.existsSync(fullPath)) {
277
+ let content = fs.readFileSync(fullPath, 'utf8');
278
+ let newContent = content;
279
+
280
+ if (alert.ecosystem === 'pip') {
281
+ newContent = content.replace(new RegExp(`^${alert.depName}==.*$`, 'm'), `${alert.depName}==${alert.patchedVersion}`);
282
+ }
283
+
284
+ if (newContent !== content) {
285
+ fs.writeFileSync(fullPath, newContent);
286
+ log(` ✓ Updated ${alert.manifestPath}`, 'green');
287
+ } else {
288
+ log(` → Could not replace version in ${alert.manifestPath}`, 'yellow');
289
+ }
290
+ } else {
291
+ log(` ✗ File not found: ${alert.manifestPath}`, 'red');
292
+ }
293
+ }
294
+ } catch (e) {
295
+ log(` ✗ Failed to resolve ${alert.depName}: ${e.message}`, 'red');
296
+ }
297
+ }
298
+
299
+ log('\n✓ Updates completed!', 'green');
300
+ log('● Run "gitset commit" to review and commit these changes.', 'pink');
301
+ }
302
+ }
303
+ } catch (e) {
304
+ log(`✗ Error: ${e.message}`, 'red');
305
+ if (e.message.includes('404')) {
306
+ log(' (Make sure Dependabot alerts are enabled for this repo)', 'yellow');
307
+ }
308
+ }
309
+ } else {
310
+ log('Usage: gitset dependabot [--resolve]', 'yellow');
311
+ }
312
+ }
313
+
314
+ module.exports = commandDependabotResolver;
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `gitset gitignore` — local BYOAI .gitignore generation.
5
+ * Auto-detects the stack locally, generates via the user's own provider
6
+ * (vendored lib/ai through lib/generate-local). No backend, no gitset_key.
7
+ *
8
+ * Flags: --select (type stacks) | --<stack> ... | --provider --model
9
+ * --append --force --print
10
+ */
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const readline = require('readline');
14
+ const genLocal = require('../../lib/generate-local');
15
+
16
+ const tty = () => process.stdout.isTTY;
17
+ const c = (code, s) => (tty() ? `\x1b[${code}m${s}\x1b[0m` : s);
18
+ const ask = (q) => new Promise((r) => {
19
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
20
+ rl.question(q, (a) => { rl.close(); r(a.trim()); });
21
+ });
22
+ function flag(argv, n) {
23
+ const i = argv.indexOf(n);
24
+ return i !== -1 && argv[i + 1] && !argv[i + 1].startsWith('-') ? argv[i + 1] : null;
25
+ }
26
+
27
+ const DETECT = {
28
+ 'package.json': ['Node'], 'requirements.txt': ['Python'], 'pyproject.toml': ['Python'],
29
+ 'Pipfile': ['Python'], 'go.mod': ['Go'], 'Cargo.toml': ['Rust'], 'Gemfile': ['Ruby'],
30
+ 'pom.xml': ['Java', 'Maven'], 'build.gradle': ['Java', 'Gradle'], 'composer.json': ['PHP'],
31
+ 'mix.exs': ['Elixir'], 'CMakeLists.txt': ['C++', 'CMake'], 'pubspec.yaml': ['Dart', 'Flutter'],
32
+ 'tsconfig.json': ['TypeScript'], 'next.config.js': ['Next.js'], 'Dockerfile': ['Docker'],
33
+ };
34
+
35
+ function detectStacks(cwd) {
36
+ const found = new Set();
37
+ if (process.platform === 'darwin') found.add('macOS');
38
+ if (process.platform === 'win32') found.add('Windows');
39
+ if (process.platform === 'linux') found.add('Linux');
40
+ let files = [];
41
+ try { files = fs.readdirSync(cwd); } catch { }
42
+ for (const f of files) (DETECT[f] || []).forEach((s) => found.add(s));
43
+ return [...found];
44
+ }
45
+
46
+ async function runGitignoreCommand(argv) {
47
+ const cwd = process.cwd();
48
+ const printOnly = argv.includes('--print');
49
+ const nonInteractive = printOnly || !process.stdin.isTTY || argv.includes('--yes') || argv.includes('-y');
50
+
51
+ const RESERVED = new Set(['select', 'provider', 'model', 'append', 'force', 'print', 'yes']);
52
+ let stacks = argv.filter((a) => a.startsWith('--') && !RESERVED.has(a.slice(2)) && a !== '-y').map((a) => a.slice(2));
53
+
54
+ if (argv.includes('--select')) {
55
+ if (nonInteractive) { console.error(`${c('31', '✗')} --select needs a TTY. Pass stacks as --node --python …`); return 1; }
56
+ const ans = await ask('Stacks/tools (comma separated, e.g. Node, TypeScript, Docker): ');
57
+ stacks = ans.split(',').map((s) => s.trim()).filter(Boolean);
58
+ } else if (stacks.length === 0) {
59
+ stacks = detectStacks(cwd);
60
+ if (stacks.length === 0) {
61
+ console.error(`${c('31', '✗')} Could not detect a stack. Use --select or --<stack> (e.g. --node).`);
62
+ return 1;
63
+ }
64
+ console.log(`${c('36', 'detected')}: ${stacks.join(', ')}`);
65
+ if (!nonInteractive) {
66
+ const ok = (await ask('Generate .gitignore for these? [Y/n] ')).toLowerCase();
67
+ if (ok === 'n') { console.log('Aborted.'); return 0; }
68
+ }
69
+ }
70
+
71
+ const giPath = path.join(cwd, '.gitignore');
72
+ const existing = fs.existsSync(giPath) ? fs.readFileSync(giPath, 'utf8') : '';
73
+
74
+ let result;
75
+ try {
76
+ process.stderr.write(c('90', `… generating .gitignore (${stacks.join(', ')})\n`));
77
+ result = await genLocal.generate({
78
+ tool: 'gitignore',
79
+ ctx: { stack: stacks.join(', '), previous: argv.includes('--append') ? existing : '' },
80
+ provider: flag(argv, '--provider'),
81
+ model: flag(argv, '--model'),
82
+ maxTokens: 2048,
83
+ });
84
+ } catch (e) {
85
+ if (e instanceof genLocal.AIError) { console.error(`${c('31', '✗')} AI provider error (${e.code}): ${e.message}`); return 2; }
86
+ console.error(`${c('31', '✗')} ${e.message}`);
87
+ return /gitset config/.test(e.message || '') ? 1 : 2;
88
+ }
89
+
90
+ const body = result.text.trim() + '\n';
91
+ if (printOnly) { process.stdout.write(body); return 0; }
92
+
93
+ let finalContent = body;
94
+ let mode = 'created';
95
+ if (existing) {
96
+ let action = argv.includes('--append') ? 'append' : argv.includes('--force') ? 'overwrite' : null;
97
+ if (!action) {
98
+ if (nonInteractive) { console.error(`${c('31', '✗')} .gitignore exists. Use --append or --force.`); return 1; }
99
+ action = (await ask('.gitignore exists — [a]ppend / [o]verwrite / [c]ancel? ')).toLowerCase();
100
+ action = action === 'a' ? 'append' : action === 'o' ? 'overwrite' : 'cancel';
101
+ }
102
+ if (action === 'cancel') { console.log('Aborted.'); return 0; }
103
+ if (action === 'append') { finalContent = existing.replace(/\n*$/, '\n') + '\n# --- added by gitset ---\n' + body; mode = 'updated'; }
104
+ }
105
+ fs.writeFileSync(giPath, finalContent);
106
+ console.log(`${c('32', '✓')} .gitignore ${mode} (${stacks.join(', ')}, via ${result.provider})`);
107
+ return 0;
108
+ }
109
+
110
+ module.exports = { runGitignoreCommand };
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `gitset init` — scaffold local template files in ~/.gitset/templates.
5
+ * No auth, no backend (Gitset v2 is BYOAI: there is no login).
6
+ */
7
+ const fs = require('fs');
8
+ const os = require('os');
9
+ const path = require('path');
10
+ const theme = require('../utils/theme');
11
+
12
+ const DIR = process.env.GITSET_CONFIG_DIR || path.join(os.homedir(), '.gitset');
13
+ const TPL_DIR = path.join(DIR, 'templates');
14
+
15
+ const TEMPLATES = {
16
+ 'commit.md': `# Commit style\nUse Conventional Commits. Imperative subject <= 72 chars.\nBody explains the WHY for non-trivial changes.\n`,
17
+ 'pr.md': `## Summary\n\n## Changes\n\n## Testing\n`,
18
+ 'issue.md': `## Summary\n\n## Steps / Acceptance criteria\n\n## Scope\n`,
19
+ 'readme.md': `# {{project}}\n\n> short description\n\n## Install\n\n## Usage\n`,
20
+ 'release.md': `## Highlights\n\n## Features\n\n## Fixes\n`,
21
+ };
22
+
23
+ function runInitCommand() {
24
+ fs.mkdirSync(TPL_DIR, { recursive: true, mode: 0o700 });
25
+ let created = 0;
26
+ for (const [name, body] of Object.entries(TEMPLATES)) {
27
+ const f = path.join(TPL_DIR, name);
28
+ if (!fs.existsSync(f)) { fs.writeFileSync(f, body); created++; }
29
+ }
30
+ console.log(`${theme.success('✓')} Templates ready at ${TPL_DIR} (${created} created, ${Object.keys(TEMPLATES).length - created} existing)`);
31
+ console.log(theme.bold('\nGitset is BYOAI — no login required. Next:'));
32
+ console.log(` ${theme.accent('gitset config')} setup wizard (or gitset config set <provider> --key <key>)`);
33
+ console.log(` ${theme.accent('gitset commit')} generate from staged changes`);
34
+ console.log(` ${theme.accent('gitset template edit commit')} customize this tool's format\n`);
35
+ return 0;
36
+ }
37
+
38
+ module.exports = { runInitCommand };
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `gitset labelspack` — manage & apply a label pack. Local pack +
5
+ * the user's `gh` CLI only. No backend, injection-safe (arg-array exec).
6
+ *
7
+ * gitset labelspack list the pack
8
+ * gitset labelspack --apply create/update those labels in the repo via gh
9
+ * gitset labelspack --init write the default pack to ~/.gitset/labels.md
10
+ * gitset labelspack --add --name x --color hex --description "..."
11
+ */
12
+ const { execFileSync } = require('child_process');
13
+ const readline = require('readline');
14
+ const { getLabels, addLabel, writeDefaultPack, getRepoLabels } = require('../utils/labels');
15
+
16
+ const tty = () => process.stdout.isTTY;
17
+ const c = (code, s) => (tty() ? `\x1b[${code}m${s}\x1b[0m` : s);
18
+ const ask = (q) => new Promise((r) => {
19
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
20
+ rl.question(q, (a) => { rl.close(); r(a.trim()); });
21
+ });
22
+ const flag = (argv, n) => {
23
+ const i = argv.indexOf(n);
24
+ return i !== -1 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : null;
25
+ };
26
+
27
+ function ghAvailable() {
28
+ try { execFileSync('gh', ['--version'], { stdio: 'ignore' }); return true; } catch { return false; }
29
+ }
30
+
31
+ async function runLabelspackCommand(argv) {
32
+ if (argv.includes('--init')) {
33
+ const f = writeDefaultPack();
34
+ console.log(`${c('32', '✓')} Wrote default label pack → ${f}`);
35
+ return 0;
36
+ }
37
+
38
+ if (argv.includes('--add')) {
39
+ let name = flag(argv, '--name');
40
+ let color = flag(argv, '--color');
41
+ let description = flag(argv, '--description');
42
+ if (!name) {
43
+ if (!process.stdin.isTTY) { console.error(`${c('31', '✗')} --add needs --name (and optionally --color/--description).`); return 1; }
44
+ name = await ask('Label name: ');
45
+ color = await ask('Color (hex, blank=random): ');
46
+ description = await ask('Description: ');
47
+ }
48
+ if (!name) { console.error(`${c('31', '✗')} Name required.`); return 1; }
49
+ addLabel({ name, color: color || '', description: description || '' });
50
+ console.log(`${c('32', '✓')} Added "${name}" to the pack.`);
51
+ return 0;
52
+ }
53
+
54
+ const { source, labels } = getLabels();
55
+
56
+ if (!argv.includes('--apply') && !argv.includes('--sync')) {
57
+ console.log(c('1', `\nLabel pack (${source}) — ${labels.length} labels\n`));
58
+ for (const l of labels) {
59
+ console.log(` ${c('36', l.name)}${l.description ? ` ${c('90', l.description)}` : ''}`);
60
+ }
61
+ console.log(c('90', '\nApply to the current repo: gitset labelspack --apply\n'));
62
+ return 0;
63
+ }
64
+
65
+ if (!ghAvailable()) {
66
+ console.error(`${c('31', '✗')} GitHub CLI \`gh\` not found / not authenticated.`);
67
+ return 1;
68
+ }
69
+ if (!process.stdin.isTTY || argv.includes('--yes') || argv.includes('-y')) {
70
+ } else {
71
+ const ok = (await ask(`Apply ${labels.length} labels (${source}) to the current repo? [y/N] `)).toLowerCase();
72
+ if (ok !== 'y') { console.log('Aborted.'); return 0; }
73
+ }
74
+
75
+ const existing = new Set(getRepoLabels().map((l) => l.name));
76
+ let created = 0; let updated = 0; let failed = 0;
77
+ for (const l of labels) {
78
+ const color = String(l.color || '').replace('#', '') || 'ededed';
79
+ const sub = existing.has(l.name) ? 'edit' : 'create';
80
+ try {
81
+ execFileSync('gh', ['label', sub, l.name, '--color', color, '--description', l.description || ''],
82
+ { stdio: 'ignore' });
83
+ if (sub === 'create') { created++; console.log(`${c('32', '✓')} created ${l.name}`); }
84
+ else { updated++; console.log(`${c('36', '↻')} updated ${l.name}`); }
85
+ } catch {
86
+ failed++;
87
+ console.error(`${c('31', '✗')} failed ${l.name}`);
88
+ }
89
+ }
90
+ console.log(c('1', `\n${created} created · ${updated} updated · ${failed} failed`));
91
+ return failed > 0 ? 1 : 0;
92
+ }
93
+
94
+ module.exports = { runLabelspackCommand };