@prave/cli 1.0.0 → 1.0.1

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.
@@ -17,6 +17,17 @@ export async function whoamiCommand() {
17
17
  process.exitCode = 1;
18
18
  return;
19
19
  }
20
+ // Detect creds left over from older CLI versions that predate the
21
+ // refresh-token flow. Without a refresh_token we can't auto-rotate the
22
+ // expired access_token, so the user gets stuck in a "session expired"
23
+ // loop. Direct them to `prave login` immediately rather than letting
24
+ // the API call fail confusingly.
25
+ if (!creds.refresh_token) {
26
+ log.warn('Stored credentials predate the refresh-token flow.');
27
+ log.dim('Run `prave login` once to upgrade — refreshes will be automatic afterwards.');
28
+ process.exitCode = 1;
29
+ return;
30
+ }
20
31
  try {
21
32
  const { data } = await api.get('/api/v1/me', true);
22
33
  const handle = data.username || data.display_name || data.email || creds.email || 'unknown';
@@ -1,5 +1,4 @@
1
1
  import { stdin, stdout } from 'node:process';
2
- import readline from 'node:readline';
3
2
  import chalk from 'chalk';
4
3
  export async function checkboxPrompt(question, items, opts = {}) {
5
4
  if (!stdin.isTTY || !stdout.isTTY) {
@@ -8,15 +7,17 @@ export async function checkboxPrompt(question, items, opts = {}) {
8
7
  const selected = new Set(opts.initial ?? []);
9
8
  let cursor = 0;
10
9
  const minSelected = opts.minSelected ?? 0;
10
+ let dataHandler = null;
11
11
  const cleanup = () => {
12
12
  if (typeof stdin.setRawMode === 'function')
13
13
  stdin.setRawMode(false);
14
- stdin.removeAllListeners('keypress');
14
+ if (dataHandler)
15
+ stdin.off('data', dataHandler);
15
16
  stdin.pause();
16
17
  stdout.write(showCursor);
18
+ stdout.write('\n');
17
19
  };
18
20
  return new Promise((resolve) => {
19
- readline.emitKeypressEvents(stdin);
20
21
  if (typeof stdin.setRawMode === 'function')
21
22
  stdin.setRawMode(true);
22
23
  stdin.resume();
@@ -45,53 +46,109 @@ export async function checkboxPrompt(question, items, opts = {}) {
45
46
  ? `${selected.size} selected · enter to confirm`
46
47
  : `at least ${minSelected} required · ${selected.size} selected`)}\n`);
47
48
  };
48
- const onKey = (_str, key) => {
49
- if (!key)
50
- return;
51
- if (key.ctrl && key.name === 'c') {
52
- cleanup();
53
- resolve(null);
54
- return;
55
- }
56
- switch (key.name) {
57
- case 'up':
58
- case 'k':
59
- cursor = (cursor - 1 + items.length) % items.length;
60
- break;
61
- case 'down':
62
- case 'j':
63
- cursor = (cursor + 1) % items.length;
64
- break;
65
- case 'space': {
49
+ /**
50
+ * Raw-byte parser. We listen to `data` directly instead of relying on
51
+ * `readline.emitKeypressEvents`, which is unreliable on macOS Terminal +
52
+ * iTerm under raw mode (the keypress event simply never fires for some
53
+ * users, leaving them stuck on whatever items the prompt prefilled).
54
+ *
55
+ * Sequences we care about:
56
+ * ESC [ A/B/C/D → arrow keys
57
+ * 0x0D / 0x0A → return
58
+ * 0x20 → space
59
+ * 0x03 → ctrl-c
60
+ * 0x1B (alone) → escape (heuristic: solo 0x1B with no follow-up)
61
+ * "k" / "j" / "a" → vim-style move + toggle-all
62
+ * "q" → cancel
63
+ */
64
+ const handle = (data) => {
65
+ // An entire keystroke can arrive as one chunk on most terminals, but
66
+ // we guard against multi-byte chunks by walking the buffer.
67
+ let i = 0;
68
+ while (i < data.length) {
69
+ const b = data[i];
70
+ // CSI escape sequences: ESC [ X
71
+ if (b === 0x1b && data[i + 1] === 0x5b && data.length > i + 2) {
72
+ const arrow = data[i + 2];
73
+ if (arrow === 0x41)
74
+ cursor = (cursor - 1 + items.length) % items.length;
75
+ else if (arrow === 0x42)
76
+ cursor = (cursor + 1) % items.length;
77
+ else if (arrow === 0x43) {
78
+ // right arrow → toggle (some users default to this)
79
+ const v = items[cursor].value;
80
+ if (selected.has(v))
81
+ selected.delete(v);
82
+ else
83
+ selected.add(v);
84
+ }
85
+ i += 3;
86
+ continue;
87
+ }
88
+ // Bare ESC = cancel.
89
+ if (b === 0x1b && (data.length === i + 1 || data[i + 1] === undefined)) {
90
+ cleanup();
91
+ resolve(null);
92
+ return;
93
+ }
94
+ // Ctrl-C.
95
+ if (b === 0x03) {
96
+ cleanup();
97
+ resolve(null);
98
+ return;
99
+ }
100
+ // Enter — CR or LF.
101
+ if (b === 0x0d || b === 0x0a) {
102
+ if (selected.size < minSelected) {
103
+ i += 1;
104
+ continue;
105
+ }
106
+ cleanup();
107
+ resolve([...selected]);
108
+ return;
109
+ }
110
+ // Space — toggle current.
111
+ if (b === 0x20) {
66
112
  const v = items[cursor].value;
67
113
  if (selected.has(v))
68
114
  selected.delete(v);
69
115
  else
70
116
  selected.add(v);
71
- break;
117
+ i += 1;
118
+ continue;
72
119
  }
73
- case 'a':
120
+ // Letters: a (toggle all), j (down), k (up), q (cancel).
121
+ if (b === 0x61) {
74
122
  if (selected.size === items.length)
75
123
  selected.clear();
76
124
  else
77
- for (const i of items)
78
- selected.add(i.value);
79
- break;
80
- case 'return':
81
- if (selected.size < minSelected)
82
- return;
83
- cleanup();
84
- resolve([...selected]);
85
- return;
86
- case 'escape':
87
- case 'q':
125
+ for (const it of items)
126
+ selected.add(it.value);
127
+ i += 1;
128
+ continue;
129
+ }
130
+ if (b === 0x6a) {
131
+ cursor = (cursor + 1) % items.length;
132
+ i += 1;
133
+ continue;
134
+ }
135
+ if (b === 0x6b) {
136
+ cursor = (cursor - 1 + items.length) % items.length;
137
+ i += 1;
138
+ continue;
139
+ }
140
+ if (b === 0x71) {
88
141
  cleanup();
89
142
  resolve(null);
90
143
  return;
144
+ }
145
+ // Unknown byte — advance and ignore.
146
+ i += 1;
91
147
  }
92
148
  render();
93
149
  };
94
- stdin.on('keypress', onKey);
150
+ dataHandler = handle;
151
+ stdin.on('data', handle);
95
152
  render();
96
153
  });
97
154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prave/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Prave CLI — import, export, install, sync Claude Skills.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "open": "^10.1.0",
17
17
  "ora": "^8.0.1",
18
18
  "undici": "^6.18.0",
19
- "@prave/shared": "1.0.0"
19
+ "@prave/shared": "1.0.1"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^20.12.7",