sleev 0.0.12 → 0.0.15
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/README.md +19 -38
- package/bin/sleev.js +84 -0
- package/package.json +27 -28
- package/postinstall.mjs +66 -0
- package/dist/apps/claude.js +0 -151
- package/dist/apps/codex.js +0 -208
- package/dist/apps/opencode-config.js +0 -268
- package/dist/apps/opencode.js +0 -158
- package/dist/apps.js +0 -148
- package/dist/auth/browser.js +0 -80
- package/dist/auth/command.js +0 -109
- package/dist/auth/index.js +0 -2
- package/dist/auth/login.js +0 -55
- package/dist/auth/loopback.js +0 -134
- package/dist/auth/schema.js +0 -46
- package/dist/auth/validation.js +0 -27
- package/dist/cli.js +0 -69
- package/dist/credentials/index.js +0 -3
- package/dist/credentials/schema.js +0 -63
- package/dist/credentials/store.js +0 -52
- package/dist/harnesses.js +0 -66
- package/dist/index.js +0 -3
- package/dist/product.js +0 -27
- package/dist/providers.js +0 -30
- package/dist/terminal.js +0 -55
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
// OpenCode config ownership picks the active config file and edits its provider state.
|
|
2
|
-
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
5
|
-
import { applyEdits, modify, parse } from 'jsonc-parser';
|
|
6
|
-
import { provider as registryProvider } from '../providers.js';
|
|
7
|
-
export async function patchOpenCodeConfig(auth, path, provider = 'openai') {
|
|
8
|
-
const target = path ?? (await resolveOpenCodePath());
|
|
9
|
-
const existing = await readJson(target);
|
|
10
|
-
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
11
|
-
if (jsoncPath(target)) {
|
|
12
|
-
const raw = await readRaw(target);
|
|
13
|
-
await writeFile(target, patchJsonc(raw, existing, auth, provider), { mode: 0o600 });
|
|
14
|
-
return target;
|
|
15
|
-
}
|
|
16
|
-
const config = patchConfig(existing, auth, provider);
|
|
17
|
-
await writeFile(target, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
18
|
-
return target;
|
|
19
|
-
}
|
|
20
|
-
export async function unpatchOpenCodeConfig(path) {
|
|
21
|
-
const target = path ?? (await resolveOpenCodePath());
|
|
22
|
-
const existing = await readExistingJson(target);
|
|
23
|
-
if (!existing)
|
|
24
|
-
return null;
|
|
25
|
-
const provider = object(existing.provider);
|
|
26
|
-
const next = unpatchProvider(unpatchOpenCodeZen(provider), 'openai');
|
|
27
|
-
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
28
|
-
if (jsoncPath(target)) {
|
|
29
|
-
const raw = await readRaw(target);
|
|
30
|
-
await writeFile(target, unpatchJsonc(raw, provider, next), { mode: 0o600 });
|
|
31
|
-
return target;
|
|
32
|
-
}
|
|
33
|
-
await writeFile(target, `${JSON.stringify({ ...existing, provider: next }, null, 2)}\n`, { mode: 0o600 });
|
|
34
|
-
return target;
|
|
35
|
-
}
|
|
36
|
-
export function openCodePath(env = process.env, platform = process.platform) {
|
|
37
|
-
return join(openCodeRoot(env, platform), 'opencode.json');
|
|
38
|
-
}
|
|
39
|
-
export function openCodeJsoncPath(env = process.env, platform = process.platform) {
|
|
40
|
-
return join(openCodeRoot(env, platform), 'opencode.jsonc');
|
|
41
|
-
}
|
|
42
|
-
export function openCodeAuthPath(env = process.env, platform = process.platform) {
|
|
43
|
-
const root = platform === 'win32'
|
|
44
|
-
? join(homedir(), '.local', 'share')
|
|
45
|
-
: env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
|
|
46
|
-
return join(root, 'opencode', 'auth.json');
|
|
47
|
-
}
|
|
48
|
-
export async function resolveOpenCodePath(env = process.env, platform = process.platform) {
|
|
49
|
-
const jsonc = openCodeJsoncPath(env, platform);
|
|
50
|
-
const json = openCodePath(env, platform);
|
|
51
|
-
const [jsoncConfig, jsonConfig] = await Promise.all([readExistingJson(jsonc), readExistingJson(json)]);
|
|
52
|
-
if (hasProvider(jsoncConfig))
|
|
53
|
-
return jsonc;
|
|
54
|
-
if (hasProvider(jsonConfig))
|
|
55
|
-
return json;
|
|
56
|
-
if (await exists(jsonc))
|
|
57
|
-
return jsonc;
|
|
58
|
-
return json;
|
|
59
|
-
}
|
|
60
|
-
function openCodeRoot(env = process.env, platform = process.platform) {
|
|
61
|
-
const root = platform === 'win32'
|
|
62
|
-
? join(homedir(), '.config')
|
|
63
|
-
: env.XDG_CONFIG_HOME || join(homedir(), '.config');
|
|
64
|
-
return join(root, 'opencode');
|
|
65
|
-
}
|
|
66
|
-
function patchConfig(input, auth, id) {
|
|
67
|
-
const provider = object(input.provider);
|
|
68
|
-
const base = trim(auth.proxyUrl);
|
|
69
|
-
const next = id === 'opencode' ? patchOpenCodeZen(provider, auth, base) : patchProvider(provider, auth, base, id);
|
|
70
|
-
return {
|
|
71
|
-
...input,
|
|
72
|
-
provider: next,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
function patchProvider(provider, auth, base, id) {
|
|
76
|
-
const existing = object(provider[id]);
|
|
77
|
-
const options = object(existing.options);
|
|
78
|
-
const headers = object(options.headers);
|
|
79
|
-
return {
|
|
80
|
-
...provider,
|
|
81
|
-
[id]: {
|
|
82
|
-
...existing,
|
|
83
|
-
options: {
|
|
84
|
-
...options,
|
|
85
|
-
baseURL: base,
|
|
86
|
-
headers: {
|
|
87
|
-
...headers,
|
|
88
|
-
...sleeveHeaders(auth, id === 'openai' ? 'codex' : id),
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
function unpatchProvider(provider, id) {
|
|
95
|
-
const existing = object(provider[id]);
|
|
96
|
-
const options = object(existing.options);
|
|
97
|
-
const headers = object(options.headers);
|
|
98
|
-
if (headers['sleeve-harness'] !== 'opencode')
|
|
99
|
-
return provider;
|
|
100
|
-
const { ['sleeve-token']: _token, ['sleeve-provider']: _provider, ['sleeve-harness']: _harness, ...rest } = headers;
|
|
101
|
-
const { baseURL: _base, ...other } = options;
|
|
102
|
-
return {
|
|
103
|
-
...provider,
|
|
104
|
-
[id]: {
|
|
105
|
-
...existing,
|
|
106
|
-
options: {
|
|
107
|
-
...other,
|
|
108
|
-
headers: rest,
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
function patchOpenCodeZen(provider, auth, base) {
|
|
114
|
-
const existing = object(provider.opencode);
|
|
115
|
-
const models = object(existing.models);
|
|
116
|
-
const registry = registryProvider('opencode');
|
|
117
|
-
const patched = Object.fromEntries((registry.models ?? []).map((id) => {
|
|
118
|
-
const model = object(models[id]);
|
|
119
|
-
const source = object(model.provider);
|
|
120
|
-
const headers = object(model.headers);
|
|
121
|
-
return [
|
|
122
|
-
id,
|
|
123
|
-
{
|
|
124
|
-
...model,
|
|
125
|
-
provider: {
|
|
126
|
-
...source,
|
|
127
|
-
api: base,
|
|
128
|
-
},
|
|
129
|
-
headers: {
|
|
130
|
-
...headers,
|
|
131
|
-
...sleeveHeaders(auth, 'opencode'),
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
];
|
|
135
|
-
}));
|
|
136
|
-
return {
|
|
137
|
-
...provider,
|
|
138
|
-
opencode: {
|
|
139
|
-
...existing,
|
|
140
|
-
models: {
|
|
141
|
-
...models,
|
|
142
|
-
...patched,
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
function unpatchOpenCodeZen(provider) {
|
|
148
|
-
const existing = object(provider.opencode);
|
|
149
|
-
const models = object(existing.models);
|
|
150
|
-
const patched = Object.fromEntries(Object.entries(models).map(([id, value]) => {
|
|
151
|
-
const model = object(value);
|
|
152
|
-
const headers = object(model.headers);
|
|
153
|
-
if (headers['sleeve-harness'] !== 'opencode')
|
|
154
|
-
return [id, model];
|
|
155
|
-
const { ['sleeve-token']: _token, ['sleeve-provider']: _provider, ['sleeve-harness']: _harness, ...rest } = headers;
|
|
156
|
-
const source = object(model.provider);
|
|
157
|
-
const { api: _api, ...provider } = source;
|
|
158
|
-
return [
|
|
159
|
-
id,
|
|
160
|
-
{
|
|
161
|
-
...model,
|
|
162
|
-
provider,
|
|
163
|
-
headers: rest,
|
|
164
|
-
},
|
|
165
|
-
];
|
|
166
|
-
}));
|
|
167
|
-
return {
|
|
168
|
-
...provider,
|
|
169
|
-
opencode: {
|
|
170
|
-
...existing,
|
|
171
|
-
models: patched,
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
function sleeveHeaders(auth, provider) {
|
|
176
|
-
return {
|
|
177
|
-
'sleeve-token': auth.token,
|
|
178
|
-
'sleeve-provider': provider,
|
|
179
|
-
'sleeve-harness': 'opencode',
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
async function readJson(path) {
|
|
183
|
-
try {
|
|
184
|
-
const raw = await readFile(path, 'utf8');
|
|
185
|
-
const parsed = jsoncPath(path) ? parse(raw, [], { allowTrailingComma: true }) : JSON.parse(raw);
|
|
186
|
-
return object(parsed);
|
|
187
|
-
}
|
|
188
|
-
catch (err) {
|
|
189
|
-
if (code(err) === 'ENOENT')
|
|
190
|
-
return {};
|
|
191
|
-
throw err;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
async function readExistingJson(path) {
|
|
195
|
-
try {
|
|
196
|
-
const raw = await readFile(path, 'utf8');
|
|
197
|
-
const parsed = jsoncPath(path) ? parse(raw, [], { allowTrailingComma: true }) : JSON.parse(raw);
|
|
198
|
-
return object(parsed);
|
|
199
|
-
}
|
|
200
|
-
catch (err) {
|
|
201
|
-
if (code(err) === 'ENOENT')
|
|
202
|
-
return null;
|
|
203
|
-
throw err;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
async function readRaw(path) {
|
|
207
|
-
try {
|
|
208
|
-
return await readFile(path, 'utf8');
|
|
209
|
-
}
|
|
210
|
-
catch (err) {
|
|
211
|
-
if (code(err) === 'ENOENT')
|
|
212
|
-
return '{}\n';
|
|
213
|
-
throw err;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
async function exists(path) {
|
|
217
|
-
try {
|
|
218
|
-
await access(path);
|
|
219
|
-
return true;
|
|
220
|
-
}
|
|
221
|
-
catch (err) {
|
|
222
|
-
if (code(err) === 'ENOENT')
|
|
223
|
-
return false;
|
|
224
|
-
throw err;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
function hasProvider(config) {
|
|
228
|
-
if (!config)
|
|
229
|
-
return false;
|
|
230
|
-
return 'provider' in config && typeof config.provider === 'object' && config.provider !== null && !Array.isArray(config.provider);
|
|
231
|
-
}
|
|
232
|
-
function patchJsonc(raw, existing, auth, id) {
|
|
233
|
-
const provider = object(existing.provider);
|
|
234
|
-
const base = trim(auth.proxyUrl);
|
|
235
|
-
const next = id === 'opencode' ? patchOpenCodeZen(provider, auth, base) : patchProvider(provider, auth, base, id);
|
|
236
|
-
const key = id === 'opencode' ? 'opencode' : id;
|
|
237
|
-
const value = next[key];
|
|
238
|
-
return editJsonc(raw, ['provider', key], value);
|
|
239
|
-
}
|
|
240
|
-
function unpatchJsonc(raw, provider, next) {
|
|
241
|
-
const openai = object(next.openai);
|
|
242
|
-
const opencode = object(next.opencode);
|
|
243
|
-
const target = 'openai' in provider ? editJsonc(raw, ['provider', 'openai'], 'openai' in next ? openai : undefined) : raw;
|
|
244
|
-
return 'opencode' in provider ? editJsonc(target, ['provider', 'opencode'], 'opencode' in next ? opencode : undefined) : target;
|
|
245
|
-
}
|
|
246
|
-
function editJsonc(raw, path, value) {
|
|
247
|
-
const edits = modify(raw, path, value, {
|
|
248
|
-
formattingOptions: { insertSpaces: true, tabSize: 2, keepLines: true, insertFinalNewline: true },
|
|
249
|
-
});
|
|
250
|
-
return applyEdits(raw, edits);
|
|
251
|
-
}
|
|
252
|
-
function jsoncPath(path) {
|
|
253
|
-
return path.endsWith('.jsonc');
|
|
254
|
-
}
|
|
255
|
-
function object(value) {
|
|
256
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
257
|
-
return {};
|
|
258
|
-
return value;
|
|
259
|
-
}
|
|
260
|
-
function trim(url) {
|
|
261
|
-
return url.replace(/\/+$/, '');
|
|
262
|
-
}
|
|
263
|
-
function code(err) {
|
|
264
|
-
if (typeof err !== 'object' || err === null || !('code' in err))
|
|
265
|
-
return undefined;
|
|
266
|
-
const value = err.code;
|
|
267
|
-
return typeof value === 'string' ? value : undefined;
|
|
268
|
-
}
|
package/dist/apps/opencode.js
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
// OpenCode setup owns credential discovery and provider selection for config patching.
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { emitKeypressEvents } from 'node:readline';
|
|
4
|
-
import { PROVIDERS } from '../providers.js';
|
|
5
|
-
import { field, line, note, prompt, success } from '../terminal.js';
|
|
6
|
-
import { openCodeAuthPath, openCodeJsoncPath, openCodePath, patchOpenCodeConfig, resolveOpenCodePath, unpatchOpenCodeConfig, } from './opencode-config.js';
|
|
7
|
-
export async function setupOpenCode(auth, input = process.stdin, output = process.stdout, options = {}) {
|
|
8
|
-
const path = options.configPath ?? (await resolveOpenCodePath());
|
|
9
|
-
const available = await availableOpenCodeProviders(options.authPath);
|
|
10
|
-
if (available.length === 0) {
|
|
11
|
-
note(output, 'Skipping OpenCode setup because no supported OpenCode credentials were found');
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
const providers = await selectOpenCodeProviders(available, input, output);
|
|
15
|
-
if (providers.length === 0)
|
|
16
|
-
return null;
|
|
17
|
-
const results = [];
|
|
18
|
-
for (const provider of providers) {
|
|
19
|
-
results.push(await patchOpenCode(auth, path, provider.id));
|
|
20
|
-
}
|
|
21
|
-
success(output, 'Configured OpenCode');
|
|
22
|
-
field(output, 'providers', providers.map((provider) => provider.label).join(', '));
|
|
23
|
-
field(output, 'config', path);
|
|
24
|
-
return results[0] ?? null;
|
|
25
|
-
}
|
|
26
|
-
export async function availableOpenCodeProviders(path = openCodeAuthPath()) {
|
|
27
|
-
const auth = await readAuth(path);
|
|
28
|
-
return openCodeProviders().filter((provider) => provider.id in auth);
|
|
29
|
-
}
|
|
30
|
-
export async function patchOpenCode(auth, path, provider = 'openai') {
|
|
31
|
-
const target = await patchOpenCodeConfig(auth, path, provider);
|
|
32
|
-
return { path: target, provider };
|
|
33
|
-
}
|
|
34
|
-
export async function unpatchOpenCode(path) {
|
|
35
|
-
await unpatchOpenCodeConfig(path);
|
|
36
|
-
}
|
|
37
|
-
export { openCodeAuthPath, openCodeJsoncPath, openCodePath, resolveOpenCodePath };
|
|
38
|
-
async function selectOpenCodeProviders(providers, input, output) {
|
|
39
|
-
if (providers.length === 1) {
|
|
40
|
-
const provider = providers[0];
|
|
41
|
-
if (!provider)
|
|
42
|
-
return [];
|
|
43
|
-
note(output, `Using OpenCode credential: ${provider.label}`);
|
|
44
|
-
return [provider];
|
|
45
|
-
}
|
|
46
|
-
if (!canSelect(input)) {
|
|
47
|
-
note(output, 'Skipping OpenCode setup because this terminal is not interactive');
|
|
48
|
-
return [];
|
|
49
|
-
}
|
|
50
|
-
return selectTty(providers, input, output);
|
|
51
|
-
}
|
|
52
|
-
function selectTty(providers, input, output) {
|
|
53
|
-
return new Promise((resolve, reject) => {
|
|
54
|
-
let cursor = 0;
|
|
55
|
-
let selected = new Set();
|
|
56
|
-
let rendered = false;
|
|
57
|
-
const render = () => {
|
|
58
|
-
if (rendered)
|
|
59
|
-
output.write(`\x1b[${providers.length + 2}A`);
|
|
60
|
-
output.write(`${prompt(output, 'choose the OpenCode credentials to patch')}\x1b[K\n`);
|
|
61
|
-
for (const [index, provider] of providers.entries()) {
|
|
62
|
-
const marker = index === cursor ? '>' : ' ';
|
|
63
|
-
const checked = selected.has(provider.id) ? 'x' : ' ';
|
|
64
|
-
output.write(`${marker} (${checked}) ${provider.label}\x1b[K\n`);
|
|
65
|
-
}
|
|
66
|
-
output.write(' space toggles, enter confirms\x1b[K\n');
|
|
67
|
-
rendered = true;
|
|
68
|
-
};
|
|
69
|
-
const cleanup = () => {
|
|
70
|
-
input.off('keypress', onKey);
|
|
71
|
-
input.setRawMode?.(false);
|
|
72
|
-
input.pause();
|
|
73
|
-
output.write('\x1b[?25h');
|
|
74
|
-
};
|
|
75
|
-
const done = () => {
|
|
76
|
-
cleanup();
|
|
77
|
-
line(output);
|
|
78
|
-
resolve(providers.filter((provider) => selected.has(provider.id)));
|
|
79
|
-
};
|
|
80
|
-
const cancel = () => {
|
|
81
|
-
cleanup();
|
|
82
|
-
line(output);
|
|
83
|
-
reject(new Error('Canceled.'));
|
|
84
|
-
};
|
|
85
|
-
const move = (delta) => {
|
|
86
|
-
cursor = (cursor + delta + providers.length) % providers.length;
|
|
87
|
-
render();
|
|
88
|
-
};
|
|
89
|
-
const toggle = () => {
|
|
90
|
-
const provider = providers[cursor];
|
|
91
|
-
if (!provider)
|
|
92
|
-
return;
|
|
93
|
-
const next = new Set(selected);
|
|
94
|
-
if (next.has(provider.id))
|
|
95
|
-
next.delete(provider.id);
|
|
96
|
-
else
|
|
97
|
-
next.add(provider.id);
|
|
98
|
-
selected = next;
|
|
99
|
-
render();
|
|
100
|
-
};
|
|
101
|
-
const onKey = (_chunk, key = {}) => {
|
|
102
|
-
if (key.ctrl && key.name === 'c') {
|
|
103
|
-
cancel();
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (key.name === 'up' || key.name === 'k') {
|
|
107
|
-
move(-1);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
if (key.name === 'down' || key.name === 'j') {
|
|
111
|
-
move(1);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
if (key.name === 'space' || key.sequence === ' ') {
|
|
115
|
-
toggle();
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
if (key.name === 'return' || key.name === 'enter')
|
|
119
|
-
done();
|
|
120
|
-
};
|
|
121
|
-
emitKeypressEvents(input);
|
|
122
|
-
input.setRawMode?.(true);
|
|
123
|
-
input.resume();
|
|
124
|
-
input.on('keypress', onKey);
|
|
125
|
-
output.write('\x1b[?25l');
|
|
126
|
-
render();
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
async function readAuth(path) {
|
|
130
|
-
try {
|
|
131
|
-
const raw = await readFile(path, 'utf8');
|
|
132
|
-
const parsed = JSON.parse(raw);
|
|
133
|
-
return object(parsed);
|
|
134
|
-
}
|
|
135
|
-
catch (err) {
|
|
136
|
-
if (code(err) === 'ENOENT')
|
|
137
|
-
return {};
|
|
138
|
-
throw err;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
function openCodeProviders() {
|
|
142
|
-
return PROVIDERS;
|
|
143
|
-
}
|
|
144
|
-
function object(value) {
|
|
145
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
146
|
-
return {};
|
|
147
|
-
return value;
|
|
148
|
-
}
|
|
149
|
-
function canSelect(input) {
|
|
150
|
-
const tty = input;
|
|
151
|
-
return tty.isTTY === true && typeof tty.setRawMode === 'function';
|
|
152
|
-
}
|
|
153
|
-
function code(err) {
|
|
154
|
-
if (typeof err !== 'object' || err === null || !('code' in err))
|
|
155
|
-
return undefined;
|
|
156
|
-
const value = err.code;
|
|
157
|
-
return typeof value === 'string' ? value : undefined;
|
|
158
|
-
}
|
package/dist/apps.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
// Interactive app selection for the post-login setup flow.
|
|
2
|
-
import { constants } from 'node:fs';
|
|
3
|
-
import { access } from 'node:fs/promises';
|
|
4
|
-
import { delimiter, join } from 'node:path';
|
|
5
|
-
import { emitKeypressEvents } from 'node:readline';
|
|
6
|
-
import { HARNESSES } from './harnesses.js';
|
|
7
|
-
import { line, note, prompt } from './terminal.js';
|
|
8
|
-
export const APPS = HARNESSES;
|
|
9
|
-
export async function selectApps(input = process.stdin, output = process.stdout, apps) {
|
|
10
|
-
const available = apps ?? (await detectApps());
|
|
11
|
-
if (available.length === 0) {
|
|
12
|
-
note(output, 'Skipping app setup because no supported local harnesses were detected');
|
|
13
|
-
return [];
|
|
14
|
-
}
|
|
15
|
-
if (canSelect(input, output))
|
|
16
|
-
return selectTty(available, input, output);
|
|
17
|
-
note(output, 'Skipping app setup because this terminal is not interactive');
|
|
18
|
-
return [];
|
|
19
|
-
}
|
|
20
|
-
export async function detectApps(env = process.env, platform = process.platform) {
|
|
21
|
-
const checks = await Promise.all(APPS.map(async (app) => ((await detected(app, env, platform)) ? app : null)));
|
|
22
|
-
return checks.filter((app) => app !== null);
|
|
23
|
-
}
|
|
24
|
-
export function appLabels(ids) {
|
|
25
|
-
if (ids.length === 0)
|
|
26
|
-
return 'none';
|
|
27
|
-
const labels = ids.map((id) => APPS.find((app) => app.id === id)?.label ?? id);
|
|
28
|
-
return labels.join(', ');
|
|
29
|
-
}
|
|
30
|
-
function selectTty(apps, input, output) {
|
|
31
|
-
return new Promise((resolve, reject) => {
|
|
32
|
-
const initial = new Set();
|
|
33
|
-
let cursor = 0;
|
|
34
|
-
let selected = initial;
|
|
35
|
-
let rendered = false;
|
|
36
|
-
const render = () => {
|
|
37
|
-
if (rendered)
|
|
38
|
-
output.write(`\x1b[${apps.length + 2}A`);
|
|
39
|
-
output.write(`${prompt(output, 'choose the apps you want to use Sleev with')}\x1b[K\n`);
|
|
40
|
-
for (const [index, app] of apps.entries()) {
|
|
41
|
-
const marker = index === cursor ? '>' : ' ';
|
|
42
|
-
const checked = selected.has(app.id) ? 'x' : ' ';
|
|
43
|
-
output.write(`${marker} (${checked}) ${app.label}\x1b[K\n`);
|
|
44
|
-
}
|
|
45
|
-
output.write(' space toggles, enter confirms\x1b[K\n');
|
|
46
|
-
rendered = true;
|
|
47
|
-
};
|
|
48
|
-
const cleanup = () => {
|
|
49
|
-
input.off('keypress', onKey);
|
|
50
|
-
input.setRawMode?.(false);
|
|
51
|
-
input.pause();
|
|
52
|
-
output.write('\x1b[?25h');
|
|
53
|
-
};
|
|
54
|
-
const done = () => {
|
|
55
|
-
cleanup();
|
|
56
|
-
line(output);
|
|
57
|
-
resolve(apps.filter((app) => selected.has(app.id)).map((app) => app.id));
|
|
58
|
-
};
|
|
59
|
-
const cancel = () => {
|
|
60
|
-
cleanup();
|
|
61
|
-
line(output);
|
|
62
|
-
reject(new Error('Canceled.'));
|
|
63
|
-
};
|
|
64
|
-
const move = (delta) => {
|
|
65
|
-
cursor = (cursor + delta + apps.length) % apps.length;
|
|
66
|
-
render();
|
|
67
|
-
};
|
|
68
|
-
const toggle = () => {
|
|
69
|
-
const app = apps[cursor];
|
|
70
|
-
if (!app)
|
|
71
|
-
return;
|
|
72
|
-
const next = new Set(selected);
|
|
73
|
-
if (next.has(app.id))
|
|
74
|
-
next.delete(app.id);
|
|
75
|
-
else
|
|
76
|
-
next.add(app.id);
|
|
77
|
-
selected = next;
|
|
78
|
-
render();
|
|
79
|
-
};
|
|
80
|
-
const toggleAll = () => {
|
|
81
|
-
selected = selected.size === apps.length ? new Set() : new Set(apps.map((app) => app.id));
|
|
82
|
-
render();
|
|
83
|
-
};
|
|
84
|
-
const onKey = (_chunk, key = {}) => {
|
|
85
|
-
if (key.ctrl && key.name === 'c') {
|
|
86
|
-
cancel();
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
if (key.name === 'up' || key.name === 'k') {
|
|
90
|
-
move(-1);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
if (key.name === 'down' || key.name === 'j') {
|
|
94
|
-
move(1);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
if (key.name === 'space' || key.sequence === ' ') {
|
|
98
|
-
toggle();
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (key.name === 'a' || key.sequence === 'a') {
|
|
102
|
-
toggleAll();
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (key.name === 'return' || key.name === 'enter')
|
|
106
|
-
done();
|
|
107
|
-
};
|
|
108
|
-
emitKeypressEvents(input);
|
|
109
|
-
input.setRawMode?.(true);
|
|
110
|
-
input.resume();
|
|
111
|
-
input.on('keypress', onKey);
|
|
112
|
-
output.write('\x1b[?25l');
|
|
113
|
-
render();
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
function canSelect(input, output) {
|
|
117
|
-
const tty = input;
|
|
118
|
-
return tty.isTTY === true && typeof tty.setRawMode === 'function';
|
|
119
|
-
}
|
|
120
|
-
async function exists(command, env, platform) {
|
|
121
|
-
const path = env.PATH ?? '';
|
|
122
|
-
const dirs = path.split(delimiter).filter(Boolean);
|
|
123
|
-
const names = platform === 'win32' ? windowsNames(command, env) : [command];
|
|
124
|
-
const paths = dirs.flatMap((dir) => names.map((name) => join(dir, name)));
|
|
125
|
-
const checks = await Promise.all(paths.map((path) => executable(path)));
|
|
126
|
-
return checks.some(Boolean);
|
|
127
|
-
}
|
|
128
|
-
async function detected(app, env, platform) {
|
|
129
|
-
if (!(await exists(app.command, env, platform)))
|
|
130
|
-
return false;
|
|
131
|
-
if (!app.available)
|
|
132
|
-
return true;
|
|
133
|
-
return app.available();
|
|
134
|
-
}
|
|
135
|
-
function windowsNames(command, env) {
|
|
136
|
-
const ext = env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD';
|
|
137
|
-
const exts = ext.split(';').filter(Boolean);
|
|
138
|
-
return [command, ...exts.map((item) => `${command}${item.toLowerCase()}`), ...exts.map((item) => `${command}${item.toUpperCase()}`)];
|
|
139
|
-
}
|
|
140
|
-
async function executable(path) {
|
|
141
|
-
try {
|
|
142
|
-
await access(path, constants.X_OK);
|
|
143
|
-
return true;
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
return false;
|
|
147
|
-
}
|
|
148
|
-
}
|
package/dist/auth/browser.js
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
// Cross-platform browser opener used by the login flow.
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
-
export async function openBrowser(url) {
|
|
5
|
-
const commands = browserCommands(url);
|
|
6
|
-
for (const command of commands) {
|
|
7
|
-
const opened = await open(command);
|
|
8
|
-
if (opened)
|
|
9
|
-
return true;
|
|
10
|
-
}
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
function open(command) {
|
|
14
|
-
return new Promise((resolve) => {
|
|
15
|
-
const child = spawn(command.command, command.args, {
|
|
16
|
-
stdio: 'ignore',
|
|
17
|
-
windowsHide: true,
|
|
18
|
-
});
|
|
19
|
-
const timer = setTimeout(() => finish(true), 3000);
|
|
20
|
-
let done = false;
|
|
21
|
-
const finish = (ok) => {
|
|
22
|
-
if (done)
|
|
23
|
-
return;
|
|
24
|
-
done = true;
|
|
25
|
-
clearTimeout(timer);
|
|
26
|
-
child.removeAllListeners('error');
|
|
27
|
-
child.removeAllListeners('close');
|
|
28
|
-
if (ok)
|
|
29
|
-
child.unref();
|
|
30
|
-
resolve(ok);
|
|
31
|
-
};
|
|
32
|
-
child.once('error', () => finish(false));
|
|
33
|
-
child.once('close', (code) => finish(code === 0));
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
export function browserCommands(url, context = {}) {
|
|
37
|
-
const platform = context.platform ?? process.platform;
|
|
38
|
-
if (platform === 'darwin')
|
|
39
|
-
return [{ command: 'open', args: [url] }];
|
|
40
|
-
if (platform === 'win32') {
|
|
41
|
-
return [
|
|
42
|
-
{ command: 'rundll32', args: ['url.dll,FileProtocolHandler', url] },
|
|
43
|
-
{ command: 'powershell', args: powershellArgs(url) },
|
|
44
|
-
];
|
|
45
|
-
}
|
|
46
|
-
if (platform === 'linux' && isWsl(context)) {
|
|
47
|
-
return [
|
|
48
|
-
...linuxCommands(url),
|
|
49
|
-
{ command: 'wslview', args: [url] },
|
|
50
|
-
{ command: 'powershell.exe', args: powershellArgs(url) },
|
|
51
|
-
];
|
|
52
|
-
}
|
|
53
|
-
return linuxCommands(url);
|
|
54
|
-
}
|
|
55
|
-
function linuxCommands(url) {
|
|
56
|
-
return [
|
|
57
|
-
{ command: 'sensible-browser', args: [url] },
|
|
58
|
-
{ command: 'gio', args: ['open', url] },
|
|
59
|
-
{ command: 'xdg-open', args: [url] },
|
|
60
|
-
];
|
|
61
|
-
}
|
|
62
|
-
function powershellArgs(url) {
|
|
63
|
-
return ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', `Start-Process '${ps(url)}'`];
|
|
64
|
-
}
|
|
65
|
-
function ps(value) {
|
|
66
|
-
return value.replace(/'/g, "''");
|
|
67
|
-
}
|
|
68
|
-
function isWsl(context) {
|
|
69
|
-
const env = context.env ?? process.env;
|
|
70
|
-
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP)
|
|
71
|
-
return true;
|
|
72
|
-
const release = context.release ?? kernelRelease();
|
|
73
|
-
return release.toLowerCase().includes('microsoft') || release.toLowerCase().includes('wsl');
|
|
74
|
-
}
|
|
75
|
-
function kernelRelease() {
|
|
76
|
-
const path = '/proc/sys/kernel/osrelease';
|
|
77
|
-
if (!existsSync(path))
|
|
78
|
-
return '';
|
|
79
|
-
return readFileSync(path, 'utf8');
|
|
80
|
-
}
|