@gitset-dev/cli 2.2.0 → 2.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -38,6 +38,7 @@ ${theme.bold('Local tools')} (no AI):
38
38
  template Manage local templates (~/.gitset/templates), edit <tool> to customize
39
39
  init Scaffold local templates
40
40
  feedback Report a bug, suggest a feature, or share feedback
41
+ auth Check GitHub access, or sync org permissions (auth sync)
41
42
 
42
43
  ${theme.bold('Setup')}:
43
44
  config interactive setup wizard
@@ -115,7 +116,9 @@ Manage & apply a label pack (local pack + gh).
115
116
  --apply create/update the labels in the current repo via gh
116
117
  --init write the default pack to ~/.gitset/labels.md
117
118
  --add --name <x> --color <hex> --description "..." add a label
118
- --sync pull the repo's labels into the local pack`,
119
+ --from-repo import the current repo's labels (name, color,
120
+ description) into the pack; --yes to overwrite
121
+ without asking (--sync is an alias)`,
119
122
  dependabot: `Usage: gitset dependabot [--resolve] [--dry-run] [--save-dev]
120
123
  Resolve Dependabot alerts for the current repo (local + gh).`,
121
124
  status: `Usage: gitset status
@@ -131,6 +134,12 @@ Scaffold the local template directory.`,
131
134
  feedback: `Usage: gitset feedback
132
135
  Interactively submit a bug report, feature suggestion, or general feedback.
133
136
  Filed as a GitHub issue in gitset-dev/gitset via your own \`gh\` auth.`,
