u-chan-secrets 5.0.0 → 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 -91
- package/dist/cli.js +115 -19
- 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 +40 -6
- package/dist/client/update-package.js +166 -0
- package/dist/client/update.js +138 -0
- package/dist/client/windows-command.js +54 -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/platform/windows/install.js +4 -0
- package/dist/service/control.js +47 -5
- package/dist/service/doctor.js +3 -0
- 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/scripts/debug-install.bat +69 -69
- package/scripts/desktop-browser.ps1 +110 -103
- package/scripts/login-bw.bat +21 -21
- package/scripts/migrate.bat +4 -4
- package/scripts/start-browser.ps1 +33 -33
- package/scripts/windows-security/desktop-matrix.mjs +54 -54
package/dist/cli.js
CHANGED
|
@@ -7,15 +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';
|
|
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
|
+
}
|
|
19
31
|
/** import-v1 shows one approval window per entry, each open for up to 2 minutes, one after another. */
|
|
20
32
|
export const IMPORT_PROMPT_MS = 120000;
|
|
21
33
|
export const IMPORT_MARGIN_MS = 60000;
|
|
@@ -27,6 +39,35 @@ function defaultV1Path(env, platform) {
|
|
|
27
39
|
}
|
|
28
40
|
/** Returns the process exit code: 0 on success, 1 on failure or bad usage. */
|
|
29
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
|
+
}
|
|
30
71
|
const [cmd, arg, ...rest] = argv;
|
|
31
72
|
let call;
|
|
32
73
|
let diagnosticEnv = d.env;
|
|
@@ -58,6 +99,7 @@ export async function main(argv, d) {
|
|
|
58
99
|
return state === 'ok' ? 'pass' : state === 'missing' ? 'fail' : 'unknown';
|
|
59
100
|
},
|
|
60
101
|
}));
|
|
102
|
+
const startBrowser = d.startBrowser ?? launchBrowser;
|
|
61
103
|
const printDoctor = (r) => d.out(r.checks.map(c => `${c.state.toUpperCase()} ${c.id}: ${c.message}${c.nextAction ? ' ' + c.nextAction : ''}`).join('\n'));
|
|
62
104
|
if (cmd === 'allow') {
|
|
63
105
|
const proposal = parseAllowArgs(argv.slice(1));
|
|
@@ -79,6 +121,7 @@ export async function main(argv, d) {
|
|
|
79
121
|
const args = argv.slice(1), seen = new Set();
|
|
80
122
|
let valid = true;
|
|
81
123
|
let browser;
|
|
124
|
+
let client = 'auto';
|
|
82
125
|
for (let i = 0; i < args.length; i++) {
|
|
83
126
|
const flag = args[i];
|
|
84
127
|
if (seen.has(flag) || !['--client', '--browser', '--apply', '--replace-existing', '--start-browser', '--json'].includes(flag)) {
|
|
@@ -86,9 +129,14 @@ export async function main(argv, d) {
|
|
|
86
129
|
break;
|
|
87
130
|
}
|
|
88
131
|
seen.add(flag);
|
|
89
|
-
if (flag === '--client'
|
|
90
|
-
|
|
91
|
-
|
|
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;
|
|
92
140
|
}
|
|
93
141
|
if (flag === '--browser') {
|
|
94
142
|
const value = args[++i];
|
|
@@ -101,27 +149,73 @@ export async function main(argv, d) {
|
|
|
101
149
|
}
|
|
102
150
|
}
|
|
103
151
|
if (!valid || (seen.has('--replace-existing') && !seen.has('--apply'))) {
|
|
104
|
-
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]');
|
|
105
153
|
return 1;
|
|
106
154
|
}
|
|
107
155
|
const options = { apply: seen.has('--apply'), replaceExisting: seen.has('--replace-existing'), startBrowser: seen.has('--start-browser'), ...(browser ? { browser } : {}) };
|
|
108
156
|
if (browser)
|
|
109
157
|
diagnosticEnv = { ...d.env, U_CHAN_BROWSER: browser };
|
|
110
158
|
const cfg = loadConfig(diagnosticEnv, d.platform), endpoint = cfg.cdpEndpoint;
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
}
|
|
119
179
|
if (seen.has('--json'))
|
|
120
180
|
d.out(JSON.stringify(r));
|
|
121
181
|
else
|
|
122
|
-
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'));
|
|
123
183
|
return setupExitCode(r);
|
|
124
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
|
+
}
|
|
125
219
|
if (cmd === 'doctor' && (arg === undefined || arg === '--json') && rest.length === 0) {
|
|
126
220
|
const r = await doctor();
|
|
127
221
|
if (arg === '--json')
|
|
@@ -233,9 +327,6 @@ export async function main(argv, d) {
|
|
|
233
327
|
}
|
|
234
328
|
call = () => d.connect({}).audit(limit);
|
|
235
329
|
}
|
|
236
|
-
else if (cmd === 'update' && arg === undefined) {
|
|
237
|
-
call = () => d.connect({ timeoutMs: 120000 }).update();
|
|
238
|
-
}
|
|
239
330
|
else if (cmd === 'manage' && arg === undefined) {
|
|
240
331
|
call = () => d.connect({ timeoutMs: 15 * 60_000 }).manage();
|
|
241
332
|
}
|
|
@@ -252,6 +343,11 @@ export async function main(argv, d) {
|
|
|
252
343
|
printDoctor(await doctor());
|
|
253
344
|
return r.ok ? 0 : 1;
|
|
254
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
|
+
}
|
|
255
351
|
const isEntry = (() => {
|
|
256
352
|
try {
|
|
257
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,10 +16,15 @@ 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
|
}
|
|
23
|
+
if (d.configureShell) {
|
|
24
|
+
r.shell = await d.configureShell(o.apply);
|
|
25
|
+
r.changed = r.shell.changed;
|
|
26
|
+
action('configure_shell', r.shell.message, !r.shell.ready);
|
|
27
|
+
}
|
|
22
28
|
if (r.checks.some(c => c.id === 'browser_target' && c.state === 'fail')) {
|
|
23
29
|
r.sharing = 'mismatch';
|
|
24
30
|
action('configure_mcp', 'Align the desktop and service loopback browser targets before applying setup.');
|
|
@@ -26,9 +32,9 @@ export async function runSetup(o, d) {
|
|
|
26
32
|
}
|
|
27
33
|
const configured = await d.configure({ apply: o.apply, replaceExisting: o.replaceExisting, ...(o.browser ? { browser: o.browser } : {}) });
|
|
28
34
|
r.client = configured.state;
|
|
29
|
-
r.changed
|
|
35
|
+
r.changed ||= configured.changed;
|
|
30
36
|
r.backupPath = configured.backupPath;
|
|
31
|
-
r.restartRequired =
|
|
37
|
+
r.restartRequired = configured.changed;
|
|
32
38
|
if (o.browser && o.apply && r.client === 'ready') {
|
|
33
39
|
if (!d.selectBrowser)
|
|
34
40
|
throw new Error('browser selection unavailable');
|
|
@@ -48,7 +54,7 @@ export async function runSetup(o, d) {
|
|
|
48
54
|
const pw = r.checks.find(c => c.id === 'playwright');
|
|
49
55
|
const mismatch = r.checks.some(c => c.id === 'browser_target' && c.state === 'fail');
|
|
50
56
|
r.sharing = mismatch || pw?.state === 'fail' ? 'mismatch' : pw?.state === 'pass' ? 'process_configured' : 'unobserved';
|
|
51
|
-
r.restartRequired
|
|
57
|
+
r.restartRequired ||= pw?.state === 'fail';
|
|
52
58
|
const ids = { registration: 'install_service', service: 'install_service', ipc: 'install_service',
|
|
53
59
|
version: 'update_service', assets: 'repair_permissions', permissions: 'repair_permissions', service_browser: 'start_browser', chrome: 'start_browser',
|
|
54
60
|
browser_target: 'configure_mcp', browser_identity: 'start_browser', playwright: 'configure_mcp', uac: 'review_uac' };
|
|
@@ -58,14 +64,14 @@ export async function runSetup(o, d) {
|
|
|
58
64
|
action(id, c.nextAction, c.required && c.state !== 'pass');
|
|
59
65
|
}
|
|
60
66
|
if (r.client !== 'ready')
|
|
61
|
-
action('configure_mcp',
|
|
67
|
+
action('configure_mcp', `Inspect ${clientName} configuration; apply missing registrations explicitly and preserve custom settings.`);
|
|
62
68
|
if (!started)
|
|
63
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.');
|
|
64
70
|
if (r.restartRequired)
|
|
65
71
|
action('restart_client', 'Restart the AI session to load saved MCP registrations.', false);
|
|
66
72
|
action('configure_mcp', 'Active AI-session browser ownership is unverified; check the current client browser connection.', false);
|
|
67
73
|
const requiredOk = r.checks.length > 0 && r.checks.filter(c => c.required).every(c => c.state === 'pass');
|
|
68
|
-
if (requiredOk && r.client === 'ready' && started && !mismatch)
|
|
74
|
+
if (requiredOk && r.client === 'ready' && started && !mismatch && r.shell?.ready !== false)
|
|
69
75
|
r.state = 'configured';
|
|
70
76
|
return r;
|
|
71
77
|
}
|
|
@@ -77,3 +83,31 @@ export async function runSetup(o, d) {
|
|
|
77
83
|
export function setupExitCode(r) {
|
|
78
84
|
return r.state === 'needs_attention' ? 1 : r.restartRequired ? 2 : 0;
|
|
79
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
|
+
}
|