@wolffycode/selo 0.1.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.
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/bin/selo.js +16 -0
- package/package.json +36 -0
- package/src/cli.js +352 -0
- package/src/core/launch.js +87 -0
- package/src/core/provider.js +171 -0
- package/src/store/cc-switch.js +180 -0
- package/src/store/selo-config.js +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wang Bingkun
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# selo
|
|
2
|
+
|
|
3
|
+
在终端中读取 `CC Switch` 的 Claude provider,并用选中的配置启动 `claude`。
|
|
4
|
+
|
|
5
|
+
`selo` 是个人开发者工具,不是 `CC Switch` 官方工具。
|
|
6
|
+
|
|
7
|
+
它解决两个问题:
|
|
8
|
+
|
|
9
|
+
- 避开 Unix/Linux/macOS 上 `cc` 命令和系统编译器的命名冲突
|
|
10
|
+
- 在终端里快速切换不同 Claude provider,并尽量与 `CC Switch` 当前状态保持一致
|
|
11
|
+
|
|
12
|
+
## Prerequisites
|
|
13
|
+
|
|
14
|
+
- 已安装并配置 `CC Switch`
|
|
15
|
+
- 已安装 `claude` CLI
|
|
16
|
+
- 已安装系统 `sqlite3`
|
|
17
|
+
|
|
18
|
+
`selo` 把 `CC Switch` 当成硬性前提。它不会自己管理 provider,也不会替你安装 `claude`。
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
当前仓库代码已经可用,但**还没有发布到 npm**。
|
|
23
|
+
|
|
24
|
+
### Local development
|
|
25
|
+
|
|
26
|
+
在本机调试时,使用:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cd /Users/wangbingkun/Desktop/person/selo
|
|
30
|
+
env npm_config_prefix=$HOME/.local npm link
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### After npm publish
|
|
34
|
+
|
|
35
|
+
发布到 npm 之后,再使用:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm i -g @wolffycode/selo
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
selo
|
|
45
|
+
selo -v
|
|
46
|
+
selo -d
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`-d` 会把 `--dangerously-skip-permissions` 传给 `claude`。
|
|
50
|
+
|
|
51
|
+
## How It Works
|
|
52
|
+
|
|
53
|
+
- 从 `~/.cc-switch/cc-switch.db` 读取 Claude providers
|
|
54
|
+
- 从 `~/.cc-switch/settings.json` 读取当前选中的 Claude provider
|
|
55
|
+
- 如果 provider 开启了 `commonConfigEnabled`,会合并 `common_config_claude`
|
|
56
|
+
- 打开 picker 时监听 `CC Switch` 配置变化,列表会自动刷新
|
|
57
|
+
- 按下回车启动前,会再次按 provider id 读取最新配置
|
|
58
|
+
- 真正启动 `claude` 时,会写入一份临时 settings 文件,运行中的 Claude 会话不会被后续的 `CC Switch` 改动污染
|
|
59
|
+
|
|
60
|
+
## Consistency Rules
|
|
61
|
+
|
|
62
|
+
默认高亮优先级:
|
|
63
|
+
|
|
64
|
+
1. `CC Switch.settings.json.currentProviderClaude`
|
|
65
|
+
2. `providers.is_current = 1`
|
|
66
|
+
3. `selo` 本地记录的上次选择
|
|
67
|
+
4. 第一条 provider
|
|
68
|
+
|
|
69
|
+
这和你当前本地 `cc` 最大的区别是:`selo` 不会让自己的本地状态覆盖 `CC Switch` 当前 provider。
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm test
|
|
75
|
+
node bin/selo.js -v
|
|
76
|
+
```
|
package/bin/selo.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { run } = require('../src/cli.js');
|
|
4
|
+
|
|
5
|
+
run(process.argv.slice(2)).catch((error) => {
|
|
6
|
+
if (error && error.signal) {
|
|
7
|
+
process.kill(process.pid, error.signal);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
process.stderr.write(`${error.message}\n`);
|
|
11
|
+
process.exit(error && error.exitCode ? error.exitCode : 1);
|
|
12
|
+
}).then((exitCode) => {
|
|
13
|
+
if (typeof exitCode === 'number') {
|
|
14
|
+
process.exit(exitCode);
|
|
15
|
+
}
|
|
16
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wolffycode/selo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Launch Claude with providers from CC Switch.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"selo": "bin/selo.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"claude",
|
|
21
|
+
"cc-switch",
|
|
22
|
+
"cli",
|
|
23
|
+
"launcher"
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/WolffyCode/selo.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/WolffyCode/selo/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/WolffyCode/selo#readme",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const readline = require('node:readline');
|
|
3
|
+
const { execFileSync, spawn } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
const { version: VERSION } = require('../package.json');
|
|
6
|
+
const { createLaunchPlan } = require('./core/launch.js');
|
|
7
|
+
const { getSubText, resolveDefaultProviderId } = require('./core/provider.js');
|
|
8
|
+
const {
|
|
9
|
+
DB_PATH,
|
|
10
|
+
SETTINGS_PATH,
|
|
11
|
+
getFingerprint,
|
|
12
|
+
loadCommonClaudeSettings,
|
|
13
|
+
loadProviderById,
|
|
14
|
+
loadSnapshot,
|
|
15
|
+
} = require('./store/cc-switch.js');
|
|
16
|
+
const { loadSeloSettings, saveSeloSettings } = require('./store/selo-config.js');
|
|
17
|
+
|
|
18
|
+
function buildVersionString(version = VERSION) {
|
|
19
|
+
return `selo v${version}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseCliArgs(argv) {
|
|
23
|
+
const args = [...argv];
|
|
24
|
+
|
|
25
|
+
if (args[0] === '-v' || args[0] === '--version') {
|
|
26
|
+
return {
|
|
27
|
+
showVersion: true,
|
|
28
|
+
danger: false,
|
|
29
|
+
claudeArgs: [],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let danger = false;
|
|
34
|
+
if (args[0] === '-d') {
|
|
35
|
+
danger = true;
|
|
36
|
+
args.shift();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
showVersion: false,
|
|
41
|
+
danger,
|
|
42
|
+
claudeArgs: args,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function reconcileSelection(
|
|
47
|
+
selectedId,
|
|
48
|
+
previousRows,
|
|
49
|
+
nextRows,
|
|
50
|
+
switchSettings = {},
|
|
51
|
+
seloSettings = {}
|
|
52
|
+
) {
|
|
53
|
+
if (!Array.isArray(nextRows) || nextRows.length === 0) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (selectedId && nextRows.some((row) => row.id === selectedId)) {
|
|
58
|
+
return selectedId;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return resolveDefaultProviderId(nextRows, switchSettings, seloSettings);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function assertClaudeAvailable({ execFileSyncFn = execFileSync } = {}) {
|
|
65
|
+
try {
|
|
66
|
+
execFileSyncFn('claude', ['--version'], { stdio: 'ignore' });
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error && error.code === 'ENOENT') {
|
|
69
|
+
throw new Error('claude CLI is required. Please install Claude Code first.');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function createSnapshotWatcher({
|
|
77
|
+
switchDir = require('node:path').dirname(DB_PATH),
|
|
78
|
+
pollIntervalMs = 750,
|
|
79
|
+
watchFn = fs.watch,
|
|
80
|
+
getFingerprintFn = getFingerprint,
|
|
81
|
+
onChange,
|
|
82
|
+
onError,
|
|
83
|
+
initialFingerprint,
|
|
84
|
+
setIntervalFn = setInterval,
|
|
85
|
+
clearIntervalFn = clearInterval,
|
|
86
|
+
} = {}) {
|
|
87
|
+
let closed = false;
|
|
88
|
+
let currentFingerprint = initialFingerprint;
|
|
89
|
+
let checking = false;
|
|
90
|
+
let queued = false;
|
|
91
|
+
let watcher = null;
|
|
92
|
+
|
|
93
|
+
const fingerprintsEqual = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
94
|
+
|
|
95
|
+
const check = async () => {
|
|
96
|
+
if (closed || checking) {
|
|
97
|
+
queued = true;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
checking = true;
|
|
102
|
+
try {
|
|
103
|
+
const nextFingerprint = await getFingerprintFn();
|
|
104
|
+
if (!fingerprintsEqual(nextFingerprint, currentFingerprint)) {
|
|
105
|
+
currentFingerprint = nextFingerprint;
|
|
106
|
+
await onChange(nextFingerprint);
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (onError) {
|
|
110
|
+
onError(error);
|
|
111
|
+
}
|
|
112
|
+
} finally {
|
|
113
|
+
checking = false;
|
|
114
|
+
if (queued && !closed) {
|
|
115
|
+
queued = false;
|
|
116
|
+
void check();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
watcher = watchFn(switchDir, () => {
|
|
123
|
+
void check();
|
|
124
|
+
});
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (onError) {
|
|
127
|
+
onError(error);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const interval = setIntervalFn(() => {
|
|
132
|
+
void check();
|
|
133
|
+
}, pollIntervalMs);
|
|
134
|
+
|
|
135
|
+
if (typeof interval.unref === 'function') {
|
|
136
|
+
interval.unref();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
close() {
|
|
141
|
+
closed = true;
|
|
142
|
+
if (watcher) {
|
|
143
|
+
watcher.close();
|
|
144
|
+
}
|
|
145
|
+
clearIntervalFn(interval);
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function clearScreen(stream) {
|
|
151
|
+
stream.write('\x1b[2J\x1b[H');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function restoreTerminal(input) {
|
|
155
|
+
if (input.isTTY && typeof input.setRawMode === 'function') {
|
|
156
|
+
input.setRawMode(false);
|
|
157
|
+
}
|
|
158
|
+
input.pause();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function drawPicker(stream, rows, selectedId, footerMessage = '') {
|
|
162
|
+
const B = '\x1b[1m';
|
|
163
|
+
const CY = '\x1b[36m';
|
|
164
|
+
const D = '\x1b[2m';
|
|
165
|
+
const R = '\x1b[0m';
|
|
166
|
+
|
|
167
|
+
clearScreen(stream);
|
|
168
|
+
stream.write('\n ' + CY + B + 'SELO - Select Provider' + R + '\n\n');
|
|
169
|
+
rows.forEach((row) => {
|
|
170
|
+
const active = row.id === selectedId;
|
|
171
|
+
const prefix = active ? ' ' + CY + B + '\u276f ' + R : ' ';
|
|
172
|
+
const name = active ? B + row.name + R : row.name;
|
|
173
|
+
const sub = D + getSubText(row) + R;
|
|
174
|
+
stream.write(prefix + name + '\n');
|
|
175
|
+
stream.write(' ' + sub + '\n');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
stream.write('\n ' + D + '\u2191\u2193 navigate Enter select Esc cancel' + R + '\n');
|
|
179
|
+
if (footerMessage) {
|
|
180
|
+
stream.write('\n ' + footerMessage + '\n');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function run(argv = [], deps = {}) {
|
|
185
|
+
const {
|
|
186
|
+
stdin = process.stdin,
|
|
187
|
+
stdout = process.stdout,
|
|
188
|
+
stderr = process.stderr,
|
|
189
|
+
spawnFn = spawn,
|
|
190
|
+
loadSnapshotFn = loadSnapshot,
|
|
191
|
+
loadProviderByIdFn = loadProviderById,
|
|
192
|
+
loadCommonClaudeSettingsFn = loadCommonClaudeSettings,
|
|
193
|
+
loadSeloSettingsFn = loadSeloSettings,
|
|
194
|
+
saveSeloSettingsFn = saveSeloSettings,
|
|
195
|
+
createLaunchPlanFn = createLaunchPlan,
|
|
196
|
+
createSnapshotWatcherFn = createSnapshotWatcher,
|
|
197
|
+
assertClaudeAvailableFn = assertClaudeAvailable,
|
|
198
|
+
} = deps;
|
|
199
|
+
const parsedArgs = parseCliArgs(argv);
|
|
200
|
+
|
|
201
|
+
if (parsedArgs.showVersion) {
|
|
202
|
+
stdout.write(buildVersionString(VERSION) + '\n');
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!stdin.isTTY || typeof stdin.setRawMode !== 'function') {
|
|
207
|
+
throw new Error('selo requires an interactive terminal.');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let snapshot = await loadSnapshotFn();
|
|
211
|
+
let seloSettings = await loadSeloSettingsFn();
|
|
212
|
+
let selectedId = resolveDefaultProviderId(
|
|
213
|
+
snapshot.providers,
|
|
214
|
+
snapshot.switchSettings,
|
|
215
|
+
seloSettings
|
|
216
|
+
);
|
|
217
|
+
let footerMessage = '';
|
|
218
|
+
let reloading = false;
|
|
219
|
+
let queuedReload = false;
|
|
220
|
+
|
|
221
|
+
const reloadSnapshot = async () => {
|
|
222
|
+
if (reloading) {
|
|
223
|
+
queuedReload = true;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
reloading = true;
|
|
228
|
+
try {
|
|
229
|
+
do {
|
|
230
|
+
queuedReload = false;
|
|
231
|
+
const nextSnapshot = await loadSnapshotFn();
|
|
232
|
+
selectedId = reconcileSelection(
|
|
233
|
+
selectedId,
|
|
234
|
+
snapshot.providers,
|
|
235
|
+
nextSnapshot.providers,
|
|
236
|
+
nextSnapshot.switchSettings,
|
|
237
|
+
seloSettings
|
|
238
|
+
);
|
|
239
|
+
snapshot = nextSnapshot;
|
|
240
|
+
footerMessage = 'CC Switch updated. Picker reloaded.';
|
|
241
|
+
drawPicker(stderr, snapshot.providers, selectedId, footerMessage);
|
|
242
|
+
} while (queuedReload);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
footerMessage = `Reload failed: ${error.message}`;
|
|
245
|
+
drawPicker(stderr, snapshot.providers, selectedId, footerMessage);
|
|
246
|
+
} finally {
|
|
247
|
+
reloading = false;
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const watcher = createSnapshotWatcherFn({
|
|
252
|
+
initialFingerprint: snapshot.fingerprint,
|
|
253
|
+
onChange: reloadSnapshot,
|
|
254
|
+
onError: (error) => {
|
|
255
|
+
footerMessage = `Watch failed: ${error.message}`;
|
|
256
|
+
drawPicker(stderr, snapshot.providers, selectedId, footerMessage);
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
readline.emitKeypressEvents(stdin);
|
|
261
|
+
stdin.setRawMode(true);
|
|
262
|
+
stdin.resume();
|
|
263
|
+
drawPicker(stderr, snapshot.providers, selectedId, footerMessage);
|
|
264
|
+
|
|
265
|
+
return new Promise((resolve, reject) => {
|
|
266
|
+
const finish = (error, exitCode = 0) => {
|
|
267
|
+
watcher.close();
|
|
268
|
+
stdin.removeListener('keypress', onKeypress);
|
|
269
|
+
restoreTerminal(stdin);
|
|
270
|
+
if (error) {
|
|
271
|
+
reject(error);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
resolve(exitCode);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const onKeypress = async (_, key) => {
|
|
278
|
+
if (key.name === 'up' || key.name === 'down') {
|
|
279
|
+
const rows = snapshot.providers;
|
|
280
|
+
const currentIndex = Math.max(0, rows.findIndex((row) => row.id === selectedId));
|
|
281
|
+
const delta = key.name === 'up' ? -1 : 1;
|
|
282
|
+
const nextIndex = (currentIndex + delta + rows.length) % rows.length;
|
|
283
|
+
selectedId = rows[nextIndex].id;
|
|
284
|
+
footerMessage = '';
|
|
285
|
+
drawPicker(stderr, rows, selectedId, footerMessage);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (key.name === 'return') {
|
|
290
|
+
watcher.close();
|
|
291
|
+
stdin.removeListener('keypress', onKeypress);
|
|
292
|
+
restoreTerminal(stdin);
|
|
293
|
+
clearScreen(stderr);
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const latestProvider = loadProviderByIdFn(selectedId);
|
|
297
|
+
const commonSettings = loadCommonClaudeSettingsFn();
|
|
298
|
+
seloSettings = await loadSeloSettingsFn();
|
|
299
|
+
await saveSeloSettingsFn({
|
|
300
|
+
...seloSettings,
|
|
301
|
+
lastProviderClaude: latestProvider.id,
|
|
302
|
+
});
|
|
303
|
+
assertClaudeAvailableFn();
|
|
304
|
+
|
|
305
|
+
const launchPlan = await createLaunchPlanFn({
|
|
306
|
+
provider: latestProvider,
|
|
307
|
+
commonSettings,
|
|
308
|
+
danger: parsedArgs.danger,
|
|
309
|
+
claudeArgs: parsedArgs.claudeArgs,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const child = spawnFn(launchPlan.command, launchPlan.args, {
|
|
313
|
+
stdio: 'inherit',
|
|
314
|
+
env: launchPlan.env,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
child.on('error', async (error) => {
|
|
318
|
+
await launchPlan.cleanup();
|
|
319
|
+
reject(error.code === 'ENOENT'
|
|
320
|
+
? new Error('claude CLI is required. Please install Claude Code first.')
|
|
321
|
+
: error);
|
|
322
|
+
});
|
|
323
|
+
child.on('exit', async (code, signal) => {
|
|
324
|
+
await launchPlan.cleanup();
|
|
325
|
+
if (signal) {
|
|
326
|
+
reject({ signal });
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
resolve(code ?? 0);
|
|
330
|
+
});
|
|
331
|
+
} catch (error) {
|
|
332
|
+
reject(error);
|
|
333
|
+
}
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
|
|
338
|
+
finish(null, 1);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
stdin.on('keypress', onKeypress);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
module.exports = {
|
|
347
|
+
buildVersionString,
|
|
348
|
+
createSnapshotWatcher,
|
|
349
|
+
parseCliArgs,
|
|
350
|
+
reconcileSelection,
|
|
351
|
+
run,
|
|
352
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const fs = require('node:fs/promises');
|
|
2
|
+
const os = require('node:os');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
buildChildEnv,
|
|
7
|
+
getPreferredModel,
|
|
8
|
+
resolveEffectiveSettings,
|
|
9
|
+
} = require('./provider.js');
|
|
10
|
+
|
|
11
|
+
async function createLaunchPlan({
|
|
12
|
+
provider,
|
|
13
|
+
commonSettings,
|
|
14
|
+
danger,
|
|
15
|
+
claudeArgs,
|
|
16
|
+
tempDir,
|
|
17
|
+
mkdtempFn = fs.mkdtemp,
|
|
18
|
+
writeFileFn = fs.writeFile,
|
|
19
|
+
unlinkFn = fs.unlink,
|
|
20
|
+
rmFn = fs.rm,
|
|
21
|
+
} = {}) {
|
|
22
|
+
const settings = resolveEffectiveSettings(provider, commonSettings);
|
|
23
|
+
const ownedTempDir = tempDir || await mkdtempFn(path.join(os.tmpdir(), 'selo-settings-'));
|
|
24
|
+
const settingsPath = path.join(
|
|
25
|
+
ownedTempDir,
|
|
26
|
+
`claude-settings-${process.pid}-${Date.now()}.json`
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
await writeFileFn(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
30
|
+
|
|
31
|
+
let cleaned = false;
|
|
32
|
+
const cleanup = async () => {
|
|
33
|
+
if (cleaned) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
cleaned = true;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await unlinkFn(settingsPath);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (!error || error.code !== 'ENOENT') {
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!tempDir) {
|
|
48
|
+
try {
|
|
49
|
+
await rmFn(ownedTempDir, { recursive: true, force: true });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (!error || error.code !== 'ENOENT') {
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const launchArgs = Array.isArray(claudeArgs) ? [...claudeArgs] : [];
|
|
59
|
+
const args = ['--setting-sources', 'project,local', '--settings', settingsPath];
|
|
60
|
+
const hasExplicitModelArg = launchArgs.some(
|
|
61
|
+
(arg, index) => arg === '--model' && index < launchArgs.length - 1
|
|
62
|
+
);
|
|
63
|
+
const preferredModel = getPreferredModel(settings);
|
|
64
|
+
|
|
65
|
+
if (preferredModel && !hasExplicitModelArg) {
|
|
66
|
+
args.push('--model', preferredModel);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (danger) {
|
|
70
|
+
args.push('--dangerously-skip-permissions');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
args.push(...launchArgs);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
command: 'claude',
|
|
77
|
+
args,
|
|
78
|
+
env: buildChildEnv(settings),
|
|
79
|
+
settings,
|
|
80
|
+
settingsPath,
|
|
81
|
+
cleanup,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
createLaunchPlan,
|
|
87
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const ENV_RESET_KEYS = new Set([
|
|
2
|
+
'API_TIMEOUT_MS',
|
|
3
|
+
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
|
|
4
|
+
]);
|
|
5
|
+
|
|
6
|
+
function parseJson(text, errorMessage) {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(text);
|
|
9
|
+
} catch (error) {
|
|
10
|
+
throw new Error(`${errorMessage}: ${error.message}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseProviderSettings(provider) {
|
|
15
|
+
return parseJson(
|
|
16
|
+
provider.settings_config,
|
|
17
|
+
`Invalid settings_config for provider "${provider.name}"`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseProviderMeta(provider) {
|
|
22
|
+
if (!provider || typeof provider.meta !== 'string' || provider.meta.trim() === '') {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return parseJson(provider.meta, `Invalid meta for provider "${provider.name}"`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isPlainObject(value) {
|
|
30
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function cloneJson(value) {
|
|
34
|
+
return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function mergeSettings(baseSettings = {}, overrideSettings = {}) {
|
|
38
|
+
const merged = isPlainObject(baseSettings) ? cloneJson(baseSettings) : {};
|
|
39
|
+
|
|
40
|
+
if (!isPlainObject(overrideSettings)) {
|
|
41
|
+
return merged;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Object.entries(overrideSettings).forEach(([key, value]) => {
|
|
45
|
+
if (isPlainObject(value) && isPlainObject(merged[key])) {
|
|
46
|
+
merged[key] = mergeSettings(merged[key], value);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
merged[key] = cloneJson(value);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return merged;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getPreferredModel(settings = {}) {
|
|
57
|
+
const env = settings.env && typeof settings.env === 'object' ? settings.env : {};
|
|
58
|
+
|
|
59
|
+
return env.ANTHROPIC_MODEL
|
|
60
|
+
|| env.ANTHROPIC_DEFAULT_OPUS_MODEL
|
|
61
|
+
|| env.ANTHROPIC_DEFAULT_SONNET_MODEL
|
|
62
|
+
|| env.ANTHROPIC_DEFAULT_HAIKU_MODEL
|
|
63
|
+
|| env.ANTHROPIC_REASONING_MODEL
|
|
64
|
+
|| settings.model
|
|
65
|
+
|| '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeProviderSettings(settings = {}) {
|
|
69
|
+
const normalized = JSON.parse(JSON.stringify(settings || {}));
|
|
70
|
+
const env = normalized.env && typeof normalized.env === 'object' ? normalized.env : {};
|
|
71
|
+
normalized.env = env;
|
|
72
|
+
|
|
73
|
+
const preferredModel = getPreferredModel(normalized);
|
|
74
|
+
if (preferredModel && !env.ANTHROPIC_MODEL) {
|
|
75
|
+
env.ANTHROPIC_MODEL = preferredModel;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (preferredModel && !env.ANTHROPIC_REASONING_MODEL) {
|
|
79
|
+
env.ANTHROPIC_REASONING_MODEL = preferredModel;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (preferredModel) {
|
|
83
|
+
normalized.model = preferredModel;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return normalized;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getBaseUrl(settingsConfig) {
|
|
90
|
+
try {
|
|
91
|
+
const cfg = JSON.parse(settingsConfig);
|
|
92
|
+
return cfg.env && cfg.env.ANTHROPIC_BASE_URL ? cfg.env.ANTHROPIC_BASE_URL : '';
|
|
93
|
+
} catch {
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getSubText(row) {
|
|
99
|
+
if (row.notes && row.notes.trim()) {
|
|
100
|
+
return row.notes;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return getBaseUrl(row.settings_config);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveEffectiveSettings(provider, commonSettings = {}) {
|
|
107
|
+
const providerSettings = parseProviderSettings(provider);
|
|
108
|
+
const providerMeta = parseProviderMeta(provider);
|
|
109
|
+
return normalizeProviderSettings(
|
|
110
|
+
providerMeta.commonConfigEnabled
|
|
111
|
+
? mergeSettings(commonSettings, providerSettings)
|
|
112
|
+
: providerSettings
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function buildChildEnv(settings) {
|
|
117
|
+
const childEnv = { ...process.env };
|
|
118
|
+
|
|
119
|
+
Object.keys(childEnv).forEach((key) => {
|
|
120
|
+
if (key.startsWith('ANTHROPIC_') || ENV_RESET_KEYS.has(key)) {
|
|
121
|
+
delete childEnv[key];
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (settings.env && typeof settings.env === 'object') {
|
|
126
|
+
Object.assign(childEnv, settings.env);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return childEnv;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolveDefaultProviderId(rows, switchSettings = {}, seloSettings = {}) {
|
|
133
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (switchSettings.currentProviderClaude) {
|
|
138
|
+
const currentRow = rows.find((row) => row.id === switchSettings.currentProviderClaude);
|
|
139
|
+
if (currentRow) {
|
|
140
|
+
return currentRow.id;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const isCurrentRow = rows.find((row) => Number(row.is_current) === 1 || row.is_current === true);
|
|
145
|
+
if (isCurrentRow) {
|
|
146
|
+
return isCurrentRow.id;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (seloSettings.lastProviderClaude) {
|
|
150
|
+
const lastRow = rows.find((row) => row.id === seloSettings.lastProviderClaude);
|
|
151
|
+
if (lastRow) {
|
|
152
|
+
return lastRow.id;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return rows[0].id;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
buildChildEnv,
|
|
161
|
+
getBaseUrl,
|
|
162
|
+
getPreferredModel,
|
|
163
|
+
getSubText,
|
|
164
|
+
mergeSettings,
|
|
165
|
+
normalizeProviderSettings,
|
|
166
|
+
parseJson,
|
|
167
|
+
parseProviderMeta,
|
|
168
|
+
parseProviderSettings,
|
|
169
|
+
resolveDefaultProviderId,
|
|
170
|
+
resolveEffectiveSettings,
|
|
171
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
const fs = require('node:fs/promises');
|
|
2
|
+
const os = require('node:os');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { execFileSync } = require('node:child_process');
|
|
5
|
+
|
|
6
|
+
const { parseJson } = require('../core/provider.js');
|
|
7
|
+
|
|
8
|
+
const SWITCH_DIR = path.join(os.homedir(), '.cc-switch');
|
|
9
|
+
const DB_PATH = path.join(SWITCH_DIR, 'cc-switch.db');
|
|
10
|
+
const SETTINGS_PATH = path.join(SWITCH_DIR, 'settings.json');
|
|
11
|
+
const PROVIDERS_SQL = [
|
|
12
|
+
'SELECT',
|
|
13
|
+
'id,',
|
|
14
|
+
'name,',
|
|
15
|
+
'notes,',
|
|
16
|
+
'settings_config,',
|
|
17
|
+
'meta,',
|
|
18
|
+
'is_current',
|
|
19
|
+
'FROM providers',
|
|
20
|
+
"WHERE app_type='claude'",
|
|
21
|
+
'ORDER BY created_at;',
|
|
22
|
+
].join(' ');
|
|
23
|
+
const COMMON_CLAUDE_CONFIG_SQL = [
|
|
24
|
+
'SELECT value',
|
|
25
|
+
'FROM settings',
|
|
26
|
+
"WHERE key='common_config_claude'",
|
|
27
|
+
'LIMIT 1;',
|
|
28
|
+
].join(' ');
|
|
29
|
+
|
|
30
|
+
function normalizeSqliteError(error, dbPath) {
|
|
31
|
+
if (error && error.code === 'ENOENT') {
|
|
32
|
+
return new Error('sqlite3 is required. Please install sqlite3 to let selo read CC Switch data.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const message = [
|
|
36
|
+
error && error.message,
|
|
37
|
+
error && error.stderr && String(error.stderr),
|
|
38
|
+
].filter(Boolean).join('\n');
|
|
39
|
+
|
|
40
|
+
if (
|
|
41
|
+
message.includes('unable to open database file')
|
|
42
|
+
|| message.includes('no such table')
|
|
43
|
+
|| message.includes('no such file')
|
|
44
|
+
) {
|
|
45
|
+
return new Error(`CC Switch DB not found at ${dbPath}. Please install CC Switch first.`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return error;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function queryJson(sql, { dbPath = DB_PATH, execFileSyncFn = execFileSync } = {}) {
|
|
52
|
+
let output;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
output = execFileSyncFn('sqlite3', ['-json', dbPath, sql], {
|
|
56
|
+
encoding: 'utf8',
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw normalizeSqliteError(error, dbPath);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return parseJson(output || '[]', `Invalid JSON returned from ${dbPath}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function loadProviders({ dbPath = DB_PATH, queryJsonFn = queryJson } = {}) {
|
|
66
|
+
const rows = queryJsonFn(PROVIDERS_SQL, { dbPath });
|
|
67
|
+
|
|
68
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
69
|
+
throw new Error('No Claude providers found in CC Switch.\nPlease configure providers in CC Switch first.');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return rows;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function loadProviderById(providerId, { dbPath = DB_PATH, queryJsonFn = queryJson } = {}) {
|
|
76
|
+
const safeId = String(providerId).replace(/'/g, "''");
|
|
77
|
+
const rows = queryJsonFn([
|
|
78
|
+
'SELECT',
|
|
79
|
+
'id,',
|
|
80
|
+
'name,',
|
|
81
|
+
'notes,',
|
|
82
|
+
'settings_config,',
|
|
83
|
+
'meta,',
|
|
84
|
+
'is_current',
|
|
85
|
+
'FROM providers',
|
|
86
|
+
"WHERE app_type='claude'",
|
|
87
|
+
`AND id='${safeId}'`,
|
|
88
|
+
'LIMIT 1;',
|
|
89
|
+
].join(' '), { dbPath });
|
|
90
|
+
|
|
91
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
92
|
+
throw new Error(`Selected provider "${providerId}" was removed from CC Switch. Please reopen selo and try again.`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return rows[0];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function loadCommonClaudeSettings({ dbPath = DB_PATH, queryJsonFn = queryJson } = {}) {
|
|
99
|
+
const rows = queryJsonFn(COMMON_CLAUDE_CONFIG_SQL, { dbPath });
|
|
100
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const [row] = rows;
|
|
105
|
+
if (!row || typeof row.value !== 'string' || row.value.trim() === '') {
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return parseJson(row.value, `Invalid common Claude config in ${dbPath}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function loadSwitchSettings({ settingsPath = SETTINGS_PATH, readFileFn = fs.readFile } = {}) {
|
|
113
|
+
try {
|
|
114
|
+
const text = await readFileFn(settingsPath, 'utf8');
|
|
115
|
+
return parseJson(text, `Invalid CC Switch settings file at ${settingsPath}`);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error && error.code === 'ENOENT') {
|
|
118
|
+
return {};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function getFingerprint({
|
|
126
|
+
dbPath = DB_PATH,
|
|
127
|
+
settingsPath = SETTINGS_PATH,
|
|
128
|
+
statFn = fs.stat,
|
|
129
|
+
} = {}) {
|
|
130
|
+
const [dbStats, settingsStats] = await Promise.all([
|
|
131
|
+
statFn(dbPath),
|
|
132
|
+
statFn(settingsPath).catch((error) => {
|
|
133
|
+
if (error && error.code === 'ENOENT') {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}),
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
dbMtimeMs: Number(dbStats.mtimeMs || 0),
|
|
142
|
+
dbSize: Number(dbStats.size || 0),
|
|
143
|
+
settingsMtimeMs: Number(settingsStats && settingsStats.mtimeMs || 0),
|
|
144
|
+
settingsSize: Number(settingsStats && settingsStats.size || 0),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function loadSnapshot({
|
|
149
|
+
loadProvidersFn = loadProviders,
|
|
150
|
+
loadCommonClaudeSettingsFn = loadCommonClaudeSettings,
|
|
151
|
+
loadSwitchSettingsFn = loadSwitchSettings,
|
|
152
|
+
getFingerprintFn = getFingerprint,
|
|
153
|
+
} = {}) {
|
|
154
|
+
const [providers, commonSettings, switchSettings, fingerprint] = await Promise.all([
|
|
155
|
+
Promise.resolve(loadProvidersFn()),
|
|
156
|
+
Promise.resolve(loadCommonClaudeSettingsFn()),
|
|
157
|
+
Promise.resolve(loadSwitchSettingsFn()),
|
|
158
|
+
Promise.resolve(getFingerprintFn()),
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
providers,
|
|
163
|
+
commonSettings,
|
|
164
|
+
switchSettings,
|
|
165
|
+
fingerprint,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
DB_PATH,
|
|
171
|
+
SETTINGS_PATH,
|
|
172
|
+
getFingerprint,
|
|
173
|
+
loadCommonClaudeSettings,
|
|
174
|
+
loadProviderById,
|
|
175
|
+
loadProviders,
|
|
176
|
+
loadSnapshot,
|
|
177
|
+
loadSwitchSettings,
|
|
178
|
+
normalizeSqliteError,
|
|
179
|
+
queryJson,
|
|
180
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const fs = require('node:fs/promises');
|
|
2
|
+
const os = require('node:os');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const { parseJson } = require('../core/provider.js');
|
|
6
|
+
|
|
7
|
+
function getConfigDir(platform = process.platform) {
|
|
8
|
+
if (platform === 'darwin') {
|
|
9
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'selo');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (platform === 'win32') {
|
|
13
|
+
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'selo');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'selo');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getSettingsPath(configDir = getConfigDir()) {
|
|
20
|
+
return path.join(configDir, 'settings.json');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function loadSeloSettings({ settingsPath = getSettingsPath(), readFileFn = fs.readFile } = {}) {
|
|
24
|
+
try {
|
|
25
|
+
const text = await readFileFn(settingsPath, 'utf8');
|
|
26
|
+
return parseJson(text, `Invalid selo settings file at ${settingsPath}`);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error && error.code === 'ENOENT') {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function saveSeloSettings(
|
|
37
|
+
settings,
|
|
38
|
+
{
|
|
39
|
+
settingsPath = getSettingsPath(),
|
|
40
|
+
mkdirFn = fs.mkdir,
|
|
41
|
+
writeFileFn = fs.writeFile,
|
|
42
|
+
} = {}
|
|
43
|
+
) {
|
|
44
|
+
await mkdirFn(path.dirname(settingsPath), { recursive: true });
|
|
45
|
+
await writeFileFn(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
getConfigDir,
|
|
50
|
+
getSettingsPath,
|
|
51
|
+
loadSeloSettings,
|
|
52
|
+
saveSeloSettings,
|
|
53
|
+
};
|