137
+ auth: `Usage: gitset auth [status|sync]
138
+ status (default) show your GitHub CLI auth status and access guidance
139
+ sync re-run GitHub authorization to approve additional
140
+ organizations or scopes (wraps \`gh auth refresh\`)
141
+ Gitset uses your own gh authentication locally — there is no Gitset account.
142
+ Web app permissions: gitset.dev/dashboard → GitHub connection.`,
134
143
  config: `Usage: gitset config [set|list|remove|theme|path]
135
144
  (no args) interactive setup wizard
136
145
  set interactive wizard, or: set <provider> --key <key> [--model m] [--base-url u] [--default]
@@ -209,7 +218,10 @@ async function main() {
209
218
  case 'feedback':
210
219
  code = (await require('./src/commands/feedback')()) || 0; break;
211
220
 
212
- case 'auth': case 'verify': case 'logout':
221
+ case 'auth':
222
+ code = require('./src/commands/auth')(rest) || 0; break;
223
+
224
+ case 'verify': case 'logout':
213
225
  code = deprecatedAuth(command); break;
214
226
 
215
227
  case 'help': case '--help': case '-h': case undefined:
@@ -0,0 +1,127 @@
1
+ // VENDORED from gitset-core-v2/lib/manifest — do not edit here. Run `pnpm sync:ai`.
2
+ 'use strict';
3
+
4
+ const MANIFESTS = [
5
+ { type: 'npm', file: 'package.json', label: 'package.json (npm)' },
6
+ { type: 'cargo', file: 'Cargo.toml', label: 'Cargo.toml (Cargo)' },
7
+ { type: 'python', file: 'pyproject.toml', label: 'pyproject.toml (Python)' },
8
+ { type: 'gemspec', file: null, label: '*.gemspec (RubyGems)' },
9
+ ];
10
+
11
+ function listSupported() {
12
+ return MANIFESTS.map(({ type, label }) => ({ type, label }));
13
+ }
14
+
15
+ function detectManifestFile(fileNames) {
16
+ const names = Array.isArray(fileNames) ? fileNames : [];
17
+ for (const m of MANIFESTS) {
18
+ if (m.file && names.includes(m.file)) return { type: m.type, file: m.file };
19
+ }
20
+ const gem = names.find((f) => f.endsWith('.gemspec'));
21
+ if (gem) return { type: 'gemspec', file: gem };
22
+ return null;
23
+ }
24
+
25
+ function normalizeVersion(tagName) {
26
+ if (typeof tagName !== 'string') return null;
27
+ const trimmed = tagName.trim();
28
+ return trimmed.replace(/^v/i, '') || null;
29
+ }
30
+
31
+ function detectIndent(text) {
32
+ const m = text.match(/^[ \t]+(?=")/m);
33
+ return m ? m[0] : ' ';
34
+ }
35
+
36
+ function findTomlSection(content, sectionName) {
37
+ const escaped = sectionName.replace(/\./g, '\\.');
38
+ const headerRe = new RegExp(`^\\[${escaped}\\][ \\t]*$`, 'm');
39
+ const m = headerRe.exec(content);
40
+ if (!m) return null;
41
+ const bodyStart = m.index + m[0].length;
42
+ const rest = content.slice(bodyStart);
43
+ const next = rest.match(/^\[[^\]]+\][ \t]*$/m);
44
+ const bodyEnd = next ? bodyStart + next.index : content.length;
45
+ return { bodyStart, bodyEnd };
46
+ }
47
+
48
+ const TOML_VERSION_LINE_RE = /^([ \t]*version[ \t]*=[ \t]*)(["'])([^"']+)\2([ \t]*)$/m;
49
+
50
+ function readTomlSectionVersion(content, sectionName) {
51
+ const sec = findTomlSection(content, sectionName);
52
+ if (!sec) return null;
53
+ const body = content.slice(sec.bodyStart, sec.bodyEnd);
54
+ const m = TOML_VERSION_LINE_RE.exec(body);
55
+ return m ? m[3] : null;
56
+ }
57
+
58
+ function bumpTomlSectionVersion(content, sectionName, newVersion) {
59
+ const sec = findTomlSection(content, sectionName);
60
+ if (!sec) return null;
61
+ const body = content.slice(sec.bodyStart, sec.bodyEnd);
62
+ if (!TOML_VERSION_LINE_RE.test(body)) return null;
63
+ const newBody = body.replace(TOML_VERSION_LINE_RE, (_full, pre, quote, _old, trail) => `${pre}${quote}${newVersion}${quote}${trail}`);
64
+ return content.slice(0, sec.bodyStart) + newBody + content.slice(sec.bodyEnd);
65
+ }
66
+
67
+ const GEMSPEC_VERSION_LINE_RE = /^([ \t]*[\w.]+\.version[ \t]*=[ \t]*)(["'])([^"']+)\2([ \t]*)$/m;
68
+
69
+ function readGemspecVersion(content) {
70
+ const m = GEMSPEC_VERSION_LINE_RE.exec(content);
71
+ return m ? m[3] : null;
72
+ }
73
+
74
+ function bumpGemspecVersion(content, newVersion) {
75
+ if (!GEMSPEC_VERSION_LINE_RE.test(content)) return null;
76
+ return content.replace(GEMSPEC_VERSION_LINE_RE, (_full, pre, quote, _old, trail) => `${pre}${quote}${newVersion}${quote}${trail}`);
77
+ }
78
+
79
+ function readVersion(type, content) {
80
+ switch (type) {
81
+ case 'npm': {
82
+ const parsed = JSON.parse(content);
83
+ return typeof parsed.version === 'string' ? parsed.version : null;
84
+ }
85
+ case 'cargo':
86
+ return readTomlSectionVersion(content, 'package');
87
+ case 'python':
88
+ return readTomlSectionVersion(content, 'project') || readTomlSectionVersion(content, 'tool.poetry');
89
+ case 'gemspec':
90
+ return readGemspecVersion(content);
91
+ default:
92
+ throw new Error(`Unsupported manifest type: ${type}`);
93
+ }
94
+ }
95
+
96
+ function bumpNpmVersion(content, newVersion) {
97
+ const parsed = JSON.parse(content);
98
+ if (typeof parsed.version !== 'string') return null;
99
+ parsed.version = newVersion;
100
+ const indent = detectIndent(content);
101
+ let out = JSON.stringify(parsed, null, indent);
102
+ if (content.endsWith('\n')) out += '\n';
103
+ return out;
104
+ }
105
+
106
+ function bumpVersion(type, content, newVersion) {
107
+ switch (type) {
108
+ case 'npm':
109
+ return bumpNpmVersion(content, newVersion);
110
+ case 'cargo':
111
+ return bumpTomlSectionVersion(content, 'package', newVersion);
112
+ case 'python':
113
+ return bumpTomlSectionVersion(content, 'project', newVersion) || bumpTomlSectionVersion(content, 'tool.poetry', newVersion);
114
+ case 'gemspec':
115
+ return bumpGemspecVersion(content, newVersion);
116
+ default:
117
+ throw new Error(`Unsupported manifest type: ${type}`);
118
+ }
119
+ }
120
+
121
+ module.exports = {
122
+ listSupported,
123
+ detectManifestFile,
124
+ normalizeVersion,
125
+ readVersion,
126
+ bumpVersion,
127
+ };
@@ -98,7 +98,13 @@ const release = {
98
98
  'headings, ordering, and tone closely — treat it as the format to reproduce. ' +
99
99
  'Never invent a repository path (e.g. placeholders like "your-project/your-repo" ' +
100
100
  'or "user/repo") for a "Full Changelog" or compare link — only include such a link ' +
101
- 'when the real repository is given below, using that exact path; otherwise omit it.',
101
+ 'when the real repository is given below, using that exact path; otherwise omit it. ' +
102
+ 'Never attribute a change to a person (e.g. "by @username") — a commit author\'s ' +
103
+ 'display name (e.g. "Jane Smith") is NOT their GitHub username and must never be ' +
104
+ 'guessed into one (e.g. never invent "@jane-smith"). If a commit message already ' +
105
+ 'ends in a real PR reference like "(#123)", you may link directly to that PR using ' +
106
+ 'the given repository path (e.g. https://github.com/<repo>/pull/123), but state only ' +
107
+ 'what changed — no author mention.',
102
108
  user: [
103
109
  repo && `Repository: ${clip(repo, 200)}`,
104
110
  tag && `Release: ${clip(tag, 100)}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitset-dev/cli",
