@duyxyz/saca 1.0.3 → 1.0.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.
Files changed (7) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +38 -38
  3. package/bin/cli.js +315 -315
  4. package/lib/adb.js +156 -156
  5. package/lib/state.js +144 -144
  6. package/lib/ui.js +205 -205
  7. package/package.json +50 -50
package/bin/cli.js CHANGED
@@ -1,315 +1,315 @@
1
- #!/usr/bin/env node
2
-
3
- import readline from 'readline';
4
- import { checkAdb, getPackages, uninstallPackage, getDeviceSummary } from '../lib/adb.js';
5
- import {
6
- state,
7
- filterItems,
8
- moveCursor,
9
- switchPane,
10
- toggleCurrent,
11
- createItems,
12
- setListHeightResolver,
13
- } from '../lib/state.js';
14
- import {
15
- render,
16
- getListHeight,
17
- } from '../lib/ui.js';
18
-
19
- const KEY = {
20
- CTRL_C: '\u0003',
21
- CTRL_Q: '\u0011',
22
- ESC: '\u001b',
23
- RETURN: '\r',
24
- NEWLINE: '\n',
25
- BACKSPACE: '\u007f',
26
- BACKSPACE_WIN: '\b',
27
- SPACE: ' ',
28
- TAB: '\t',
29
- };
30
-
31
- const KEY_SEQUENCES = {
32
- up: ['\u001b[A', '\u001bOA', '\u0000H', '\u00e0H'],
33
- down: ['\u001b[B', '\u001bOB', '\u0000P', '\u00e0P'],
34
- left: ['\u001b[D', '\u001bOD', '\u0000K', '\u00e0K', '\u001b[Z'],
35
- right: ['\u001b[C', '\u001bOC', '\u0000M', '\u00e0M', KEY.TAB],
36
- pageup: ['\u001b[5~', '\u0000I', '\u00e0I'],
37
- pagedown: ['\u001b[6~', '\u0000Q', '\u00e0Q'],
38
- };
39
-
40
- // Connect state logic with UI config
41
- setListHeightResolver(getListHeight);
42
-
43
- let isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
44
- let rawModeEnabled = false;
45
- let lastSummary = '';
46
-
47
- async function pollDeviceStatus() {
48
- if (!state.running || state.screen === 'running' || state.screen === 'loading') return;
49
-
50
- const devices = getDeviceSummary();
51
- const currentSummary = devices.length > 0 ? devices[0] : '';
52
-
53
- if (currentSummary !== lastSummary) {
54
- const isNewConnection = !lastSummary && currentSummary;
55
- lastSummary = currentSummary;
56
-
57
- if (!currentSummary) {
58
- state.screen = 'error';
59
- state.error = 'Device disconnected.\nConnect a device to continue.';
60
- state.device = null;
61
- render();
62
- } else if (isNewConnection || (state.device && state.device.serial !== currentSummary.split(':')[0])) {
63
- try {
64
- state.device = checkAdb();
65
- await refreshList();
66
- } catch (error) {
67
- state.screen = 'error';
68
- state.error = error.message;
69
- render();
70
- }
71
- }
72
- }
73
- }
74
-
75
- function enterAltScreen() {
76
- if (!isInteractive) return;
77
- process.stdout.write('\x1b[?1049h\x1b[?25l');
78
- }
79
-
80
- function leaveAltScreen() {
81
- if (!isInteractive) return;
82
- process.stdout.write('\x1b[?25h\x1b[?1049l');
83
- }
84
-
85
- function cleanupAndExit(code = 0) {
86
- state.running = false;
87
- if (isInteractive) {
88
- process.stdin.off('keypress', onKeypress);
89
- process.stdout.off('resize', render);
90
- if (rawModeEnabled) {
91
- process.stdin.setRawMode(false);
92
- rawModeEnabled = false;
93
- }
94
- leaveAltScreen();
95
- }
96
- process.exit(code);
97
- }
98
-
99
- function isKeyMatch(key, input, name, sequences = []) {
100
- if (key?.name === name) return true;
101
- return sequences.includes(input);
102
- }
103
-
104
- async function refreshList() {
105
- state.message = 'Refreshing package list...';
106
- state.screen = 'loading';
107
- render();
108
-
109
- try {
110
- const { sysPackages, userPackages } = getPackages(state.device.adbPath);
111
- state.items = createItems(sysPackages, userPackages);
112
- state.selected.clear();
113
- state.pending = [];
114
- state.currentPackage = '';
115
- state.progressIndex = 0;
116
- state.successCount = 0;
117
- state.failCount = 0;
118
- filterItems();
119
- state.screen = 'list';
120
- } catch (error) {
121
- state.screen = 'error';
122
- state.error = error.message;
123
- }
124
- render();
125
- }
126
-
127
- function onKeypress(str, key) {
128
- if (!state.running) return;
129
- const input = typeof str === 'string' ? str : '';
130
- const lowerInput = input.toLowerCase();
131
-
132
- if ((key && key.ctrl && key.name === 'c') || input === KEY.CTRL_C) {
133
- cleanupAndExit(0);
134
- return;
135
- }
136
-
137
- if (state.screen === 'error' || state.screen === 'summary') {
138
- if (state.screen === 'summary' && (isKeyMatch(key, input, 'return', [KEY.RETURN, KEY.NEWLINE]) || isKeyMatch(key, input, 'escape', [KEY.ESC]))) {
139
- void refreshList();
140
- return;
141
- }
142
- if (!key || ['return', 'escape'].includes(key.name) || lowerInput === KEY.CTRL_Q) cleanupAndExit(0);
143
- return;
144
- }
145
-
146
- if (state.screen === 'running') return;
147
-
148
- if ((key && key.ctrl && key.name === 'q') || input === KEY.CTRL_Q) {
149
- cleanupAndExit(0);
150
- return;
151
- }
152
-
153
- if (state.screen === 'confirm') {
154
- if (isKeyMatch(key, input, 'return', [KEY.RETURN, KEY.NEWLINE])) {
155
- void startUninstall();
156
- return;
157
- }
158
- if (isKeyMatch(key, input, 'escape', [KEY.ESC])) {
159
- state.screen = 'list';
160
- render();
161
- }
162
- return;
163
- }
164
-
165
- if (isKeyMatch(key, input, 'up', KEY_SEQUENCES.up)) {
166
- moveCursor(-1);
167
- render();
168
- return;
169
- }
170
- if (isKeyMatch(key, input, 'down', KEY_SEQUENCES.down)) {
171
- moveCursor(1);
172
- render();
173
- return;
174
- }
175
- if (isKeyMatch(key, input, 'left', KEY_SEQUENCES.left)) {
176
- switchPane(-1);
177
- render();
178
- return;
179
- }
180
- if (isKeyMatch(key, input, 'tab', [KEY.TAB])) {
181
- switchPane(state.activePane === 'System' ? 1 : -1);
182
- render();
183
- return;
184
- }
185
- if (isKeyMatch(key, input, 'right', KEY_SEQUENCES.right)) {
186
- switchPane(1);
187
- render();
188
- return;
189
- }
190
- if (isKeyMatch(key, input, 'pageup', KEY_SEQUENCES.pageup)) {
191
- moveCursor(-getListHeight());
192
- render();
193
- return;
194
- }
195
- if (isKeyMatch(key, input, 'pagedown', KEY_SEQUENCES.pagedown)) {
196
- moveCursor(getListHeight());
197
- render();
198
- return;
199
- }
200
- if (isKeyMatch(key, input, 'return', [KEY.RETURN, KEY.NEWLINE])) {
201
- if (state.selected.size > 0) {
202
- state.pending = Array.from(state.selected);
203
- state.screen = 'confirm';
204
- render();
205
- }
206
- return;
207
- }
208
- if (isKeyMatch(key, input, 'backspace', [KEY.BACKSPACE, KEY.BACKSPACE_WIN])) {
209
- if (state.query.length > 0) {
210
- state.query = state.query.slice(0, -1);
211
- filterItems();
212
- render();
213
- }
214
- return;
215
- }
216
- if (isKeyMatch(key, input, 'escape', [KEY.ESC])) {
217
- if (state.query) {
218
- state.query = '';
219
- filterItems();
220
- render();
221
- }
222
- return;
223
- }
224
- if (input === KEY.SPACE) {
225
- toggleCurrent();
226
- render();
227
- return;
228
- }
229
- if (input && !key?.ctrl && !key?.meta && /^[\w.-]$/i.test(input)) {
230
- state.query += input;
231
- filterItems();
232
- render();
233
- }
234
- }
235
-
236
- async function startUninstall() {
237
- state.screen = 'running';
238
- state.progressIndex = 0;
239
- state.successCount = 0;
240
- state.failCount = 0;
241
- render();
242
- for (const pkg of state.pending) {
243
- state.progressIndex += 1;
244
- state.currentPackage = pkg;
245
- render();
246
- const success = uninstallPackage(state.device.adbPath, pkg);
247
- if (success) state.successCount += 1;
248
- else state.failCount += 1;
249
- }
250
- state.screen = 'summary';
251
- state.currentPackage = '';
252
- render();
253
- }
254
-
255
- function printFallback(message) {
256
- process.stdout.write(`${message}\n`);
257
- }
258
-
259
- async function bootstrap() {
260
- if (!isInteractive) {
261
- printFallback('This command now expects an interactive terminal (TTY).');
262
- process.exit(1);
263
- }
264
- enterAltScreen();
265
- readline.emitKeypressEvents(process.stdin);
266
- process.stdin.setRawMode(true);
267
- rawModeEnabled = true;
268
- process.stdin.on('keypress', onKeypress);
269
- process.stdout.on('resize', render);
270
- render();
271
-
272
- state.message = 'Checking ADB connection...';
273
- render();
274
- try {
275
- state.device = checkAdb();
276
- lastSummary = `${state.device.serial}:${state.device.state}`;
277
- setInterval(pollDeviceStatus, 2000);
278
- } catch (error) {
279
- state.screen = 'error';
280
- state.error = error.message;
281
- render();
282
- return;
283
- }
284
-
285
- state.message = 'Loading installed packages...';
286
- render();
287
- try {
288
- const { sysPackages, userPackages } = getPackages(state.device.adbPath);
289
- state.items = createItems(sysPackages, userPackages);
290
- filterItems();
291
- state.screen = 'list';
292
- render();
293
- } catch (error) {
294
- state.screen = 'error';
295
- state.error = error.message;
296
- render();
297
- }
298
- }
299
-
300
- process.on('SIGINT', () => cleanupAndExit(0));
301
- process.on('uncaughtException', (error) => {
302
- if (isInteractive) leaveAltScreen();
303
- console.error(error);
304
- process.exit(1);
305
- });
306
- process.on('exit', () => {
307
- if (isInteractive && rawModeEnabled) process.stdin.setRawMode(false);
308
- leaveAltScreen();
309
- });
310
-
311
- bootstrap().catch((error) => {
312
- leaveAltScreen();
313
- console.error(error);
314
- process.exit(1);
315
- });
1
+ #!/usr/bin/env node
2
+
3
+ import readline from 'readline';
4
+ import { checkAdb, getPackages, uninstallPackage, getDeviceSummary } from '../lib/adb.js';
5
+ import {
6
+ state,
7
+ filterItems,
8
+ moveCursor,
9
+ switchPane,
10
+ toggleCurrent,
11
+ createItems,
12
+ setListHeightResolver,
13
+ } from '../lib/state.js';
14
+ import {
15
+ render,
16
+ getListHeight,
17
+ } from '../lib/ui.js';
18
+
19
+ const KEY = {
20
+ CTRL_C: '\u0003',
21
+ CTRL_Q: '\u0011',
22
+ ESC: '\u001b',
23
+ RETURN: '\r',
24
+ NEWLINE: '\n',
25
+ BACKSPACE: '\u007f',
26
+ BACKSPACE_WIN: '\b',
27
+ SPACE: ' ',
28
+ TAB: '\t',
29
+ };
30
+
31
+ const KEY_SEQUENCES = {
32
+ up: ['\u001b[A', '\u001bOA', '\u0000H', '\u00e0H'],
33
+ down: ['\u001b[B', '\u001bOB', '\u0000P', '\u00e0P'],
34
+ left: ['\u001b[D', '\u001bOD', '\u0000K', '\u00e0K', '\u001b[Z'],
35
+ right: ['\u001b[C', '\u001bOC', '\u0000M', '\u00e0M', KEY.TAB],
36
+ pageup: ['\u001b[5~', '\u0000I', '\u00e0I'],
37
+ pagedown: ['\u001b[6~', '\u0000Q', '\u00e0Q'],
38
+ };
39
+
40
+ // Connect state logic with UI config
41
+ setListHeightResolver(getListHeight);
42
+
43
+ let isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
44
+ let rawModeEnabled = false;
45
+ let lastSummary = '';
46
+
47
+ async function pollDeviceStatus() {
48
+ if (!state.running || state.screen === 'running' || state.screen === 'loading') return;
49
+
50
+ const devices = getDeviceSummary();
51
+ const currentSummary = devices.length > 0 ? devices[0] : '';
52
+
53
+ if (currentSummary !== lastSummary) {
54
+ const isNewConnection = !lastSummary && currentSummary;
55
+ lastSummary = currentSummary;
56
+
57
+ if (!currentSummary) {
58
+ state.screen = 'error';
59
+ state.error = 'Device disconnected.\nConnect a device to continue.';
60
+ state.device = null;
61
+ render();
62
+ } else if (isNewConnection || (state.device && state.device.serial !== currentSummary.split(':')[0])) {
63
+ try {
64
+ state.device = checkAdb();
65
+ await refreshList();
66
+ } catch (error) {
67
+ state.screen = 'error';
68
+ state.error = error.message;
69
+ render();
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ function enterAltScreen() {
76
+ if (!isInteractive) return;
77
+ process.stdout.write('\x1b[?1049h\x1b[?25l');
78
+ }
79
+
80
+ function leaveAltScreen() {
81
+ if (!isInteractive) return;
82
+ process.stdout.write('\x1b[?25h\x1b[?1049l');
83
+ }
84
+
85
+ function cleanupAndExit(code = 0) {
86
+ state.running = false;
87
+ if (isInteractive) {
88
+ process.stdin.off('keypress', onKeypress);
89
+ process.stdout.off('resize', render);
90
+ if (rawModeEnabled) {
91
+ process.stdin.setRawMode(false);
92
+ rawModeEnabled = false;
93
+ }
94
+ leaveAltScreen();
95
+ }
96
+ process.exit(code);
97
+ }
98
+
99
+ function isKeyMatch(key, input, name, sequences = []) {
100
+ if (key?.name === name) return true;
101
+ return sequences.includes(input);
102
+ }
103
+
104
+ async function refreshList() {
105
+ state.message = 'Refreshing package list...';
106
+ state.screen = 'loading';
107
+ render();
108
+
109
+ try {
110
+ const { sysPackages, userPackages } = getPackages(state.device.adbPath);
111
+ state.items = createItems(sysPackages, userPackages);
112
+ state.selected.clear();
113
+ state.pending = [];
114
+ state.currentPackage = '';
115
+ state.progressIndex = 0;
116
+ state.successCount = 0;
117
+ state.failCount = 0;
118
+ filterItems();
119
+ state.screen = 'list';
120
+ } catch (error) {
121
+ state.screen = 'error';
122
+ state.error = error.message;
123
+ }
124
+ render();
125
+ }
126
+
127
+ function onKeypress(str, key) {
128
+ if (!state.running) return;
129
+ const input = typeof str === 'string' ? str : '';
130
+ const lowerInput = input.toLowerCase();
131
+
132
+ if ((key && key.ctrl && key.name === 'c') || input === KEY.CTRL_C) {
133
+ cleanupAndExit(0);
134
+ return;
135
+ }
136
+
137
+ if (state.screen === 'error' || state.screen === 'summary') {
138
+ if (state.screen === 'summary' && (isKeyMatch(key, input, 'return', [KEY.RETURN, KEY.NEWLINE]) || isKeyMatch(key, input, 'escape', [KEY.ESC]))) {
139
+ void refreshList();
140
+ return;
141
+ }
142
+ if (!key || ['return', 'escape'].includes(key.name) || lowerInput === KEY.CTRL_Q) cleanupAndExit(0);
143
+ return;
144
+ }
145
+
146
+ if (state.screen === 'running') return;
147
+
148
+ if ((key && key.ctrl && key.name === 'q') || input === KEY.CTRL_Q) {
149
+ cleanupAndExit(0);
150
+ return;
151
+ }
152
+
153
+ if (state.screen === 'confirm') {
154
+ if (isKeyMatch(key, input, 'return', [KEY.RETURN, KEY.NEWLINE])) {
155
+ void startUninstall();
156
+ return;
157
+ }
158
+ if (isKeyMatch(key, input, 'escape', [KEY.ESC])) {
159
+ state.screen = 'list';
160
+ render();
161
+ }
162
+ return;
163
+ }
164
+
165
+ if (isKeyMatch(key, input, 'up', KEY_SEQUENCES.up)) {
166
+ moveCursor(-1);
167
+ render();
168
+ return;
169
+ }
170
+ if (isKeyMatch(key, input, 'down', KEY_SEQUENCES.down)) {
171
+ moveCursor(1);
172
+ render();
173
+ return;
174
+ }
175
+ if (isKeyMatch(key, input, 'left', KEY_SEQUENCES.left)) {
176
+ switchPane(-1);
177
+ render();
178
+ return;
179
+ }
180
+ if (isKeyMatch(key, input, 'tab', [KEY.TAB])) {
181
+ switchPane(state.activePane === 'System' ? 1 : -1);
182
+ render();
183
+ return;
184
+ }
185
+ if (isKeyMatch(key, input, 'right', KEY_SEQUENCES.right)) {
186
+ switchPane(1);
187
+ render();
188
+ return;
189
+ }
190
+ if (isKeyMatch(key, input, 'pageup', KEY_SEQUENCES.pageup)) {
191
+ moveCursor(-getListHeight());
192
+ render();
193
+ return;
194
+ }
195
+ if (isKeyMatch(key, input, 'pagedown', KEY_SEQUENCES.pagedown)) {
196
+ moveCursor(getListHeight());
197
+ render();
198
+ return;
199
+ }
200
+ if (isKeyMatch(key, input, 'return', [KEY.RETURN, KEY.NEWLINE])) {
201
+ if (state.selected.size > 0) {
202
+ state.pending = Array.from(state.selected);
203
+ state.screen = 'confirm';
204
+ render();
205
+ }
206
+ return;
207
+ }
208
+ if (isKeyMatch(key, input, 'backspace', [KEY.BACKSPACE, KEY.BACKSPACE_WIN])) {
209
+ if (state.query.length > 0) {
210
+ state.query = state.query.slice(0, -1);
211
+ filterItems();
212
+ render();
213
+ }
214
+ return;
215
+ }
216
+ if (isKeyMatch(key, input, 'escape', [KEY.ESC])) {
217
+ if (state.query) {
218
+ state.query = '';
219
+ filterItems();
220
+ render();
221
+ }
222
+ return;
223
+ }
224
+ if (input === KEY.SPACE) {
225
+ toggleCurrent();
226
+ render();
227
+ return;
228
+ }
229
+ if (input && !key?.ctrl && !key?.meta && /^[\w.-]$/i.test(input)) {
230
+ state.query += input;
231
+ filterItems();
232
+ render();
233
+ }
234
+ }
235
+
236
+ async function startUninstall() {
237
+ state.screen = 'running';
238
+ state.progressIndex = 0;
239
+ state.successCount = 0;
240
+ state.failCount = 0;
241
+ render();
242
+ for (const pkg of state.pending) {
243
+ state.progressIndex += 1;
244
+ state.currentPackage = pkg;
245
+ render();
246
+ const success = uninstallPackage(state.device.adbPath, pkg);
247
+ if (success) state.successCount += 1;
248
+ else state.failCount += 1;
249
+ }
250
+ state.screen = 'summary';
251
+ state.currentPackage = '';
252
+ render();
253
+ }
254
+
255
+ function printFallback(message) {
256
+ process.stdout.write(`${message}\n`);
257
+ }
258
+
259
+ async function bootstrap() {
260
+ if (!isInteractive) {
261
+ printFallback('This command now expects an interactive terminal (TTY).');
262
+ process.exit(1);
263
+ }
264
+ enterAltScreen();
265
+ readline.emitKeypressEvents(process.stdin);
266
+ process.stdin.setRawMode(true);
267
+ rawModeEnabled = true;
268
+ process.stdin.on('keypress', onKeypress);
269
+ process.stdout.on('resize', render);
270
+ render();
271
+
272
+ state.message = 'Checking ADB connection...';
273
+ render();
274
+ try {
275
+ state.device = checkAdb();
276
+ lastSummary = `${state.device.serial}:${state.device.state}`;
277
+ setInterval(pollDeviceStatus, 2000);
278
+ } catch (error) {
279
+ state.screen = 'error';
280
+ state.error = error.message;
281
+ render();
282
+ return;
283
+ }
284
+
285
+ state.message = 'Loading installed packages...';
286
+ render();
287
+ try {
288
+ const { sysPackages, userPackages } = getPackages(state.device.adbPath);
289
+ state.items = createItems(sysPackages, userPackages);
290
+ filterItems();
291
+ state.screen = 'list';
292
+ render();
293
+ } catch (error) {
294
+ state.screen = 'error';
295
+ state.error = error.message;
296
+ render();
297
+ }
298
+ }
299
+
300
+ process.on('SIGINT', () => cleanupAndExit(0));
301
+ process.on('uncaughtException', (error) => {
302
+ if (isInteractive) leaveAltScreen();
303
+ console.error(error);
304
+ process.exit(1);
305
+ });
306
+ process.on('exit', () => {
307
+ if (isInteractive && rawModeEnabled) process.stdin.setRawMode(false);
308
+ leaveAltScreen();
309
+ });
310
+
311
+ bootstrap().catch((error) => {
312
+ leaveAltScreen();
313
+ console.error(error);
314
+ process.exit(1);
315
+ });