u-chan-secrets 5.0.1 → 5.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/README.md +233 -106
- package/dist/cli.js +114 -20
- package/dist/client/claude-setup.js +97 -0
- package/dist/client/codex-setup.js +6 -5
- package/dist/client/discord.js +67 -0
- package/dist/client/github.js +28 -0
- package/dist/client/setup.js +31 -2
- package/dist/client/update-package.js +166 -0
- package/dist/client/update.js +138 -0
- package/dist/core/browser.js +0 -5
- package/dist/core/config.js +1 -0
- package/dist/core/doctor.js +5 -1
- package/dist/core/vault.js +110 -5
- package/dist/discord/http.js +84 -0
- package/dist/discord/policy.js +47 -0
- package/dist/discord/service.js +349 -0
- package/dist/discord/store.js +85 -0
- package/dist/github/http.js +93 -0
- package/dist/github/policy.js +24 -0
- package/dist/github/service.js +185 -0
- package/dist/github/store.js +54 -0
- package/dist/index.js +1 -1
- package/dist/ipc/client.js +74 -6
- package/dist/ipc/server.js +38 -3
- package/dist/mcp/server.js +25 -2
- package/dist/platform/windows/assets/prompt-src.js +160 -38
- package/dist/platform/windows/doctor.js +2 -2
- package/dist/service/control.js +47 -5
- package/dist/service/main.js +34 -17
- package/dist/service/manage.js +20 -16
- package/dist/service/state.js +28 -0
- package/dist/service/update/coordinator.js +171 -0
- package/dist/service/update/files.js +47 -0
- package/dist/service/update/gate.js +21 -0
- package/dist/service/update/journal.js +157 -0
- package/dist/service/update/native.js +50 -0
- package/dist/service/update/network.js +28 -0
- package/dist/service/update/protocol.js +28 -0
- package/dist/service/update/release.js +102 -0
- package/dist/service/update/worker-main.js +116 -0
- package/dist/service/update/worker.js +58 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7,16 +7,27 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import { IpcClient } from './ipc/client.js';
|
|
8
8
|
import { defaultEndpoint } from './ipc/endpoint.js';
|
|
9
9
|
import { MAX_FRAME, encode } from './ipc/protocol.js';
|
|
10
|
-
import { runDoctor, doctorExitCode, checkPlaywrightNow, chromeState, safeVersion, SERVICE_DIAGNOSTIC_TIMEOUT_MS } from './core/doctor.js';
|
|
10
|
+
import { runDoctor, doctorExitCode, bounded, checkPlaywrightNow, chromeState, safeVersion, SERVICE_DIAGNOSTIC_TIMEOUT_MS } from './core/doctor.js';
|
|
11
11
|
import { loadConfig } from './core/config.js';
|
|
12
12
|
import { execFile } from 'node:child_process';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { configureCodex, isElevated } from './client/codex-setup.js';
|
|
15
|
-
import {
|
|
15
|
+
import { configureClaude } from './client/claude-setup.js';
|
|
16
|
+
import { combineClients, runSetup, setupExitCode, SETUP_CLIENT_NAMES } from './client/setup.js';
|
|
16
17
|
import { parseAllowArgs } from './client/allow.js';
|
|
17
18
|
import { saveBrowserChoice } from './client/browser-choice.js';
|
|
18
19
|
import { configureWindowsCommand } from './client/windows-command.js';
|
|
19
|
-
|
|
20
|
+
import { runDiscordCli } from './client/discord.js';
|
|
21
|
+
import { runCoordinatedUpdate } from './client/update.js';
|
|
22
|
+
import { desktopUpdateAdapter } from './client/update-package.js';
|
|
23
|
+
import { runGithubCli } from './client/github.js';
|
|
24
|
+
const USAGE = 'usage: u-chan start | install [--mode <easy|hardcore>] [--phrase <phrase>] | uninstall | unlock | lock | mode <easy|hardcore> | import-v1 [path] | audit [limit] | update | doctor [--json] | manage';
|
|
25
|
+
/** AI client CLIs on PATH. Detection only runs `where`/`which`; it never executes the clients themselves. */
|
|
26
|
+
export function detectClients(platform = process.platform) {
|
|
27
|
+
const finder = platform === 'win32' ? 'where.exe' : 'which';
|
|
28
|
+
const found = (name) => new Promise(resolve => execFile(finder, [name], { timeout: 5000, windowsHide: true, maxBuffer: 64 * 1024 }, err => resolve(!err)));
|
|
29
|
+
return Promise.all(['codex', 'claude'].map(async (c) => (await found(c)) ? [c] : [])).then(r => r.flat());
|
|
30
|
+
}
|
|
20
31
|
/** import-v1 shows one approval window per entry, each open for up to 2 minutes, one after another. */
|
|
21
32
|
export const IMPORT_PROMPT_MS = 120000;
|
|
22
33
|
export const IMPORT_MARGIN_MS = 60000;
|
|
@@ -28,6 +39,35 @@ function defaultV1Path(env, platform) {
|
|
|
28
39
|
}
|
|
29
40
|
/** Returns the process exit code: 0 on success, 1 on failure or bad usage. */
|
|
30
41
|
export async function main(argv, d) {
|
|
42
|
+
if (argv[0] === 'github') {
|
|
43
|
+
const result = await runGithubCli(argv.slice(1), d.connect({ timeoutMs: 240000 }), d.readFile);
|
|
44
|
+
if (result.ok)
|
|
45
|
+
d.out(result.message);
|
|
46
|
+
else
|
|
47
|
+
d.err(result.message);
|
|
48
|
+
return result.ok ? 0 : 1;
|
|
49
|
+
}
|
|
50
|
+
if (argv[0] === 'update') {
|
|
51
|
+
if (argv.length > 2 || (argv.length === 2 && argv[1] !== '--check')) {
|
|
52
|
+
d.err('usage: u-chan update [--check]');
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
const checkOnly = argv[1] === '--check';
|
|
56
|
+
const result = d.update ? await d.update(checkOnly) : await runCoordinatedUpdate({ checkOnly }, { client: d.connect({ timeoutMs: 180000 }), desktop: desktopUpdateAdapter(d.env), progress: (phase, seconds) => d.out(`Update: ${phase} (${seconds}s)`) });
|
|
57
|
+
if (result.ok)
|
|
58
|
+
d.out(result.message);
|
|
59
|
+
else
|
|
60
|
+
d.err(result.message);
|
|
61
|
+
return result.ok ? 0 : 1;
|
|
62
|
+
}
|
|
63
|
+
if (argv[0] === 'discord') {
|
|
64
|
+
const result = await runDiscordCli(argv.slice(1), d.connect({ timeoutMs: 240000 }), d.readFile);
|
|
65
|
+
if (result.ok)
|
|
66
|
+
d.out(result.message);
|
|
67
|
+
else
|
|
68
|
+
d.err(result.message);
|
|
69
|
+
return result.ok ? 0 : 1;
|
|
70
|
+
}
|
|
31
71
|
const [cmd, arg, ...rest] = argv;
|
|
32
72
|
let call;
|
|
33
73
|
let diagnosticEnv = d.env;
|
|
@@ -59,6 +99,7 @@ export async function main(argv, d) {
|
|
|
59
99
|
return state === 'ok' ? 'pass' : state === 'missing' ? 'fail' : 'unknown';
|
|
60
100
|
},
|
|
61
101
|
}));
|
|
102
|
+
const startBrowser = d.startBrowser ?? launchBrowser;
|
|
62
103
|
const printDoctor = (r) => d.out(r.checks.map(c => `${c.state.toUpperCase()} ${c.id}: ${c.message}${c.nextAction ? ' ' + c.nextAction : ''}`).join('\n'));
|
|
63
104
|
if (cmd === 'allow') {
|
|
64
105
|
const proposal = parseAllowArgs(argv.slice(1));
|
|
@@ -80,6 +121,7 @@ export async function main(argv, d) {
|
|
|
80
121
|
const args = argv.slice(1), seen = new Set();
|
|
81
122
|
let valid = true;
|
|
82
123
|
let browser;
|
|
124
|
+
let client = 'auto';
|
|
83
125
|
for (let i = 0; i < args.length; i++) {
|
|
84
126
|
const flag = args[i];
|
|
85
127
|
if (seen.has(flag) || !['--client', '--browser', '--apply', '--replace-existing', '--start-browser', '--json'].includes(flag)) {
|
|
@@ -87,9 +129,14 @@ export async function main(argv, d) {
|
|
|
87
129
|
break;
|
|
88
130
|
}
|
|
89
131
|
seen.add(flag);
|
|
90
|
-
if (flag === '--client'
|
|
91
|
-
|
|
92
|
-
|
|
132
|
+
if (flag === '--client') {
|
|
133
|
+
const value = args[++i];
|
|
134
|
+
if (value !== 'codex' && value !== 'claude' && value !== 'all') {
|
|
135
|
+
valid = false;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
;
|
|
139
|
+
client = value;
|
|
93
140
|
}
|
|
94
141
|
if (flag === '--browser') {
|
|
95
142
|
const value = args[++i];
|
|
@@ -102,28 +149,73 @@ export async function main(argv, d) {
|
|
|
102
149
|
}
|
|
103
150
|
}
|
|
104
151
|
if (!valid || (seen.has('--replace-existing') && !seen.has('--apply'))) {
|
|
105
|
-
d.err('usage: u-chan setup [--client codex] [--browser chrome|edge] [--apply] [--replace-existing] [--start-browser] [--json]');
|
|
152
|
+
d.err('usage: u-chan setup [--client codex|claude|all] [--browser chrome|edge] [--apply] [--replace-existing] [--start-browser] [--json]');
|
|
106
153
|
return 1;
|
|
107
154
|
}
|
|
108
155
|
const options = { apply: seen.has('--apply'), replaceExisting: seen.has('--replace-existing'), startBrowser: seen.has('--start-browser'), ...(browser ? { browser } : {}) };
|
|
109
156
|
if (browser)
|
|
110
157
|
diagnosticEnv = { ...d.env, U_CHAN_BROWSER: browser };
|
|
111
158
|
const cfg = loadConfig(diagnosticEnv, d.platform), endpoint = cfg.cdpEndpoint;
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
159
|
+
const entryPath = fileURLToPath(new URL('./index.js', import.meta.url)), claudeDir = d.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude');
|
|
160
|
+
const configureFor = (c) => (opts) => c === 'claude'
|
|
161
|
+
? configureClaude({ ...opts, browser: cfg.browser, endpoint, ipcEndpoint: defaultEndpoint(d.env, d.platform), nodePath: process.execPath, entryPath, configDir: claudeDir, configPath: d.env.CLAUDE_CONFIG_DIR ? join(claudeDir, '.claude.json') : join(homedir(), '.claude.json') })
|
|
162
|
+
: configureCodex({ ...opts, browser: cfg.browser, endpoint, ipcEndpoint: defaultEndpoint(d.env, d.platform), nodePath: process.execPath, entryPath, configDir: d.env.CODEX_HOME ?? join(homedir(), '.codex') });
|
|
163
|
+
let r;
|
|
164
|
+
if (d.setup)
|
|
165
|
+
r = await d.setup(options, client);
|
|
166
|
+
else {
|
|
167
|
+
const clients = client === 'all' ? ['codex', 'claude'] : client === 'auto' ? await (d.detectClients ?? detectClients)() : [client];
|
|
168
|
+
const combined = combineClients(clients.map(c => ({ client: c, configure: configureFor(c) })));
|
|
169
|
+
r = await runSetup(options, {
|
|
170
|
+
platform: d.platform, endpoint, browser: cfg.browser, elevated: isElevated, doctor, clientName: combined.clientName,
|
|
171
|
+
configureShell: apply => configureWindowsCommand(apply, d.env.PATH ?? d.env.Path ?? ''),
|
|
172
|
+
selectBrowser: choice => saveBrowserChoice(cfg.browserChoicePath, choice),
|
|
173
|
+
configure: combined.configure,
|
|
174
|
+
startBrowser: () => startBrowser(cfg.browser, diagnosticEnv, true),
|
|
175
|
+
});
|
|
176
|
+
if (clients.length > 1 && combined.results().length)
|
|
177
|
+
r.clients = combined.results();
|
|
178
|
+
}
|
|
121
179
|
if (seen.has('--json'))
|
|
122
180
|
d.out(JSON.stringify(r));
|
|
123
181
|
else
|
|
124
|
-
d.out([`Setup: ${r.state}; sharing: ${r.sharing}; restart required: ${r.restartRequired}`, ...r.actions.map(a => a.message), ...(r.backupPath ? ['Configuration backup: ' + r.backupPath] : [])].join('\n'));
|
|
182
|
+
d.out([`Setup: ${r.state}; sharing: ${r.sharing}; restart required: ${r.restartRequired}`, ...(r.clients ?? []).map(c => `${SETUP_CLIENT_NAMES[c.client]}: ${c.state}. ${c.message}`), ...r.actions.map(a => a.message), ...(r.backupPath ? ['Configuration backup: ' + r.backupPath] : [])].join('\n'));
|
|
125
183
|
return setupExitCode(r);
|
|
126
184
|
}
|
|
185
|
+
if (cmd === 'start' && arg === undefined) {
|
|
186
|
+
// Daily routine: dedicated browser, vault unlock through the protected prompt, then a doctor summary.
|
|
187
|
+
if (d.platform !== 'win32') {
|
|
188
|
+
d.err('u-chan start supports Windows only; start the browser and run u-chan unlock manually.');
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
191
|
+
const cfg = loadConfig(d.env, d.platform);
|
|
192
|
+
if (await chromeState(cfg.cdpEndpoint) === 'pass')
|
|
193
|
+
d.out('Browser: ready.');
|
|
194
|
+
else if (await bounded(() => startBrowser(cfg.browser, d.env, false), false, 25000))
|
|
195
|
+
d.out('Browser: started.');
|
|
196
|
+
else
|
|
197
|
+
d.err('Browser: could not start the dedicated browser; run u-chan doctor.');
|
|
198
|
+
let vault;
|
|
199
|
+
try {
|
|
200
|
+
const r = await d.connect({ timeoutMs: SERVICE_DIAGNOSTIC_TIMEOUT_MS }).diagnose();
|
|
201
|
+
vault = r.ok ? JSON.parse(r.message)?.vault : undefined;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
vault = undefined;
|
|
205
|
+
}
|
|
206
|
+
if (vault === 'unlocked')
|
|
207
|
+
d.out('Vault: unlocked.');
|
|
208
|
+
else if (vault === 'locked') {
|
|
209
|
+
const r = await d.connect({}).unlock();
|
|
210
|
+
if (r.ok)
|
|
211
|
+
d.out('Vault: ' + r.message);
|
|
212
|
+
else
|
|
213
|
+
d.err('Vault: ' + r.message);
|
|
214
|
+
}
|
|
215
|
+
const report = await doctor();
|
|
216
|
+
printDoctor(report);
|
|
217
|
+
return doctorExitCode(report) === 1 || report.checks.some(c => c.id === 'vault' && c.nextAction === 'Run u-chan unlock.') ? 1 : 0;
|
|
218
|
+
}
|
|
127
219
|
if (cmd === 'doctor' && (arg === undefined || arg === '--json') && rest.length === 0) {
|
|
128
220
|
const r = await doctor();
|
|
129
221
|
if (arg === '--json')
|
|
@@ -235,9 +327,6 @@ export async function main(argv, d) {
|
|
|
235
327
|
}
|
|
236
328
|
call = () => d.connect({}).audit(limit);
|
|
237
329
|
}
|
|
238
|
-
else if (cmd === 'update' && arg === undefined) {
|
|
239
|
-
call = () => d.connect({ timeoutMs: 120000 }).update();
|
|
240
|
-
}
|
|
241
330
|
else if (cmd === 'manage' && arg === undefined) {
|
|
242
331
|
call = () => d.connect({ timeoutMs: 15 * 60_000 }).manage();
|
|
243
332
|
}
|
|
@@ -254,6 +343,11 @@ export async function main(argv, d) {
|
|
|
254
343
|
printDoctor(await doctor());
|
|
255
344
|
return r.ok ? 0 : 1;
|
|
256
345
|
}
|
|
346
|
+
function launchBrowser(browser, env, background) {
|
|
347
|
+
return new Promise(resolve => {
|
|
348
|
+
execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', fileURLToPath(new URL('../scripts/start-browser.ps1', import.meta.url)), '-Browser', browser, ...(background ? ['-Background'] : [])], { timeout: 24000, windowsHide: true, env, maxBuffer: 65536 }, err => resolve(!err));
|
|
349
|
+
});
|
|
350
|
+
}
|
|
257
351
|
const isEntry = (() => {
|
|
258
352
|
try {
|
|
259
353
|
return !!process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import { safeCdpOrigin } from '../core/doctor.js';
|
|
5
|
+
import { defaultEndpoint } from '../ipc/endpoint.js';
|
|
6
|
+
import { browserKind } from './browser-choice.js';
|
|
7
|
+
import { backupFile, isElevated } from './codex-setup.js';
|
|
8
|
+
function simple(entry, name) {
|
|
9
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
10
|
+
return false;
|
|
11
|
+
if (Object.keys(entry).some(k => !['type', 'command', 'args', 'env'].includes(k)) || (entry.type ?? 'stdio') !== 'stdio')
|
|
12
|
+
return false;
|
|
13
|
+
if (typeof entry.command !== 'string' || !Array.isArray(entry.args) || !entry.args.every((a) => typeof a === 'string'))
|
|
14
|
+
return false;
|
|
15
|
+
if (entry.env != null && (typeof entry.env !== 'object' || Array.isArray(entry.env) || Object.entries(entry.env).some(([key, value]) => name !== 'u-chan-secrets' || (key === 'U_CHAN_BROWSER' ? value !== 'chrome' && value !== 'edge' : key === 'U_CHAN_CDP' ? !safeCdpOrigin(value) : true))))
|
|
16
|
+
return false;
|
|
17
|
+
const command = basename(entry.command.replaceAll('\\', '/')).toLowerCase();
|
|
18
|
+
if (name === 'u-chan-secrets')
|
|
19
|
+
return ['node', 'node.exe'].includes(command) && entry.args.length === 1 && /(?:^|[\\/])index\.js$/.test(entry.args[0]);
|
|
20
|
+
return ['npx', 'npx.cmd', 'npx.exe'].includes(command) && /^@playwright\/mcp(?:@[^\s]+)?$/.test(entry.args[0] ?? '')
|
|
21
|
+
&& (entry.args.length === 1 || (entry.args.length === 3 && entry.args[1] === '--cdp-endpoint' && !!safeCdpOrigin(entry.args[2])));
|
|
22
|
+
}
|
|
23
|
+
/** Inspect Claude Code's user-scope JSON read-only; every change goes through the official `claude mcp` CLI. */
|
|
24
|
+
export async function configureClaude(o, injected) {
|
|
25
|
+
let changed = false, backupPath;
|
|
26
|
+
const result = (state, message) => ({ state, changed, ...(backupPath ? { backupPath } : {}), message });
|
|
27
|
+
const d = injected ?? { elevated: isElevated, read: path => readFile(path, 'utf8'), exec: args => new Promise(resolve => {
|
|
28
|
+
execFile('claude', args, { timeout: 30000, windowsHide: true, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => resolve({ code: err ? 1 : 0, stdout, stderr }));
|
|
29
|
+
}) };
|
|
30
|
+
try {
|
|
31
|
+
const origin = safeCdpOrigin(o.endpoint);
|
|
32
|
+
if (!origin || (o.replaceExisting && !o.apply))
|
|
33
|
+
return result('manual', 'Use a safe loopback browser target and explicit apply flags.');
|
|
34
|
+
if (o.browser !== undefined)
|
|
35
|
+
browserKind(o.browser);
|
|
36
|
+
if (await d.elevated())
|
|
37
|
+
return result('unavailable', 'Run setup from a non-administrator desktop terminal.');
|
|
38
|
+
let servers = {};
|
|
39
|
+
try {
|
|
40
|
+
const text = await d.read(o.configPath);
|
|
41
|
+
const parsed = JSON.parse(text.charCodeAt(0) === 0xfeff ? text.slice(1) : text);
|
|
42
|
+
if (!parsed || typeof parsed !== 'object')
|
|
43
|
+
return result('unavailable', 'Could not inspect Claude Code registrations.');
|
|
44
|
+
servers = parsed.mcpServers && typeof parsed.mcpServers === 'object' ? parsed.mcpServers : {};
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (err.code !== 'ENOENT')
|
|
48
|
+
return result('unavailable', 'Could not inspect Claude Code registrations.');
|
|
49
|
+
}
|
|
50
|
+
const env = o.browser ? { U_CHAN_BROWSER: o.browser, U_CHAN_CDP: origin } : {};
|
|
51
|
+
const desired = [
|
|
52
|
+
{ name: 'u-chan-secrets', server: { type: 'stdio', command: o.nodePath, args: [o.entryPath], env } },
|
|
53
|
+
{ name: 'playwright', server: { type: 'stdio', command: 'npx', args: ['@playwright/mcp@latest', '--cdp-endpoint', origin], env: {} } },
|
|
54
|
+
];
|
|
55
|
+
const ipc = o.ipcEndpoint ?? defaultEndpoint(process.env, process.platform);
|
|
56
|
+
const pending = [];
|
|
57
|
+
for (const { name, server } of desired) {
|
|
58
|
+
const current = servers[name];
|
|
59
|
+
if (current === undefined) {
|
|
60
|
+
pending.push({ name, server, replace: false });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const curEnv = current?.env && typeof current.env === 'object' ? current.env : {};
|
|
64
|
+
if (Object.keys(curEnv).some(key => {
|
|
65
|
+
const k = key.toUpperCase();
|
|
66
|
+
return k === 'U_CHAN_CDP' ? safeCdpOrigin(curEnv[key]) !== origin : k === 'U_CHAN_IPC' && curEnv[key] !== ipc;
|
|
67
|
+
}))
|
|
68
|
+
return result('manual', 'Custom target environment preserved; align browser and service targets manually.');
|
|
69
|
+
if ((current?.type ?? 'stdio') === 'stdio' && current?.command === server.command && JSON.stringify(current.args) === JSON.stringify(server.args)
|
|
70
|
+
&& (name !== 'u-chan-secrets' || o.browser === undefined || (curEnv.U_CHAN_BROWSER === o.browser && safeCdpOrigin(curEnv.U_CHAN_CDP) === origin)))
|
|
71
|
+
continue;
|
|
72
|
+
if (!simple(current, name))
|
|
73
|
+
return result('manual', 'Custom Claude Code settings preserved; configure this registration manually.');
|
|
74
|
+
pending.push({ name, server, replace: true });
|
|
75
|
+
}
|
|
76
|
+
if (pending.length === 0)
|
|
77
|
+
return result('ready', 'Claude Code registrations match; active session ownership is unverified.');
|
|
78
|
+
const replaces = pending.some(p => p.replace);
|
|
79
|
+
if (!o.apply || (replaces && !o.replaceExisting))
|
|
80
|
+
return result(replaces ? 'needs_update' : 'missing', 'Explicit setup apply is required; replacements also require --replace-existing.');
|
|
81
|
+
backupPath = await backupFile(o.configPath, join(o.configDir, '#0 backup'), '.json');
|
|
82
|
+
for (const { name, server, replace } of pending) {
|
|
83
|
+
if (replace) {
|
|
84
|
+
if ((await d.exec(['mcp', 'remove', '--scope', 'user', name])).code !== 0)
|
|
85
|
+
return result('unavailable', 'Claude Code change failed; inspect configuration before retrying.');
|
|
86
|
+
changed = true;
|
|
87
|
+
}
|
|
88
|
+
if ((await d.exec(['mcp', 'add-json', '--scope', 'user', name, JSON.stringify(server)])).code !== 0)
|
|
89
|
+
return result('unavailable', 'Claude Code change failed; completed changes and backup were retained.');
|
|
90
|
+
changed = true;
|
|
91
|
+
}
|
|
92
|
+
return result('ready', 'Claude Code registrations saved. Restart the AI session; active ownership remains unverified.');
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return result('unavailable', 'Claude Code setup unavailable; inspect configuration and any retained backup before retrying.');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -43,7 +43,10 @@ async function backup(configDir) {
|
|
|
43
43
|
const info = await lstat(root);
|
|
44
44
|
if (!info.isDirectory() || info.isSymbolicLink() || (await realpath(root)).toLowerCase() !== root.toLowerCase())
|
|
45
45
|
throw new Error('redirected config');
|
|
46
|
-
|
|
46
|
+
return backupFile(join(root, 'config.toml'), join(root, '#0 backup'), '.toml');
|
|
47
|
+
}
|
|
48
|
+
/** Copy a regular config file into a non-redirected backup directory; undefined when the file does not exist. */
|
|
49
|
+
export async function backupFile(config, dir, ext) {
|
|
47
50
|
try {
|
|
48
51
|
const file = await lstat(config);
|
|
49
52
|
if (!file.isFile() || file.isSymbolicLink())
|
|
@@ -54,13 +57,11 @@ async function backup(configDir) {
|
|
|
54
57
|
return undefined;
|
|
55
58
|
throw err;
|
|
56
59
|
}
|
|
57
|
-
|
|
58
|
-
await mkdir(dir, { mode: 0o700 }).catch(err => { if (err.code !== 'EEXIST')
|
|
59
|
-
throw err; });
|
|
60
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
60
61
|
const target = await lstat(dir);
|
|
61
62
|
if (!target.isDirectory() || target.isSymbolicLink() || (await realpath(dir)).toLowerCase() !== dir.toLowerCase())
|
|
62
63
|
throw new Error('redirected backup');
|
|
63
|
-
const path = join(dir, `u-chan-setup-${randomUUID()}
|
|
64
|
+
const path = join(dir, `u-chan-setup-${randomUUID()}${ext}`);
|
|
64
65
|
await copyFile(config, path, constants.COPYFILE_EXCL);
|
|
65
66
|
await chmod(path, 0o600);
|
|
66
67
|
return path;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { aliasSchema, batchSchema, connectionRequestSchema, snowflake } from '../discord/policy.js';
|
|
3
|
+
const connectionResult = z.object({ guild: z.object({ id: snowflake }), totals: z.object({ channels: z.number().int().min(0).max(1000), roles: z.number().int().min(0).max(1000) }) });
|
|
4
|
+
/** Reuse approved metadata inspection; return only a bounded health summary, never raw metadata. */
|
|
5
|
+
export async function testDiscordConnection(client, alias) {
|
|
6
|
+
if (!aliasSchema.safeParse(alias).success)
|
|
7
|
+
return { ok: false, message: 'invalid Discord alias' };
|
|
8
|
+
if (!client.discordInspect)
|
|
9
|
+
return { ok: false, message: 'Discord broker unavailable; update the local client' };
|
|
10
|
+
let result;
|
|
11
|
+
try {
|
|
12
|
+
result = await client.discordInspect(alias);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return { ok: false, message: 'Discord connection test unavailable; check service and vault status' };
|
|
16
|
+
}
|
|
17
|
+
if (!result.ok)
|
|
18
|
+
return result;
|
|
19
|
+
try {
|
|
20
|
+
const data = connectionResult.parse(JSON.parse(result.message));
|
|
21
|
+
return { ok: true, message: JSON.stringify({ connection: alias, status: 'connected', readOnly: true, serverId: data.guild.id, channels: data.totals.channels, roles: data.totals.roles }) };
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return { ok: false, message: 'Discord connection test received an invalid service response' };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export const DISCORD_USAGE = 'usage: u-chan discord allow <connection.json> | list | test <alias> | inspect <alias> [member-id] [--page <0..999>] | apply <alias> <operations.json> | revoke <alias>';
|
|
28
|
+
export async function runDiscordCli(args, client, read) {
|
|
29
|
+
const [cmd, arg, third] = args;
|
|
30
|
+
const invalid = { ok: false, message: DISCORD_USAGE };
|
|
31
|
+
try {
|
|
32
|
+
if (cmd === 'test' && args.length === 2)
|
|
33
|
+
return await testDiscordConnection(client, arg);
|
|
34
|
+
if (cmd === 'list' && args.length === 1)
|
|
35
|
+
return await client.discordListAllowed?.() ?? invalid;
|
|
36
|
+
if (cmd === 'revoke' && args.length === 2 && aliasSchema.safeParse(arg).success)
|
|
37
|
+
return await client.discordRevoke?.(arg) ?? invalid;
|
|
38
|
+
if (cmd === 'inspect' && aliasSchema.safeParse(arg).success) {
|
|
39
|
+
let member, page = 0;
|
|
40
|
+
const extra = args.slice(2);
|
|
41
|
+
if (extra[0] && extra[0] !== '--page') {
|
|
42
|
+
member = extra.shift();
|
|
43
|
+
if (!snowflake.safeParse(member).success)
|
|
44
|
+
return invalid;
|
|
45
|
+
}
|
|
46
|
+
if (extra.length) {
|
|
47
|
+
if (extra.length !== 2 || extra[0] !== '--page' || !/^(0|[1-9][0-9]{0,2})$/.test(extra[1]))
|
|
48
|
+
return invalid;
|
|
49
|
+
page = Number(extra[1]);
|
|
50
|
+
}
|
|
51
|
+
return await client.discordInspect?.(arg, member, page) ?? invalid;
|
|
52
|
+
}
|
|
53
|
+
if ((cmd === 'allow' && args.length === 2) || (cmd === 'apply' && args.length === 3 && aliasSchema.safeParse(arg).success)) {
|
|
54
|
+
const raw = await read(cmd === 'allow' ? arg : third);
|
|
55
|
+
if (Buffer.byteLength(raw) > 16384)
|
|
56
|
+
return { ok: false, message: 'Discord input file is too large' };
|
|
57
|
+
const data = JSON.parse(raw);
|
|
58
|
+
if (cmd === 'allow')
|
|
59
|
+
return connectionRequestSchema.safeParse(data).success ? await client.discordRequestAllow?.(data) ?? invalid : invalid;
|
|
60
|
+
return batchSchema.safeParse(data).success ? await client.discordApply?.(arg, data) ?? invalid : invalid;
|
|
61
|
+
}
|
|
62
|
+
return invalid;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return { ok: false, message: 'Discord command failed; check input JSON and service availability' };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { aliasSchema, githubProposalSchema, githubReadSchema, githubCreateSchema } from '../github/policy.js';
|
|
2
|
+
export const GITHUB_USAGE = 'usage: u-chan github allow <connection.json> | list | test <alias> | read <alias> <request.json> | create <alias> <operation.json> | revoke <alias>';
|
|
3
|
+
export async function runGithubCli(args, client, read) {
|
|
4
|
+
const [command, alias, file] = args, invalid = { ok: false, message: GITHUB_USAGE };
|
|
5
|
+
try {
|
|
6
|
+
if (command === 'list' && args.length === 1)
|
|
7
|
+
return await client.githubListAllowed?.() ?? invalid;
|
|
8
|
+
if (command === 'revoke' && args.length === 2 && aliasSchema.safeParse(alias).success)
|
|
9
|
+
return await client.githubRevoke?.(alias) ?? invalid;
|
|
10
|
+
if (command === 'test' && args.length === 2 && aliasSchema.safeParse(alias).success)
|
|
11
|
+
return await client.githubRead?.(alias, { resource: 'repository' }) ?? invalid;
|
|
12
|
+
if ((command === 'allow' && args.length === 2) || (['read', 'create'].includes(command) && args.length === 3 && aliasSchema.safeParse(alias).success)) {
|
|
13
|
+
const raw = await read(command === 'allow' ? alias : file);
|
|
14
|
+
if (Buffer.byteLength(raw) > 16384)
|
|
15
|
+
return { ok: false, message: 'GitHub input file is too large' };
|
|
16
|
+
const data = JSON.parse(raw);
|
|
17
|
+
if (command === 'allow')
|
|
18
|
+
return githubProposalSchema.safeParse(data).success ? await client.githubRequestAllow?.(data) ?? invalid : invalid;
|
|
19
|
+
if (command === 'read')
|
|
20
|
+
return githubReadSchema.safeParse(data).success ? await client.githubRead?.(alias, data) ?? invalid : invalid;
|
|
21
|
+
return githubCreateSchema.safeParse(data).success ? await client.githubCreate?.(alias, data) ?? invalid : invalid;
|
|
22
|
+
}
|
|
23
|
+
return invalid;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { ok: false, message: 'GitHub command failed; check JSON input and service availability' };
|
|
27
|
+
}
|
|
28
|
+
}
|
package/dist/client/setup.js
CHANGED
|
@@ -2,6 +2,7 @@ import { bounded, safeCdpOrigin } from '../core/doctor.js';
|
|
|
2
2
|
export async function runSetup(o, d) {
|
|
3
3
|
const r = { version: 1, state: 'needs_attention', sharing: 'unobserved', restartRequired: false, checks: [], actions: [], client: 'unavailable', changed: false };
|
|
4
4
|
const action = (id, message, required = true) => r.actions.push({ id, message, required });
|
|
5
|
+
const clientName = d.clientName ?? 'Codex';
|
|
5
6
|
if (d.browser || o.browser)
|
|
6
7
|
r.browser = o.browser ?? d.browser;
|
|
7
8
|
try {
|
|
@@ -15,7 +16,7 @@ export async function runSetup(o, d) {
|
|
|
15
16
|
}
|
|
16
17
|
r.checks = (await d.doctor()).checks;
|
|
17
18
|
if (d.platform !== 'win32') {
|
|
18
|
-
action('configure_mcp',
|
|
19
|
+
action('configure_mcp', `Automatic ${clientName} setup supports Windows only; configure other platforms manually.`);
|
|
19
20
|
r.client = 'manual';
|
|
20
21
|
return r;
|
|
21
22
|
}
|
|
@@ -63,7 +64,7 @@ export async function runSetup(o, d) {
|
|
|
63
64
|
action(id, c.nextAction, c.required && c.state !== 'pass');
|
|
64
65
|
}
|
|
65
66
|
if (r.client !== 'ready')
|
|
66
|
-
action('configure_mcp',
|
|
67
|
+
action('configure_mcp', `Inspect ${clientName} configuration; apply missing registrations explicitly and preserve custom settings.`);
|
|
67
68
|
if (!started)
|
|
68
69
|
action('start_browser', 'Selected dedicated browser is unavailable or mismatched. Close only the previous dedicated U-Chan instance before switching, then run setup again.');
|
|
69
70
|
if (r.restartRequired)
|
|
@@ -82,3 +83,31 @@ export async function runSetup(o, d) {
|
|
|
82
83
|
export function setupExitCode(r) {
|
|
83
84
|
return r.state === 'needs_attention' ? 1 : r.restartRequired ? 2 : 0;
|
|
84
85
|
}
|
|
86
|
+
export const SETUP_CLIENT_NAMES = { codex: 'Codex', claude: 'Claude Code' };
|
|
87
|
+
const STATE_RANK = ['ready', 'missing', 'needs_update', 'manual', 'unavailable'];
|
|
88
|
+
/** Configure several AI clients in order; the combined state is the least ready one, and every client is attempted. */
|
|
89
|
+
export function combineClients(entries) {
|
|
90
|
+
let last = [];
|
|
91
|
+
return {
|
|
92
|
+
clientName: entries.length ? entries.map(e => SETUP_CLIENT_NAMES[e.client]).join(' and ') : 'Codex or Claude Code',
|
|
93
|
+
results: () => last,
|
|
94
|
+
configure: async (options) => {
|
|
95
|
+
last = [];
|
|
96
|
+
if (entries.length === 0)
|
|
97
|
+
return { state: 'unavailable', changed: false, message: 'No Codex or Claude Code CLI found on PATH; install one or pass --client.' };
|
|
98
|
+
for (const e of entries) {
|
|
99
|
+
let r;
|
|
100
|
+
try {
|
|
101
|
+
r = await e.configure(options);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
r = { state: 'unavailable', changed: false, message: `${SETUP_CLIENT_NAMES[e.client]} setup unavailable.` };
|
|
105
|
+
}
|
|
106
|
+
last.push({ client: e.client, ...r });
|
|
107
|
+
}
|
|
108
|
+
const state = last.reduce((w, r) => STATE_RANK.indexOf(r.state) > STATE_RANK.indexOf(w) ? r.state : w, 'ready');
|
|
109
|
+
const backups = last.flatMap(r => r.backupPath ? [r.backupPath] : []);
|
|
110
|
+
return { state, changed: last.some(r => r.changed), ...(backups.length ? { backupPath: backups.join('; ') } : {}), message: last.map(r => `${SETUP_CLIENT_NAMES[r.client]}: ${r.message}`).join(' ') };
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|