3
- "version": "2.2.0",
3
+ "version": "2.3.3",
4
4
  "description": "Gitset CLI — drafts commits, PRs, issues, READMEs and release notes with your own AI key. Fully local: your code and keys never touch a Gitset server. Draft. Refine. Ship.",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -0,0 +1,54 @@
1
+ const { execFileSync } = require('child_process');
2
+ const { log } = require('../utils/ui');
3
+ const theme = require('../utils/theme');
4
+
5
+ function ghAvailable() {
6
+ try { execFileSync('gh', ['--version'], { stdio: 'ignore' }); return true; }
7
+ catch { return false; }
8
+ }
9
+
10
+ function showStatus() {
11
+ console.log(`\n=== Gitset Auth (via GitHub CLI) ===\n`);
12
+ try {
13
+ execFileSync('gh', ['auth', 'status'], { stdio: 'inherit' });
14
+ } catch {
15
+ log('\nYou are not logged in with the GitHub CLI. Run: gh auth login', 'yellow');
16
+ }
17
+ console.log(`
18
+ ${theme.bold('How Gitset access works')}
19
+ Locally, Gitset uses your own ${theme.accent('gh')} authentication for every GitHub
20
+ action (issues, PRs, releases, labels). No Gitset account is involved.
21
+
22
+ ${theme.bold('Missing an organization or repository?')}
23
+ - Local CLI: run ${theme.accent('gitset auth sync')} to re-run GitHub's authorization
24
+ and approve additional organizations or scopes.
25
+ - Web app: open ${theme.accent('https://gitset.dev/dashboard')} → GitHub connection →
26
+ Manage organization access.
27
+ `);
28
+ return 0;
29
+ }
30
+
31
+ function sync() {
32
+ console.log(`\n=== Gitset Auth Sync ===\n`);
33
+ log('Re-running GitHub authorization. Approve any missing organizations when your browser opens.', 'dim');
34
+ try {
35
+ execFileSync('gh', ['auth', 'refresh'], { stdio: 'inherit' });
36
+ log('\n✔ GitHub access refreshed. Newly granted organizations are now available.', 'green');
37
+ return 0;
38
+ } catch {
39
+ log('\nAuthorization was not completed. You can retry with `gitset auth sync` or run `gh auth refresh` directly.', 'red');
40
+ return 1;
41
+ }
42
+ }
43
+
44
+ module.exports = function commandAuth(rest = []) {
45
+ if (!ghAvailable()) {
46
+ log('The `gh` CLI is required (https://cli.github.com). Gitset uses your own gh authentication — no Gitset account exists.', 'red');
47
+ return 1;
48
+ }
49
+ const sub = (rest[0] || 'status').toLowerCase();
50
+ if (sub === 'sync' || sub === 'refresh') return sync();
51
+ if (sub === 'status') return showStatus();
52
+ log(`Unknown subcommand "${sub}". Use: gitset auth [status|sync]`, 'red');
53
+ return 1;
54
+ };
@@ -4,14 +4,15 @@
4
4
  * `gitset labelspack` — manage & apply a label pack. Local pack +
