@gitset-dev/cli 1.2.0 → 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,92 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `gitset template` — manage LOCAL templates in ~/.gitset/templates.
5
+ * No remote template library (that was the old hosted system).
6
+ * gitset template list
7
+ * gitset template show <name>
8
+ * gitset template edit <name>
9
+ * gitset template path
10
+ */
11
+ const fs = require('fs');
12
+ const os = require('os');
13
+ const path = require('path');
14
+ const { spawnSync } = require('child_process');
15
+ const theme = require('../utils/theme');
16
+
17
+ const DIR = process.env.GITSET_CONFIG_DIR || path.join(os.homedir(), '.gitset');
18
+ const TPL_DIR = path.join(DIR, 'templates');
19
+ const KNOWN_TOOLS = ['commit', 'pr', 'issue', 'readme', 'release'];
20
+
21
+ const DEFAULTS = {
22
+ 'commit.md': `# Commit style\nUse Conventional Commits. Imperative subject <= 72 chars.\nBody explains the WHY for non-trivial changes.\n`,
23
+ 'pr.md': `## Summary\n\n## Changes\n\n## Testing\n`,
24
+ 'issue.md': `## Summary\n\n## Steps / Acceptance criteria\n\n## Scope\n`,
25
+ 'readme.md': `# {{project}}\n\n> short description\n\n## Install\n\n## Usage\n`,
26
+ 'release.md': `## Highlights\n\n## Features\n\n## Fixes\n`,
27
+ };
28
+
29
+ function listFiles() {
30
+ try { return fs.readdirSync(TPL_DIR).filter((f) => f.endsWith('.md')).sort(); }
31
+ catch { return []; }
32
+ }
33
+
34
+ function runTemplateCommand(argv) {
35
+ const sub = argv[0] || 'list';
36
+
37
+ if (sub === 'path') { console.log(TPL_DIR); return 0; }
38
+
39
+ if (sub === 'list') {
40
+ const files = listFiles();
41
+ if (files.length === 0) {
42
+ console.log(`No local templates. Run ${theme.accent('gitset init')} to scaffold them, or ${theme.accent('gitset template edit <tool>')} to create one now.`);
43
+ return 0;
44
+ }
45
+ console.log(theme.bold(`Local templates (${TPL_DIR}):`));
46
+ for (const f of files) console.log(` ${f.replace(/\.md$/, '')}`);
47
+ console.log();
48
+ console.log(theme.dim('gitset template edit <name> open one in your editor'));
49
+ return 0;
50
+ }
51
+
52
+ if (sub === 'show') {
53
+ const name = argv[1];
54
+ if (!name) { console.error(`${theme.error('✗')} Usage: gitset template show <name>`); return 1; }
55
+ const f = path.join(TPL_DIR, name.endsWith('.md') ? name : `${name}.md`);
56
+ if (!fs.existsSync(f)) { console.error(`${theme.error('✗')} No template "${name}". Try: gitset template list`); return 1; }
57
+ process.stdout.write(fs.readFileSync(f, 'utf8'));
58
+ return 0;
59
+ }
60
+
61
+ if (sub === 'edit') {
62
+ const name = argv[1];
63
+ if (!name) {
64
+ console.error(`${theme.error('✗')} Usage: gitset template edit <${KNOWN_TOOLS.join('|')}>`);
65
+ return 1;
66
+ }
67
+ const base = name.endsWith('.md') ? name : `${name}.md`;
68
+ const f = path.join(TPL_DIR, base);
69
+ if (!fs.existsSync(f)) {
70
+ fs.mkdirSync(TPL_DIR, { recursive: true, mode: 0o700 });
71
+ fs.writeFileSync(f, DEFAULTS[base] || `# ${name} template\n`);
72
+ console.log(theme.dim(`Created ${f} from the default.`));
73
+ }
74
+ const editor = process.env.VISUAL || process.env.EDITOR;
75
+ if (!editor) {
76
+ console.log(`No $EDITOR set. Open it yourself:\n ${f}`);
77
+ return 0;
78
+ }
79
+ const result = spawnSync(editor, [f], { stdio: 'inherit' });
80
+ if (result.error) {
81
+ console.error(`${theme.error('✗')} Could not launch "${editor}". Edit the file directly:\n ${f}`);
82
+ return 1;
83
+ }
84
+ console.log(`${theme.success('✓')} Saved ${base.replace(/\.md$/, '')} template.`);
85
+ return 0;
86
+ }
87
+
88
+ console.error(`${theme.error('✗')} Unknown subcommand "${sub}". Use: list | show <name> | edit <name> | path`);
89
+ return 1;
90
+ }
91
+
92
+ module.exports = { runTemplateCommand };
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `gitset tree` — local repo structure visualizer. No backend, no AI.
5
+ * Flags: --all (include dotfiles) --depth <n> --exclude <pat> (repeatable)
6
+ * --no-gitignore (don't honor .gitignore)
7
+ */
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const tty = () => process.stdout.isTTY;
12
+ const c = (code, s) => (tty() ? `\x1b[${code}m${s}\x1b[0m` : s);
13
+
14
+ function loadGitignore(root) {
15
+ const f = path.join(root, '.gitignore');
16
+ if (!fs.existsSync(f)) return [];
17
+ return fs.readFileSync(f, 'utf8').split('\n')
18
+ .map((l) => l.trim()).filter((l) => l && !l.startsWith('#'))
19
+ .map((l) => l.replace(/\/$/, ''));
20
+ }
21
+
22
+ function makeExcluder(patterns) {
23
+ const set = new Set(['.git', ...patterns]);
24
+ return (name) => {
25
+ if (set.has(name)) return true;
26
+ for (const p of patterns) {
27
+ if (p.startsWith('*.') && name.endsWith(p.slice(1))) return true;
28
+ if (p === name) return true;
29
+ }
30
+ return false;
31
+ };
32
+ }
33
+
34
+ function walk(dir, prefix, opts, depth, stats) {
35
+ if (opts.maxDepth >= 0 && depth > opts.maxDepth) return;
36
+ let entries;
37
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
38
+ entries = entries
39
+ .filter((e) => opts.all || !e.name.startsWith('.') || e.name === '.github' || e.name === '.gitignore')
40
+ .filter((e) => !opts.exclude(e.name))
41
+ .sort((a, b) => (a.isDirectory() === b.isDirectory() ? a.name.localeCompare(b.name) : a.isDirectory() ? -1 : 1));
42
+
43
+ entries.forEach((e, i) => {
44
+ const last = i === entries.length - 1;
45
+ const branch = last ? '└── ' : '├── ';
46
+ if (e.isDirectory()) {
47
+ stats.dirs++;
48
+ console.log(prefix + branch + c('36', e.name + '/'));
49
+ walk(path.join(dir, e.name), prefix + (last ? ' ' : '│ '), opts, depth + 1, stats);
50
+ } else {
51
+ stats.files++;
52
+ console.log(prefix + branch + e.name);
53
+ }
54
+ });
55
+ }
56
+
57
+ function runTreeCommand(argv) {
58
+ const root = process.cwd();
59
+ const exclude = [];
60
+ for (let i = 0; i < argv.length; i++) {
61
+ if (argv[i] === '--exclude' && argv[i + 1]) { exclude.push(argv[i + 1]); i++; }
62
+ }
63
+ const depthIdx = argv.indexOf('--depth');
64
+ const maxDepth = depthIdx !== -1 && argv[depthIdx + 1] ? parseInt(argv[depthIdx + 1], 10) : -1;
65
+ const useGitignore = !argv.includes('--no-gitignore');
66
+ const patterns = [...exclude, ...(useGitignore ? loadGitignore(root) : [])];
67
+
68
+ const opts = { all: argv.includes('--all'), maxDepth, exclude: makeExcluder(patterns) };
69
+ const stats = { dirs: 0, files: 0 };
70
+
71
+ console.log(c('1', path.basename(root) + '/'));
72
+ walk(root, '', opts, 0, stats);
73
+ console.log(c('90', `\n${stats.dirs} directories, ${stats.files} files`));
74
+ return 0;
75
+ }
76
+
77
+ module.exports = { runTreeCommand };
@@ -0,0 +1,101 @@
1
+ class DependabotAnalyzer {
2
+ constructor() {
3
+ this.RISK_LEVELS = {
4
+ NONE: 'RISK_NONE',
5
+ LOW: 'RISK_LOW_BEHAVIORAL',
6
+ MODERATE: 'RISK_MODERATE_API',
7
+ HIGH: 'RISK_HIGH_BREAKING'
8
+ };
9
+ }
10
+
11
+ parseSemver(version) {
12
+ const cleanVersion = version.replace(/^v/, '');
13
+ const parts = cleanVersion.split('.').map(Number);
14
+
15
+ while (parts.length < 3) {
16
+ parts.push(0);
17
+ }
18
+
19
+ return {
20
+ major: parts[0],
21
+ minor: parts[1],
22
+ patch: parts[2],
23
+ original: version
24
+ };
25
+ }
26
+
27
+ getUpdateType(current, target) {
28
+ const v1 = this.parseSemver(current);
29
+ const v2 = this.parseSemver(target);
30
+
31
+ if (v1.major === v2.major && v1.minor === v2.minor && v1.patch === v2.patch) return 'EQUAL';
32
+ if (v2.major > v1.major) return 'MAJOR';
33
+ if (v2.minor > v1.minor) return 'MINOR';
34
+ if (v2.patch > v1.patch) return 'PATCH';
35
+ return 'UNKNOWN';
36
+ }
37
+
38
+ analyzeRisk(alert) {
39
+ const dependency = alert.dependency.package.name;
40
+ const ecosystem = alert.dependency.package.ecosystem;
41
+
42
+ const patchedVersion = alert.security_advisory.patched_versions
43
+ ? alert.security_advisory.patched_versions[0]
44
+ : null;
45
+
46
+ if (!patchedVersion) {
47
+ return {
48
+ level: this.RISK_LEVELS.HIGH,
49
+ reason: 'Could not determine patched version',
50
+ updateType: 'UNKNOWN'
51
+ };
52
+ }
53
+
54
+ return {
55
+ level: this.RISK_LEVELS.HIGH,
56
+ targetVersion: patchedVersion.identifier,
57
+ reason: 'Pending local file analysis'
58
+ };
59
+ }
60
+
61
+ calculateRisk(currentVersion, targetVersion, ecosystem) {
62
+ const updateType = this.getUpdateType(currentVersion, targetVersion);
63
+ let risk = this.RISK_LEVELS.HIGH;
64
+ let reason = '';
65
+
66
+ switch (updateType) {
67
+ case 'EQUAL':
68
+ risk = this.RISK_LEVELS.NONE;
69
+ reason = 'Already on patched version';
70
+ break;
71
+ case 'MAJOR':
72
+ risk = this.RISK_LEVELS.HIGH;
73
+ reason = 'Major version update - likely breaking changes';
74
+ break;
75
+ case 'MINOR':
76
+
77
+ if (ecosystem === 'npm' || ecosystem === 'pip') {
78
+ risk = this.RISK_LEVELS.LOW;
79
+ } else {
80
+ risk = this.RISK_LEVELS.MODERATE;
81
+ }
82
+ reason = 'Minor version update - check changelog for behavioral changes';
83
+ break;
84
+ case 'PATCH':
85
+ risk = this.RISK_LEVELS.NONE;
86
+ reason = 'Patch update - bug fixes only';
87
+ break;
88
+ default:
89
+ risk = this.RISK_LEVELS.HIGH;
90
+ reason = 'Unknown update type';
91
+ }
92
+
93
+ return {
94
+ level: risk,
95
+ reason,
96
+ updateType
97
+ };
98
+ }
99
+ }
100
+
101
+ module.exports = DependabotAnalyzer;
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Local label-pack util. No backend, no gitset_key — the old remote
5
+ * /api/issue label CRUD has been removed. Source of truth is a local
6
+ * ~/.gitset/labels.md (a fenced ```yaml block) or a built-in default set.
7
+ * Live repo labels are read via the user's authenticated `gh` CLI.
8
+ */
9
+ const fs = require('fs');
10
+ const os = require('os');
11
+ const path = require('path');
12
+ const { execFileSync } = require('child_process');
13
+
14
+ const DIR = process.env.GITSET_CONFIG_DIR || path.join(os.homedir(), '.gitset');
15
+ const LABELS_FILE = path.join(DIR, 'labels.md');
16
+
17
+ const DEFAULT_LABELS = [
18
+ { name: 'bug', color: 'd73a4a', description: "Something isn't working" },
19
+ { name: 'enhancement', color: 'a2eeef', description: 'New feature or request' },
20
+ { name: 'documentation', color: '0075ca', description: 'Documentation changes' },
21
+ { name: 'question', color: 'd876e3', description: 'Further information requested' },
22
+ { name: 'good first issue', color: '7057ff', description: 'Good for newcomers' },
23
+ { name: 'help wanted', color: '008672', description: 'Extra attention is needed' },
24
+ { name: 'dependencies', color: '0366d6', description: 'Dependency updates' },
25
+ { name: 'security', color: 'b60205', description: 'Security-related' },
26
+ { name: 'refactor', color: 'fbca04', description: 'Code refactor, no behavior change' },
27
+ { name: 'wontfix', color: 'ffffff', description: 'This will not be worked on' },
28
+ { name: 'duplicate', color: 'cfd3d7', description: 'Already exists' },
29
+ ];
30
+
31
+ function parseLabelsYaml(content) {
32
+ const labels = [];
33
+ let cur = null;
34
+ for (let line of String(content).split('\n')) {
35
+ line = line.trim();
36
+ if (!line || line.startsWith('#')) continue;
37
+ if (line.startsWith('- name:')) {
38
+ if (cur) labels.push(cur);
39
+ cur = { name: unquote(line.replace('- name:', '').trim()) };
40
+ } else if (cur && line.startsWith('color:')) {
41
+ cur.color = unquote(line.replace('color:', '').trim());
42
+ } else if (cur && line.startsWith('description:')) {
43
+ cur.description = unquote(line.replace('description:', '').trim());
44
+ }
45
+ }
46
+ if (cur) labels.push(cur);
47
+ return labels;
48
+ }
49
+ function unquote(s) {
50
+ return (s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'")) ? s.slice(1, -1) : s;
51
+ }
52
+
53
+ function serializePack(labels) {
54
+ const body = labels.map((l) =>
55
+ ` - name: "${l.name}"\n color: "${(l.color || '').replace('#', '')}"\n description: "${l.description || ''}"`).join('\n');
56
+ return `# Gitset label pack — edit freely.\n\n\`\`\`yaml\n${body}\n\`\`\`\n`;
57
+ }
58
+
59
+ function getLabels() {
60
+ if (fs.existsSync(LABELS_FILE)) {
61
+ const content = fs.readFileSync(LABELS_FILE, 'utf8');
62
+ const m = content.match(/```ya?ml([\s\S]*?)```/);
63
+ const labels = m ? parseLabelsYaml(m[1]) : parseLabelsYaml(content);
64
+ if (labels.length) return { source: `local ${path.relative(os.homedir(), LABELS_FILE) || LABELS_FILE}`, labels };
65
+ }
66
+ return { source: 'built-in default pack', labels: DEFAULT_LABELS };
67
+ }
68
+
69
+ function writeDefaultPack() {
70
+ fs.mkdirSync(DIR, { recursive: true, mode: 0o700 });
71
+ fs.writeFileSync(LABELS_FILE, serializePack(DEFAULT_LABELS));
72
+ return LABELS_FILE;
73
+ }
74
+
75
+ function addLabel(label) {
76
+ if (!label || !label.name) return false;
77
+ const { labels } = fs.existsSync(LABELS_FILE) ? getLabels() : { labels: [...DEFAULT_LABELS] };
78
+ if (labels.some((l) => l.name === label.name)) {
79
+ const i = labels.findIndex((l) => l.name === label.name);
80
+ labels[i] = { ...labels[i], ...label };
81
+ } else {
82
+ labels.push({ name: label.name, color: (label.color || '').replace('#', ''), description: label.description || '' });
83
+ }
84
+ fs.mkdirSync(DIR, { recursive: true, mode: 0o700 });
85
+ fs.writeFileSync(LABELS_FILE, serializePack(labels));
86
+ return true;
87
+ }
88
+
89
+ function getRepoLabels() {
90
+ try {
91
+ const out = execFileSync('gh', ['label', 'list', '--limit', '200', '--json', 'name,color,description'],
92
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
93
+ return JSON.parse(out);
94
+ } catch {
95
+ return [];
96
+ }
97
+ }
98
+
99
+ module.exports = {
100
+ LABELS_FILE, DEFAULT_LABELS,
101
+ getLabels, addLabel, writeDefaultPack, getRepoLabels, parseLabelsYaml,
102
+ };
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Shared terminal color system — same teal accent as the Gitset web app
5
+ * (see gitset-web/DESIGN.md), split into a dark-terminal tone and a
6
+ * light-terminal tone because a bright cyan is unreadable on a white
7
+ * background. Respects NO_COLOR (https://no-color.org) and non-TTY output.
8
+ */
9
+ const config = require('../../lib/config');
10
+
11
+ const tty = () => Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
12
+
13
+ // Same hue family (~178°) as the web's --brand token, at two lightness
14
+ // levels per mode so there's still hierarchy (primary vs. quieter accent)
15
+ // even in a small terminal palette.
16
+ const PALETTE = {
17
+ dark: {
18
+ accent: [108, 224, 219], // #6CE0DB
19
+ accentDim: [79, 173, 168],
20
+ },
21
+ light: {
22
+ accent: [25, 118, 114], // #197672
23
+ accentDim: [15, 83, 80],
24
+ },
25
+ };
26
+
27
+ function detectDefaultMode() {
28
+ // Many terminals set COLORFGBG as "fg;bg" (0-15 ANSI index). A light
29
+ // background index means a light theme; anything else, assume dark —
30
+ // by far the more common terminal default.
31
+ const fgbg = process.env.COLORFGBG;
32
+ if (fgbg) {
33
+ const bg = parseInt(fgbg.split(';').pop(), 10);
34
+ if (!Number.isNaN(bg) && bg >= 7) return 'light';
35
+ }
36
+ return 'dark';
37
+ }
38
+
39
+ function mode() {
40
+ const stored = config.getTheme();
41
+ return stored === 'light' ? 'light' : stored === 'dark' ? 'dark' : detectDefaultMode();
42
+ }
43
+
44
+ function rgb(triplet, s) {
45
+ if (!tty()) return s;
46
+ const [r, g, b] = triplet;
47
+ return `\x1b[38;2;${r};${g};${b}m${s}\x1b[0m`;
48
+ }
49
+ function code(n, s) {
50
+ return tty() ? `\x1b[${n}m${s}\x1b[0m` : s;
51
+ }
52
+
53
+ const accent = (s) => rgb(PALETTE[mode()].accent, s);
54
+ const accentDim = (s) => rgb(PALETTE[mode()].accentDim, s);
55
+ const bold = (s) => code('1', s);
56
+ const dim = (s) => code('2', s);
57
+ const error = (s) => code('31', s);
58
+ const warn = (s) => code('33', s);
59
+ // No separate "success" hue, mirroring the web decision: the brand accent
60
+ // IS the affirmative color everywhere, not a second green.
61
+ const success = (s) => accent(s);
62
+
63
+ // Highlights the bracketed letter of a menu hint in accent+bold and leaves
64
+ // the rest of the word plain, e.g. key('a', 'ccept') -> "[a]ccept" with
65
+ // only "[a]" colored — makes the actionable keystroke pop at a glance.
66
+ function key(letter, rest) {
67
+ return `${accent(bold(`[${letter}]`))}${rest}`;
68
+ }
69
+
70
+ module.exports = { mode, accent, accentDim, bold, dim, error, warn, success, key, tty };
@@ -0,0 +1,173 @@
1
+ const readline = require('readline');
2
+ const theme = require('./theme');
3
+
4
+ const tty = () => process.stdout.isTTY;
5
+
6
+ // Legacy color names kept so existing call sites don't need to change, now
7
+ // routed through the shared teal theme (see theme.js) instead of a fixed
8
+ // palette that only worked on dark backgrounds. 'green' and 'cyan' both
9
+ // resolve to the same brand hue (matching the web decision: no separate
10
+ // "success" color) at two lightness tiers, which also keeps the severity
11
+ // ladder in dependabot-resolver.js (red > yellow > cyan > green) visually
12
+ // distinct without a fifth hue. Each value is a paint FUNCTION (not a raw
13
+ // ANSI prefix string) since the color depends on the live theme setting.
14
+ const colors = {
15
+ reset: (s) => s,
16
+ green: theme.accent,
17
+ yellow: theme.warn,
18
+ red: theme.error,
19
+ cyan: theme.accentDim,
20
+ blue: theme.dim,
21
+ pink: theme.warn,
22
+ dim: theme.dim,
23
+ };
24
+
25
+ function log(msg, color = 'reset') {
26
+ const paint = colors[color] || ((s) => s);
27
+ console.log(paint(msg));
28
+ }
29
+
30
+ function askQuestion(query) {
31
+ if (!process.stdin.isTTY) {
32
+ return Promise.reject(new Error(
33
+ 'This step needs an interactive terminal. Re-run in a terminal, or pass explicit flags to skip prompts.',
34
+ ));
35
+ }
36
+ const rl = readline.createInterface({
37
+ input: process.stdin,
38
+ output: process.stdout,
39
+ });
40
+
41
+ return new Promise((resolve, reject) => {
42
+ let answered = false;
43
+ rl.question(query, (ans) => {
44
+ answered = true;
45
+ rl.close();
46
+ resolve(ans.trim());
47
+ });
48
+ rl.on('close', () => {
49
+ if (!answered) reject(new Error('Input closed before answering.'));
50
+ });
51
+ });
52
+ }
53
+
54
+ // Masks input as it's typed (for API keys) instead of echoing it in plain
55
+ // text. Falls back to visible input if the terminal doesn't support raw
56
+ // mode (e.g. some CI wrappers) rather than failing outright.
57
+ function askSecret(query) {
58
+ if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function') {
59
+ return askQuestion(query);
60
+ }
61
+ return new Promise((resolve, reject) => {
62
+ process.stdout.write(query);
63
+ const stdin = process.stdin;
64
+ const wasRaw = stdin.isRaw;
65
+ stdin.setRawMode(true);
66
+ stdin.resume();
67
+ stdin.setEncoding('utf8');
68
+
69
+ let value = '';
70
+ function cleanup() {
71
+ stdin.removeListener('data', onData);
72
+ stdin.setRawMode(wasRaw || false);
73
+ stdin.pause();
74
+ }
75
+ function onData(chunk) {
76
+ const ch = String(chunk);
77
+ if (ch === '\r' || ch === '\n') {
78
+ cleanup();
79
+ process.stdout.write('\n');
80
+ resolve(value.trim());
81
+ return;
82
+ }
83
+ if (ch === '\u0003') { // Ctrl+C
84
+ cleanup();
85
+ process.stdout.write('\n');
86
+ reject(new Error('Cancelled.'));
87
+ return;
88
+ }
89
+ if (ch === '\u007f' || ch === '\b') { // backspace (DEL on most terminals, BS on some)
90
+ if (value.length) {
91
+ value = value.slice(0, -1);
92
+ process.stdout.write('\b \b');
93
+ }
94
+ return;
95
+ }
96
+ if (ch.charCodeAt(0) < 32) return; // ignore other control/escape bytes
97
+ value += ch;
98
+ process.stdout.write('*');
99
+ }
100
+ stdin.on('data', onData);
101
+ });
102
+ }
103
+
104
+ async function selectOption(title, options) {
105
+ log(`\n${title}`, 'reset');
106
+ options.forEach((opt, i) => {
107
+ log(`${i + 1}. ${opt.label}`, 'cyan');
108
+ });
109
+
110
+ while (true) {
111
+ const answer = await askQuestion('\nSelect an option (number): ');
112
+ const index = parseInt(answer) - 1;
113
+ if (index >= 0 && index < options.length) {
114
+ return options[index].value;
115
+ }
116
+ log('Invalid selection', 'red');
117
+ }
118
+ }
119
+
120
+ async function selectMultipleOptions(title, options) {
121
+ log(`\n${title}`, 'reset');
122
+ options.forEach((opt, i) => {
123
+ log(`${i + 1}. ${opt.label}`, 'cyan');
124
+ });
125
+ log('0. Done / Confirm Selection', 'green');
126
+ log('C. Cancel', 'red');
127
+
128
+ const selectedValues = new Set();
129
+
130
+ while (true) {
131
+ const currentSelection = options
132
+ .filter(opt => selectedValues.has(opt.value))
133
+ .map(opt => opt.label)
134
+ .join(', ');
135
+
136
+ if (currentSelection) {
137
+ log(`\nCurrent selection: ${currentSelection}`, 'yellow');
138
+ }
139
+
140
+ const answer = await askQuestion('\nSelect options (enter number to toggle, 0 to confirm, C to cancel): ');
141
+ const index = parseInt(answer) - 1;
142
+
143
+ if (answer === '0') {
144
+ return Array.from(selectedValues);
145
+ }
146
+
147
+ if (answer.toLowerCase() === 'c') {
148
+ return null;
149
+ }
150
+
151
+ if (index >= 0 && index < options.length) {
152
+ const value = options[index].value;
153
+ if (selectedValues.has(value)) {
154
+ selectedValues.delete(value);
155
+ log(`Removed: ${options[index].label}`, 'red');
156
+ } else {
157
+ selectedValues.add(value);
158
+ log(`Added: ${options[index].label}`, 'green');
159
+ }
160
+ } else {
161
+ log('Invalid selection', 'red');
162
+ }
163
+ }
164
+ }
165
+
166
+ module.exports = {
167
+ log,
168
+ askQuestion,
169
+ askSecret,
170
+ selectOption,
171
+ selectMultipleOptions,
172
+ colors
173
+ };
@@ -1,38 +0,0 @@
1
- name: Publish to GitHub Packages
2
-
3
- on:
4
- workflow_dispatch:
5
- push:
6
- tags:
7
- - 'v*'
8
-
9
- jobs:
10
- publish-gpr:
11
- runs-on: ubuntu-latest
12
- permissions:
13
- contents: read
14
- packages: write
15
- steps:
16
- - uses: actions/checkout@v4
17
-
18
- - uses: actions/setup-node@v4
19
- with:
20
- node-version: '18'
21
- registry-url: 'https://npm.pkg.github.com'
22
- scope: '@gitset-dev'
23
-
24
- - name: Update package name for GitHub Packages
25
- run: |
26
- cp package.json package.json.backup
27
- npm pkg set name=@gitset-dev/gitset-cli
28
-
29
- - name: Publish to GitHub Packages
30
- run: |
31
- echo "registry=https://npm.pkg.github.com" > .npmrc
32
- echo "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" >> .npmrc
33
- npm publish --registry=https://npm.pkg.github.com
34
- env:
35
- NODE_AUTH_TOKEN: ${{ secrets.PAT_GITHUB }}
36
-
37
- - name: Restore original package.json
38
- run: mv package.json.backup package.json