5
5
  * the user's `gh` CLI only. No backend, injection-safe (arg-array exec).
6
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
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 --from-repo import the current repo's labels into the pack
10
11
  * gitset labelspack --add --name x --color hex --description "..."
11
12
  */
12
13
  const { execFileSync } = require('child_process');
13
14
  const readline = require('readline');
14
- const { getLabels, addLabel, writeDefaultPack, getRepoLabels } = require('../utils/labels');
15
+ const { getLabels, addLabel, writeDefaultPack, writePack, getRepoLabels } = require('../utils/labels');
15
16
 
16
17
  const tty = () => process.stdout.isTTY;
17
18
  const c = (code, s) => (tty() ? `\x1b[${code}m${s}\x1b[0m` : s);
@@ -35,6 +36,36 @@ async function runLabelspackCommand(argv) {
35
36
  return 0;
36
37
  }
37
38
 
39
+ if (argv.includes('--from-repo') || argv.includes('--sync')) {
40
+ if (!ghAvailable()) {
41
+ console.error(`${c('31', '✗')} GitHub CLI \`gh\` not found / not authenticated.`);
42
+ return 1;
43
+ }
44
+ const repoLabels = getRepoLabels();
45
+ if (repoLabels.length === 0) {
46
+ console.error(`${c('31', '✗')} No labels found — run this inside a repository your \`gh\` account can access.`);
47
+ return 1;
48
+ }
49
+ const { source, labels: current } = getLabels();
50
+ if (source.startsWith('local') && !argv.includes('--yes') && !argv.includes('-y')) {
51
+ if (!process.stdin.isTTY) {
52
+ console.error(`${c('31', '✗')} A label pack already exists (${current.length} labels). Pass --yes to overwrite.`);
53
+ return 1;
54
+ }
55
+ const ok = (await ask(`Overwrite your existing pack (${current.length} labels) with ${repoLabels.length} labels from this repo? [y/N] `)).toLowerCase();
56
+ if (ok !== 'y') { console.log('Aborted.'); return 0; }
57
+ }
58
+ const imported = repoLabels.map((l) => ({
59
+ name: l.name,
60
+ color: String(l.color || '').replace('#', ''),
61
+ description: l.description || '',
62
+ }));
63
+ const f = writePack(imported);
64
+ console.log(`${c('32', '✓')} Imported ${imported.length} labels from this repository → ${f}`);
65
+ console.log(c('90', 'Apply them to any other repo: cd <repo> && gitset labelspack --apply'));
66
+ return 0;
67
+ }
68
+
38
69
  if (argv.includes('--add')) {
39
70
  let name = flag(argv, '--name');
40
71
  let color = flag(argv, '--color');
@@ -53,7 +84,7 @@ async function runLabelspackCommand(argv) {
53
84
 
54
85
  const { source, labels } = getLabels();
55
86
 
56
- if (!argv.includes('--apply') && !argv.includes('--sync')) {
87
+ if (!argv.includes('--apply')) {
57
88
  console.log(c('1', `\nLabel pack (${source}) — ${labels.length} labels\n`));
58
89
  for (const l of labels) {
59
90
  console.log(` ${c('36', l.name)}${l.description ? ` ${c('90', l.description)}` : ''}`);
@@ -4,6 +4,7 @@ const path = require('path');
4
4
  const os = require('os');
5
5
  const { log, askQuestion, selectOption } = require('../utils/ui');
6
6
  const genLocal = require('../../lib/generate-local');
7
+ const manifestLib = require('../../lib/manifest');
7
8
 
8
9
  function execCommand(cmd) {
9
10
  try {
@@ -32,11 +33,102 @@ function getCurrentBranch() {
32
33
  return execCommand('git rev-parse --abbrev-ref HEAD') || 'HEAD';
33
34
  }
34
35
 
36
+ function getRepoOwnerName() {
37
+ const remoteUrl = execCommand('git config --get remote.origin.url');
38
+ if (!remoteUrl) return '';
39
+ const match = remoteUrl.match(/[:/]([^/]+)\/([^/.]+)(?:\.git)?$/);
40
+ return match ? `${match[1]}/${match[2]}` : '';
41
+ }
42
+
35
43
  function commitsToText(commits) {
36
44
  if (!commits || commits.length === 0) return '';
37
45
  return commits.map(c => `- ${c.hash || ''} ${c.message || ''} (${c.author || ''})`).join('\n');
38
46
  }
39
47
 
48
+ async function syncManifestVersion(tagName) {
49
+ const newVersion = manifestLib.normalizeVersion(tagName);
50
+ if (!newVersion) return;
51
+
52
+ let fileNames;
53
+ try {
54
+ fileNames = fs.readdirSync(process.cwd());
55
+ } catch {
56
+ return;
57
+ }
58
+
59
+ const found = manifestLib.detectManifestFile(fileNames);
60
+ if (!found) return;
61
+
62
+ const filePath = path.join(process.cwd(), found.file);
63
+ let content;
64
+ try {
65
+ content = fs.readFileSync(filePath, 'utf8');
66
+ } catch {
67
+ return;
68
+ }
69
+
70
+ let oldVersion;
71
+ try {
72
+ oldVersion = manifestLib.readVersion(found.type, content);
73
+ } catch (err) {
74
+ log(`\nCould not parse ${found.file}, skipping version sync (${err.message}).`, 'yellow');
75
+ return;
76
+ }
77
+
78
+ if (!oldVersion) {
79
+ log(`\n${found.file} has no static top-level version field, skipping version sync.`, 'dim');
80
+ return;
81
+ }
82
+
83
+ if (oldVersion === newVersion) return;
84
+
85
+ let newContent;
86
+ try {
87
+ newContent = manifestLib.bumpVersion(found.type, content, newVersion);
88
+ } catch (err) {
89
+ log(`\nCould not bump ${found.file}, skipping version sync (${err.message}).`, 'yellow');
90
+ return;
91
+ }
92
+ if (!newContent) return;
93
+
94
+ log(`\n${found.file} version differs from this release:`, 'cyan');
95
+ log(` - version: ${oldVersion}`, 'red');
96
+ log(` + version: ${newVersion}`, 'green');
97
+ log(' Only the top-level version field changes — dependency versions are left untouched.', 'dim');
98
+
99
+ const bumpChoice = await selectOption(`Update ${found.file} to ${newVersion} as part of this release?`, [
100
+ { label: `Yes, update ${found.file}`, value: 'yes' },
101
+ { label: 'No, skip', value: 'no' },
102
+ ]);
103
+ if (bumpChoice !== 'yes') return;
104
+
105
+ try {
106
+ fs.writeFileSync(filePath, newContent);
107
+ execFileSync('git', ['add', found.file], { stdio: 'pipe' });
108
+ execFileSync('git', ['commit', '-m', `chore: bump version to ${newVersion}`], { stdio: 'pipe' });
109
+ log(`Committed: chore: bump version to ${newVersion}`, 'green');
110
+ } catch (err) {
111
+ log(`Failed to commit the version bump: ${err.message}`, 'red');
112
+ return;
113
+ }
114
+
115
+ log('\nThis commit needs to be pushed before creating the release, otherwise the release tag will not include it.', 'dim');
116
+ const pushChoice = await selectOption('Push this commit now?', [
117
+ { label: 'Yes, push', value: 'yes' },
118
+ { label: 'No, I\'ll push it myself', value: 'no' },
119
+ ]);
120
+ if (pushChoice === 'yes') {
121
+ try {
122
+ execFileSync('git', ['push'], { stdio: 'inherit' });
123
+ log('Pushed.', 'green');
124
+ } catch {
125
+ log('Failed to push. Push manually before the release tag will reflect this version.', 'red');
126
+ }
127
+ } else {
128
+ log('Not pushed — the release tag will not include this version bump until you push it.', 'yellow');
129
+ }
130
+ }
131
+
40
132
  module.exports = async function commandRelease(options = {}) {
41
133
  console.log('\n=== Gitset Release Manager (BYOAI) ===\n');
42
134
 
@@ -100,6 +192,7 @@ module.exports = async function commandRelease(options = {}) {
100
192
  }
101
193
 
102
194
  let currentNotes = '';
195
+ const repo = getRepoOwnerName();
103
196
 
104
197
  const generate = async (instr = '') => {
105
198
  log('\nGenerating release notes…', 'cyan');
@@ -108,6 +201,7 @@ module.exports = async function commandRelease(options = {}) {
108
201
  tool: 'release',
109
202
  ctx: {
110
203
  tag: tagName,
204
+ repo,
111
205
  commits: commitsToText(commits),
112
206
  mode: commits.length === 0 ? 'manual' : 'summary',
113
207
  instruction: instr,
@@ -163,7 +257,10 @@ module.exports = async function commandRelease(options = {}) {
163
257
  const notesFile = 'RELEASE_NOTES.md';
164
258
  fs.writeFileSync(notesFile, currentNotes);
165
259
  log(`Saved notes to ${notesFile}`, 'dim');
166
- log('Creating release via gh CLI…', 'cyan');
260
+
261
+ await syncManifestVersion(tagName);
262
+
263
+ log('\nCreating release via gh CLI…', 'cyan');
167
264
  try {
168
265
  execFileSync('gh', ['release', 'create', tagName, '--title', tagName, '--notes-file', notesFile], { stdio: 'inherit' });
169
266
  log('\nRelease created successfully!', 'green');
@@ -96,7 +96,13 @@ function getRepoLabels() {
96
96
  }
97
97
  }
98
98
 
99
+ function writePack(labels) {
100
+ fs.mkdirSync(DIR, { recursive: true, mode: 0o700 });
101
+ fs.writeFileSync(LABELS_FILE, serializePack(labels));
102
+ return LABELS_FILE;
103
+ }
104
+
99
105
  module.exports = {
100
106
  LABELS_FILE, DEFAULT_LABELS,
101
- getLabels, addLabel, writeDefaultPack, getRepoLabels, parseLabelsYaml,
107
+ getLabels, addLabel, writeDefaultPack, writePack, getRepoLabels, parseLabelsYaml,
102
108
  };
package/src/utils/ui.js CHANGED
@@ -73,29 +73,31 @@ function askSecret(query) {
73
73
  stdin.pause();
74
74
  }
75
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');
76
+ const cleaned = String(chunk).replace(/\x1b\[[0-9;]*[a-zA-Z~]/g, '');
77
+ for (const c of cleaned) {
78
+ if (c === '\r' || c === '\n') {
79
+ cleanup();
80
+ process.stdout.write('\n');
81
+ resolve(value.trim());
82
+ return;
83
+ }
84
+ if (c === '\u0003') { // Ctrl+C
85
+ cleanup();
86
+ process.stdout.write('\n');
87
+ reject(new Error('Cancelled.'));
88
+ return;
89
+ }
90
+ if (c === '\u007f' || c === '\b') { // backspace (DEL on most terminals, BS on some)
91
+ if (value.length) {
92
+ value = value.slice(0, -1);
93
+ process.stdout.write('\b \b');
94
+ }
95
+ continue;
93
96
  }
94
- return;
97
+ if (c.charCodeAt(0) < 32) continue; // ignore other control bytes
98
+ value += c;
99
+ process.stdout.write('*');
95
100
  }
96
- if (ch.charCodeAt(0) < 32) return; // ignore other control/escape bytes
97
- value += ch;
98
- process.stdout.write('*');
99
101
  }
100
102
  stdin.on('data', onData);
101
103
  });