@harness-mix/cli 0.1.4 → 0.1.5
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/CHANGELOG.md +8 -0
- package/README.md +26 -96
- package/docs/chatgpt-web-sidebar.md +7 -1
- package/docs/cross-platform.md +15 -0
- package/docs/harness-handoff-design.md +377 -0
- package/docs/harness-management.md +28 -1
- package/docs/native-codex.md +21 -10
- package/output/native-build/harness-mix-shim.exe +0 -0
- package/output/native-build/renderer-extension.js +972 -35
- package/package.json +18 -9
- package/scripts/chatgpt-context-ui-smoke.cjs +16 -0
- package/scripts/codex-accounts-test.cjs +81 -0
- package/scripts/codex-accounts-ui-smoke.cjs +70 -0
- package/scripts/e2e-integrations.cjs +52 -0
- package/scripts/e2e-native-host.cjs +39 -0
- package/scripts/handoff-checkpoint-test.cjs +83 -0
- package/scripts/install-native.cjs +4 -1
- package/scripts/integrations-test.cjs +64 -0
- package/scripts/integrations-ui-smoke.cjs +63 -0
- package/scripts/native-protocol-test.cjs +78 -4
- package/scripts/native-secret-test.cjs +2 -2
- package/scripts/native-shim-test.cjs +20 -4
- package/scripts/native-ui-smoke.cjs +58 -2
- package/scripts/publish-native-package.cjs +8 -5
- package/scripts/support/integrations-mcp-fixture.cjs +15 -0
- package/scripts/switch-harness-test.cjs +27 -1
- package/src/main/adapters/acp.js +3 -3
- package/src/main/adapters/antigravity.js +10 -13
- package/src/main/adapters/claude.js +10 -3
- package/src/main/adapters/codex-app-server.js +20 -13
- package/src/main/adapters/codex.js +6 -4
- package/src/main/adapters/dsh.js +2 -1
- package/src/main/adapters/grok.js +4 -3
- package/src/main/adapters/managed-mcp.js +12 -0
- package/src/main/adapters/native-acp.js +4 -4
- package/src/main/adapters/opencode.js +9 -3
- package/src/main/adapters/pi.js +1 -0
- package/src/main/harness-adapter/manifest.js +17 -2
- package/src/main/host/handoff-access.js +53 -0
- package/src/main/host/handoff-checkpoints.js +206 -0
- package/src/main/host/handoff-mcp.cjs +38 -0
- package/src/main/host/handoff-tools.js +13 -0
- package/src/main/host/handoff.js +28 -6
- package/src/main/host/integrations.js +201 -0
- package/src/main/host/runtime.js +75 -35
- package/src/main/host/store.js +4 -4
- package/src/main/native/codex-accounts.js +260 -0
- package/src/main/native/host.js +1 -1
- package/src/main/native/launcher.js +2 -2
- package/src/main/native/protocol.js +74 -23
- package/src/main/native/redact.js +1 -1
- package/src/main/native/rs/crates/shim/src/main.rs +155 -12
- package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/index.d.ts +4 -0
- package/src/native-ui/renderer-extension/dist/types/index.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts +2 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts +36 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts +3 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts +53 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts +80 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +7 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts +2 -0
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts +5 -0
- package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts +5 -0
- package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +8 -4
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/src/harness-mix-settings.ts +3 -1
- package/src/native-ui/renderer-extension/src/index.ts +28 -0
- package/src/native-ui/renderer-extension/src/renderer-agent-icon.ts +4 -4
- package/src/native-ui/renderer-extension/src/renderer-agent-picker.ts +46 -6
- package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +159 -40
- package/src/native-ui/renderer-extension/src/renderer-chatgpt-context.ts +83 -0
- package/src/native-ui/renderer-extension/src/renderer-codex-account-state.ts +10 -0
- package/src/native-ui/renderer-extension/src/renderer-composer-dom.ts +10 -0
- package/src/native-ui/renderer-extension/src/renderer-harness-handoff.ts +330 -0
- package/src/native-ui/renderer-extension/src/renderer-integrations-client.ts +26 -0
- package/src/native-ui/renderer-extension/src/renderer-model-client.ts +17 -1
- package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +2 -0
- package/src/native-ui/renderer-extension/src/settings/accounts-list.ts +16 -1
- package/src/native-ui/renderer-extension/src/settings/accounts-page.ts +32 -3
- package/src/native-ui/renderer-extension/src/settings/integrations-page.ts +102 -0
- package/src/native-ui/renderer-extension/src/settings/localization.ts +30 -13
- package/src/native-ui/renderer-extension/src/settings/pages.ts +47 -18
- package/src/native-ui/renderer-extension/src/settings/shell.css +13 -0
- package/src/native-ui/renderer-extension/test/renderer-agent-picker.test.ts +44 -13
- package/src/native-ui/renderer-extension/test/renderer-chatgpt-context.test.ts +18 -0
- package/src/native-ui/renderer-extension/test/renderer-codex-account-state.test.ts +13 -1
- package/src/native-ui/renderer-extension/test/renderer-harness-handoff.test.ts +20 -0
- package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +9 -2
- package/src/native-ui/renderer-extension/test/settings/localization.test.ts +1 -1
- package/src/native-ui/renderer-extension/test/settings/pages.test.ts +77 -8
- package/src/native-ui/renderer-extension/test/settings/shell.test.ts +3 -0
- package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts +19 -4
- package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts.map +1 -1
- package/src/native-ui/shared-contracts/dist/codex-accounts.js +2 -0
- package/src/native-ui/shared-contracts/dist/codex-accounts.js.map +1 -1
- package/src/native-ui/shared-contracts/dist/index.d.ts +2 -2
- package/src/native-ui/shared-contracts/dist/index.d.ts.map +1 -1
- package/src/native-ui/shared-contracts/dist/index.js +1 -1
- package/src/native-ui/shared-contracts/dist/index.js.map +1 -1
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts +29 -0
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts.map +1 -1
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.js +13 -0
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.js.map +1 -1
- package/src/native-ui/shared-contracts/dist/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/shared-contracts/src/codex-accounts.ts +2 -0
- package/src/native-ui/shared-contracts/src/index.ts +8 -1
- package/src/native-ui/shared-contracts/src/thread-harness-switch.ts +17 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const { randomUUID } = require('node:crypto');
|
|
5
|
+
const { CodexAppServer } = require('../adapters/codex-app-server');
|
|
6
|
+
|
|
7
|
+
const OFFICIAL_ACCOUNT_ID = 'official-codex';
|
|
8
|
+
const ACCOUNT_ID = /^[A-Za-z0-9._~-]{1,256}$/;
|
|
9
|
+
const PLAN_TYPES = new Set(['free', 'go', 'plus', 'pro', 'prolite', 'team', 'self_serve_business_prolite', 'self_serve_business_usage_based', 'business', 'ent26', 'enterprise_cbp_automation', 'enterprise_cbp_usage_based', 'enterprise', 'edu', 'edu_plus', 'edu_pro', 'unknown']);
|
|
10
|
+
|
|
11
|
+
function isoFromUnixSeconds(value) {
|
|
12
|
+
return Number.isSafeInteger(value) && value >= 0 ? new Date(value * 1000).toISOString() : undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function rateLimitView(result) {
|
|
16
|
+
const snapshot = result?.rateLimits;
|
|
17
|
+
const primary = snapshot?.primary;
|
|
18
|
+
const secondary = snapshot?.secondary;
|
|
19
|
+
const usage = {};
|
|
20
|
+
if (typeof primary?.usedPercent === 'number' && primary.usedPercent >= 0 && primary.usedPercent <= 100) {
|
|
21
|
+
usage.planFiveHourUsedPercent = primary.usedPercent;
|
|
22
|
+
if (Number.isSafeInteger(primary.resetsAt) && primary.resetsAt >= 0) usage.planFiveHourResetsAtUnix = primary.resetsAt;
|
|
23
|
+
}
|
|
24
|
+
if (typeof secondary?.usedPercent === 'number' && secondary.usedPercent >= 0 && secondary.usedPercent <= 100) {
|
|
25
|
+
usage.planSevenDayUsedPercent = secondary.usedPercent;
|
|
26
|
+
if (Number.isSafeInteger(secondary.resetsAt) && secondary.resetsAt >= 0) usage.planSevenDayResetsAtUnix = secondary.resetsAt;
|
|
27
|
+
}
|
|
28
|
+
let accountCredits;
|
|
29
|
+
if (typeof primary?.usedPercent === 'number' && primary.usedPercent >= 0 && primary.usedPercent <= 100) {
|
|
30
|
+
accountCredits = {
|
|
31
|
+
usedPercent: primary.usedPercent,
|
|
32
|
+
periodType: primary.windowDurationMins === 300 ? 'five_hour' : 'unknown',
|
|
33
|
+
...(isoFromUnixSeconds(primary.resetsAt) ? { resetsAt: isoFromUnixSeconds(primary.resetsAt) } : {}),
|
|
34
|
+
};
|
|
35
|
+
if (typeof secondary?.usedPercent === 'number' && secondary.usedPercent >= 0 && secondary.usedPercent <= 100) {
|
|
36
|
+
accountCredits.productUsage = [{
|
|
37
|
+
product: secondary.windowDurationMins === 10080 ? '7-day window' : (snapshot.limitName || 'Secondary limit'),
|
|
38
|
+
usagePercent: secondary.usedPercent,
|
|
39
|
+
...(isoFromUnixSeconds(secondary.resetsAt) ? { resetsAt: isoFromUnixSeconds(secondary.resetsAt) } : {}),
|
|
40
|
+
}];
|
|
41
|
+
}
|
|
42
|
+
const reset = result?.rateLimitResetCredits;
|
|
43
|
+
if (Number.isSafeInteger(reset?.availableCount) && reset.availableCount > 0) {
|
|
44
|
+
const expiresAt = Array.isArray(reset.credits)
|
|
45
|
+
? reset.credits.map(credit => isoFromUnixSeconds(credit?.expiresAt)).filter(Boolean).slice(0, 32)
|
|
46
|
+
: [];
|
|
47
|
+
accountCredits.resetCredits = {
|
|
48
|
+
availableCount: reset.availableCount,
|
|
49
|
+
...(expiresAt.length ? { nextExpiresAt: expiresAt[0], expiresAt } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { usage: Object.keys(usage).length ? usage : null, ...(accountCredits ? { accountCredits } : {}) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function accountIdentity(account) {
|
|
57
|
+
const email = account?.type === 'chatgpt' && typeof account.email === 'string' && account.email.trim()
|
|
58
|
+
? account.email.trim() : undefined;
|
|
59
|
+
const planType = account?.type === 'chatgpt' && PLAN_TYPES.has(account.planType) ? account.planType : undefined;
|
|
60
|
+
return { email, planType, authenticated: !!account };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
class CodexAccountManager {
|
|
64
|
+
constructor({ dataDirectory, requestOfficial, emit, acquireServer = CodexAppServer.acquire }) {
|
|
65
|
+
this.root = path.resolve(dataDirectory, 'codex-accounts');
|
|
66
|
+
this.profilesRoot = path.join(this.root, 'profiles');
|
|
67
|
+
this.registryFile = path.join(this.root, 'accounts.json');
|
|
68
|
+
this.requestOfficial = requestOfficial;
|
|
69
|
+
this.emit = emit;
|
|
70
|
+
this.acquireServer = acquireServer;
|
|
71
|
+
this.logins = new Map();
|
|
72
|
+
this.registry = this.#load();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#load() {
|
|
76
|
+
try {
|
|
77
|
+
const value = JSON.parse(fs.readFileSync(this.registryFile, 'utf8'));
|
|
78
|
+
const seen = new Set();
|
|
79
|
+
const accounts = (Array.isArray(value.accounts) ? value.accounts : []).filter(entry => {
|
|
80
|
+
if (!entry || !ACCOUNT_ID.test(entry.accountId) || typeof entry.label !== 'string' || !entry.label.trim() || entry.accountId === OFFICIAL_ACCOUNT_ID || seen.has(entry.accountId)) return false;
|
|
81
|
+
seen.add(entry.accountId);
|
|
82
|
+
entry.label = entry.label.trim().slice(0, 256);
|
|
83
|
+
return true;
|
|
84
|
+
}).slice(0, 127);
|
|
85
|
+
const activeAccountId = value.activeAccountId === OFFICIAL_ACCOUNT_ID || accounts.some(entry => entry.accountId === value.activeAccountId)
|
|
86
|
+
? value.activeAccountId : OFFICIAL_ACCOUNT_ID;
|
|
87
|
+
return { version: 1, activeAccountId, accounts };
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error.code !== 'ENOENT') throw error;
|
|
90
|
+
return { version: 1, activeAccountId: OFFICIAL_ACCOUNT_ID, accounts: [] };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#save() {
|
|
95
|
+
fs.mkdirSync(this.root, { recursive: true });
|
|
96
|
+
const temp = `${this.registryFile}.${process.pid}.tmp`;
|
|
97
|
+
fs.writeFileSync(temp, JSON.stringify(this.registry, null, 2));
|
|
98
|
+
fs.renameSync(temp, this.registryFile);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#entry(accountId) {
|
|
102
|
+
if (accountId === OFFICIAL_ACCOUNT_ID) return { accountId, label: 'Codex 官方账号', codexHome: process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), native: true };
|
|
103
|
+
const entry = this.registry.accounts.find(candidate => candidate.accountId === accountId);
|
|
104
|
+
if (!entry) throw new Error('Unknown Codex account');
|
|
105
|
+
const codexHome = path.resolve(this.profilesRoot, entry.accountId);
|
|
106
|
+
if (!codexHome.startsWith(`${path.resolve(this.profilesRoot)}${path.sep}`)) throw new Error('Invalid Codex account directory');
|
|
107
|
+
return { ...entry, codexHome, native: false };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async #request(entry, method, params) {
|
|
111
|
+
if (entry.native) {
|
|
112
|
+
if (!this.requestOfficial) throw new Error('Official Codex account service is unavailable');
|
|
113
|
+
return this.requestOfficial(method, params);
|
|
114
|
+
}
|
|
115
|
+
const server = await this.acquireServer(undefined, entry.codexHome);
|
|
116
|
+
try { return await server.request(method, params); }
|
|
117
|
+
finally { server.release(); }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async #summary(entry, refreshToken = false) {
|
|
121
|
+
let raw = null;
|
|
122
|
+
try { raw = await this.#request(entry, 'account/read', { refreshToken }); }
|
|
123
|
+
catch { /* A signed-out profile is still a useful selectable Account slot. */ }
|
|
124
|
+
const identity = accountIdentity(raw?.account);
|
|
125
|
+
return {
|
|
126
|
+
accountId: entry.accountId,
|
|
127
|
+
label: identity.email || entry.label,
|
|
128
|
+
...(identity.email ? { email: identity.email } : {}),
|
|
129
|
+
...(identity.planType ? { planType: identity.planType } : {}),
|
|
130
|
+
codexHome: entry.codexHome,
|
|
131
|
+
active: this.registry.activeAccountId === entry.accountId,
|
|
132
|
+
isDefault: entry.native,
|
|
133
|
+
authenticated: identity.authenticated,
|
|
134
|
+
management: entry.native ? 'native' : 'isolated',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async list(refresh = false) {
|
|
139
|
+
const entries = [this.#entry(OFFICIAL_ACCOUNT_ID), ...this.registry.accounts.map(entry => this.#entry(entry.accountId))];
|
|
140
|
+
return { accounts: await Promise.all(entries.map(entry => this.#summary(entry, refresh))) };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async create(label) {
|
|
144
|
+
if (this.registry.accounts.length >= 127) throw new Error('Codex Account limit reached (128 including the official account)');
|
|
145
|
+
const accountId = `account-${randomUUID().slice(0, 8)}`;
|
|
146
|
+
const entry = { accountId, label: typeof label === 'string' && label.trim() ? label.trim().slice(0, 256) : `Codex 账号 ${this.registry.accounts.length + 2}`, createdAt: Date.now() };
|
|
147
|
+
fs.mkdirSync(path.join(this.profilesRoot, accountId), { recursive: true });
|
|
148
|
+
this.registry.accounts.push(entry);
|
|
149
|
+
this.#save();
|
|
150
|
+
return { account: await this.#summary(this.#entry(accountId)) };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async activate(accountId) {
|
|
154
|
+
const entry = this.#entry(accountId);
|
|
155
|
+
this.registry.activeAccountId = entry.accountId;
|
|
156
|
+
this.#save();
|
|
157
|
+
return { account: await this.#summary(entry) };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async delete(accountId) {
|
|
161
|
+
const entry = this.#entry(accountId);
|
|
162
|
+
if (entry.native) throw new Error('The official Codex account cannot be deleted');
|
|
163
|
+
if ([...this.logins.values()].some(login => login.accountId === accountId)) throw new Error('Cancel account sign-in before deleting it');
|
|
164
|
+
const resolved = path.resolve(entry.codexHome);
|
|
165
|
+
const root = path.resolve(this.profilesRoot);
|
|
166
|
+
if (!resolved.startsWith(`${root}${path.sep}`)) throw new Error('Refusing to delete an account outside the managed profile directory');
|
|
167
|
+
fs.rmSync(resolved, { recursive: true, force: true });
|
|
168
|
+
this.registry.accounts = this.registry.accounts.filter(candidate => candidate.accountId !== accountId);
|
|
169
|
+
if (this.registry.activeAccountId === accountId) this.registry.activeAccountId = OFFICIAL_ACCOUNT_ID;
|
|
170
|
+
this.#save();
|
|
171
|
+
return { deletedAccountId: accountId };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async startLogin(accountId) {
|
|
175
|
+
const entry = this.#entry(accountId);
|
|
176
|
+
if (entry.native) {
|
|
177
|
+
const result = await this.#request(entry, 'account/login/start', { type: 'chatgptDeviceCode' });
|
|
178
|
+
return { accountId, loginId: result.loginId, verificationUrl: result.verificationUrl, userCode: result.userCode };
|
|
179
|
+
}
|
|
180
|
+
const server = await this.acquireServer(undefined, entry.codexHome);
|
|
181
|
+
let unwatch;
|
|
182
|
+
try {
|
|
183
|
+
unwatch = server.onNotification(message => {
|
|
184
|
+
if (message?.method !== 'account/login/completed') return;
|
|
185
|
+
const pending = this.logins.get(message.params?.loginId);
|
|
186
|
+
if (!pending || pending.server !== server) return;
|
|
187
|
+
this.logins.delete(message.params.loginId);
|
|
188
|
+
pending.unwatch?.();
|
|
189
|
+
pending.server.release();
|
|
190
|
+
this.emit?.({ method: 'codexhost/account/login/completed', params: {
|
|
191
|
+
accountId: pending.accountId,
|
|
192
|
+
loginId: message.params.loginId,
|
|
193
|
+
success: message.params.success === true,
|
|
194
|
+
error: typeof message.params.error === 'string' ? message.params.error : null,
|
|
195
|
+
} });
|
|
196
|
+
});
|
|
197
|
+
const result = await server.request('account/login/start', { type: 'chatgptDeviceCode' });
|
|
198
|
+
this.logins.set(result.loginId, { accountId, server, unwatch });
|
|
199
|
+
return { accountId, loginId: result.loginId, verificationUrl: result.verificationUrl, userCode: result.userCode };
|
|
200
|
+
} catch (error) {
|
|
201
|
+
unwatch?.();
|
|
202
|
+
server.release();
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async cancelLogin(loginId) {
|
|
208
|
+
const pending = this.logins.get(loginId);
|
|
209
|
+
if (!pending) {
|
|
210
|
+
if (!this.requestOfficial) return { cancelled: false };
|
|
211
|
+
const result = await this.requestOfficial('account/login/cancel', { loginId });
|
|
212
|
+
return { cancelled: ['canceled', 'cancelled'].includes(result?.status) };
|
|
213
|
+
}
|
|
214
|
+
const result = await pending.server.request('account/login/cancel', { loginId });
|
|
215
|
+
this.logins.delete(loginId);
|
|
216
|
+
pending.unwatch?.();
|
|
217
|
+
pending.server.release();
|
|
218
|
+
return { cancelled: ['canceled', 'cancelled'].includes(result?.status) };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async usage(accountId) {
|
|
222
|
+
const entry = this.#entry(accountId);
|
|
223
|
+
return { accountId, ...rateLimitView(await this.#request(entry, 'account/rateLimits/read', {})) };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async consumeReset(accountId, idempotencyKey) {
|
|
227
|
+
const entry = this.#entry(accountId);
|
|
228
|
+
const result = await this.#request(entry, 'account/rateLimitResetCredit/consume', { idempotencyKey: idempotencyKey || randomUUID() });
|
|
229
|
+
const refreshed = rateLimitView(await this.#request(entry, 'account/rateLimits/read', {}));
|
|
230
|
+
return { accountId, outcome: result.outcome, ...(refreshed.accountCredits ? { accountCredits: refreshed.accountCredits } : {}) };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
signedOutOfficial() {
|
|
234
|
+
const entry = this.#entry(OFFICIAL_ACCOUNT_ID);
|
|
235
|
+
return {
|
|
236
|
+
account: {
|
|
237
|
+
accountId: entry.accountId,
|
|
238
|
+
label: entry.label,
|
|
239
|
+
codexHome: entry.codexHome,
|
|
240
|
+
active: this.registry.activeAccountId === entry.accountId,
|
|
241
|
+
isDefault: true,
|
|
242
|
+
authenticated: false,
|
|
243
|
+
management: 'native',
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
executionContext(accountId) {
|
|
249
|
+
const entry = this.#entry(accountId);
|
|
250
|
+
if (entry.native) throw new Error('The official account stays on the native Codex route');
|
|
251
|
+
return { accountId: entry.accountId, codexHome: entry.codexHome };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
close() {
|
|
255
|
+
for (const pending of this.logins.values()) { pending.unwatch?.(); pending.server.release(); }
|
|
256
|
+
this.logins.clear();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = { CodexAccountManager, OFFICIAL_ACCOUNT_ID, rateLimitView };
|
package/src/main/native/host.js
CHANGED
|
@@ -148,6 +148,6 @@ async function runNativeHost() {
|
|
|
148
148
|
official.on('error', error => { console.error(error); void close(); });
|
|
149
149
|
official.on('exit', () => { void close(); });
|
|
150
150
|
await ready;
|
|
151
|
-
console.error('[Harness Mix]
|
|
151
|
+
console.error('[Harness Mix] official Codex passthrough + external Harness routes via HostRuntime/ProtocolCore');
|
|
152
152
|
}
|
|
153
153
|
module.exports = { runNativeHost };
|
|
@@ -120,7 +120,7 @@ async function launch(args = []) {
|
|
|
120
120
|
const paths = nativePaths();
|
|
121
121
|
for (const file of Object.values(paths)) if (!fs.existsSync(file)) throw new Error(`Missing ${file}; run npm run build:native`);
|
|
122
122
|
if (flags.has('--check')) {
|
|
123
|
-
console.log(`platform=${process.platform}\narchitecture=${process.arch}\ndesktop_version=${installation.version}\ndesktop_compatibility=${compatibility.state}\ndesktop_evidence=${compatibility.evidence?.level || 'none'}\nexecutable_codex_cli=${installation.stock}\nlauncher=${paths.cli}\nshim=${paths.shim}\nruntime=${paths.runtime}\nrenderer=${paths.renderer}\ncore=src/main/protocol-core`);
|
|
123
|
+
console.log(`platform=${process.platform}\narchitecture=${process.arch}\ndesktop_version=${installation.version}\ndesktop_compatibility=${compatibility.state}\ndesktop_evidence=${compatibility.evidence?.level || 'none'}\nexecutable_codex_cli=${installation.stock}\ncodex_mode=official-passthrough\ncodex_managed_route=codex-harness\nlauncher=${paths.cli}\nshim=${paths.shim}\nruntime=${paths.runtime}\nrenderer=${paths.renderer}\ncore=src/main/protocol-core`);
|
|
124
124
|
return;
|
|
125
125
|
}
|
|
126
126
|
const skipUpdate = flags.has('--no-update') || process.env.HARNESS_MIX_AUTO_UPDATE === '0';
|
|
@@ -151,7 +151,7 @@ async function launch(args = []) {
|
|
|
151
151
|
CODEXHOST_DATA_DIR: env.CODEXHOST_DATA_DIR, HARNESS_MIX_NODE_PATH: process.execPath,
|
|
152
152
|
CODEXHOST_DEFAULT_AGENT: 'codex' };
|
|
153
153
|
const block = Buffer.from(Object.entries(overrides).map(([k, v]) => `${k}=${v}`).join('\0') + '\0\0', 'utf16le').toString('base64');
|
|
154
|
-
console.log('[Harness Mix] Restarting Codex Desktop
|
|
154
|
+
console.log('[Harness Mix] Restarting Codex Desktop: official Codex passthrough + Harness Mix routes.');
|
|
155
155
|
stopDesktopProcesses(installation);
|
|
156
156
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
157
157
|
sweepLeftovers(root, dataDir);
|
|
@@ -20,7 +20,8 @@ const {
|
|
|
20
20
|
compareVersions,
|
|
21
21
|
readState,
|
|
22
22
|
} = require('./update-state');
|
|
23
|
-
const { nativeEnvironment } = require('./config');
|
|
23
|
+
const { nativeEnvironment } = require('./config');
|
|
24
|
+
const { CodexAccountManager } = require('./codex-accounts');
|
|
24
25
|
|
|
25
26
|
const REPO_ROOT = path.resolve(__dirname, '../../..');
|
|
26
27
|
const PACKAGE_JSON_PATH = path.join(REPO_ROOT, 'package.json');
|
|
@@ -31,9 +32,8 @@ const modelRef = model => ({ id: Buffer.from(JSON.stringify({ id: model.id, prov
|
|
|
31
32
|
const routeModel = harnessId => ['pi', 'claude-code', 'deepseek-harness', 'antigravity', 'omp', 'opencode', 'grok'].includes(harnessId)
|
|
32
33
|
? `codexhost/${harnessId}-native`
|
|
33
34
|
: `codexhost/plugin-v1@${Buffer.from(JSON.stringify({ harnessId })).toString('hex')}`;
|
|
34
|
-
const terminal = status => ['completed', 'cancelled', 'error', 'failed'].includes(status);
|
|
35
|
-
const turnStatus = status => ({ cancelled: 'interrupted', error: 'failed', failed: 'failed', completed: 'completed' }[status] || 'inProgress');
|
|
36
|
-
|
|
35
|
+
const terminal = status => ['completed', 'cancelled', 'error', 'failed'].includes(status);
|
|
36
|
+
const turnStatus = status => ({ cancelled: 'interrupted', error: 'failed', failed: 'failed', completed: 'completed' }[status] || 'inProgress');
|
|
37
37
|
const HARNESS_INSTALL_COMMANDS = {
|
|
38
38
|
qoder: { win32: 'npm install -g @qoder-ai/qodercli', default: 'npm install -g @qoder-ai/qodercli' },
|
|
39
39
|
codex: { win32: 'npm install -g @openai/codex', default: 'npm install -g @openai/codex' },
|
|
@@ -189,10 +189,15 @@ function projectItem(item) {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
class NativeProtocol {
|
|
192
|
-
constructor(runtime, emit, requestOfficial = null) {
|
|
192
|
+
constructor(runtime, emit, requestOfficial = null) {
|
|
193
193
|
this.runtime = runtime;
|
|
194
194
|
this.emit = emit;
|
|
195
|
-
this.requestOfficial = requestOfficial;
|
|
195
|
+
this.requestOfficial = requestOfficial;
|
|
196
|
+
this.codexAccounts = new CodexAccountManager({
|
|
197
|
+
dataDirectory: nativeEnvironment().CODEXHOST_DATA_DIR,
|
|
198
|
+
requestOfficial,
|
|
199
|
+
emit,
|
|
200
|
+
});
|
|
196
201
|
this.approvals = new Map();
|
|
197
202
|
this.published = new Map();
|
|
198
203
|
this.steering = new Map(); // threadId -> in-flight steer promise
|
|
@@ -317,13 +322,34 @@ class NativeProtocol {
|
|
|
317
322
|
if (!model) throw new Error('Selected model is no longer in the native catalog');
|
|
318
323
|
return model;
|
|
319
324
|
}
|
|
320
|
-
async request(method, params = {}) {
|
|
325
|
+
async request(method, params = {}) {
|
|
326
|
+
if (method === 'codexhost/integrations/catalog') return this.runtime.integrations.catalog();
|
|
327
|
+
if (method === 'codexhost/integrations/list') return this.runtime.integrations.list(params);
|
|
328
|
+
if (method === 'codexhost/integrations/mcp/save') return this.runtime.integrations.save(params);
|
|
329
|
+
if (method === 'codexhost/integrations/mcp/remove') return this.runtime.integrations.remove(params);
|
|
330
|
+
if (method === 'codexhost/integrations/skill/change') return this.runtime.integrations.skillChange(params);
|
|
321
331
|
if (method === 'codexhost/harness/session-import/sources') return this.runtime.history.sources();
|
|
322
332
|
if (method === 'codexhost/harness/session-import/list') return this.runtime.history.list(params);
|
|
323
333
|
if (method === 'codexhost/harness/session-import/import') return this.runtime.history.import(params);
|
|
324
334
|
if (method === 'codexhost/collaboration/agents') return [...this.runtime.adapters.values()].map(a => ({ id: externalId(a.manifest.id), name: a.manifest.name, available: !!this.runtime.status[a.manifest.id]?.available, lead: a.manifest.capabilities?.collaborationTools === true }));
|
|
325
335
|
const thread = this.thread(params.threadId);
|
|
326
|
-
if (method === 'harness-mix/runtime/inspect') return {
|
|
336
|
+
if (method === 'harness-mix/runtime/inspect') return {
|
|
337
|
+
owner: 'harness-mix',
|
|
338
|
+
runtime: 'src/main/host/runtime.js',
|
|
339
|
+
core: 'src/main/protocol-core/protocol-core.js',
|
|
340
|
+
threads: this.runtime.threads.length,
|
|
341
|
+
codex: {
|
|
342
|
+
mode: 'official-passthrough',
|
|
343
|
+
// `codex` remains owned by the stock app-server. Only the explicit
|
|
344
|
+
// `codex-harness` route creates a HostRuntime-managed Codex worker.
|
|
345
|
+
managedRoute: 'codex-harness',
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
if (method === 'harness-mix/runtime/version') {
|
|
349
|
+
let version = '0.0.0';
|
|
350
|
+
try { version = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8')).version || version; } catch {}
|
|
351
|
+
return { version };
|
|
352
|
+
}
|
|
327
353
|
// Updates
|
|
328
354
|
if (method === 'codexhost/update/check') {
|
|
329
355
|
const dataDir = nativeEnvironment().CODEXHOST_DATA_DIR;
|
|
@@ -429,8 +455,22 @@ class NativeProtocol {
|
|
|
429
455
|
return { status: null };
|
|
430
456
|
}
|
|
431
457
|
|
|
432
|
-
//
|
|
433
|
-
|
|
458
|
+
// Each extra Account is an isolated native CODEX_HOME. Harness Mix stores
|
|
459
|
+
// only the profile label/path; auth.json and token refresh remain owned by Codex.
|
|
460
|
+
if (method === 'codexhost/account/list') return this.codexAccounts.list(false);
|
|
461
|
+
if (method === 'codexhost/account/refresh') return this.codexAccounts.list(true);
|
|
462
|
+
if (method === 'codexhost/account/create') return this.codexAccounts.create(params.label);
|
|
463
|
+
if (method === 'codexhost/account/delete') return this.codexAccounts.delete(params.accountId);
|
|
464
|
+
if (method === 'codexhost/account/activate') return this.codexAccounts.activate(params.accountId);
|
|
465
|
+
if (method === 'codexhost/account/usage/inspect') return this.codexAccounts.usage(params.accountId);
|
|
466
|
+
if (method === 'codexhost/account/login/start') return this.codexAccounts.startLogin(params.accountId);
|
|
467
|
+
if (method === 'codexhost/account/login/cancel') return this.codexAccounts.cancelLogin(params.loginId);
|
|
468
|
+
if (method === 'codexhost/account/logout') {
|
|
469
|
+
if (!this.requestOfficial) throw new Error('Official Codex logout is unavailable');
|
|
470
|
+
await this.requestOfficial('account/logout', undefined);
|
|
471
|
+
return this.codexAccounts.signedOutOfficial();
|
|
472
|
+
}
|
|
473
|
+
if (method === 'codexhost/account/rate-limit-reset/consume') return this.codexAccounts.consumeReset(params.accountId, params.idempotencyKey);
|
|
434
474
|
if (method === 'codexhost/harness/account/login') {
|
|
435
475
|
const extId = params.harnessId;
|
|
436
476
|
const meta = HARNESS_AUTH_INFO[extId] || HARNESS_AUTH_INFO[ALIASES[extId]];
|
|
@@ -569,17 +609,24 @@ class NativeProtocol {
|
|
|
569
609
|
...(usage ? { usage: projectUsage(usage) } : {}),
|
|
570
610
|
...(credits ? { accountCredits: credits } : {}) };
|
|
571
611
|
}
|
|
572
|
-
if (method === 'thread/start') {
|
|
573
|
-
const route = decodeRoute(params.model);
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if (!
|
|
612
|
+
if (method === 'thread/start') {
|
|
613
|
+
const route = decodeRoute(params.model);
|
|
614
|
+
const accountContext = typeof params.__codexhostAccountId === 'string'
|
|
615
|
+
? this.codexAccounts.executionContext(params.__codexhostAccountId) : null;
|
|
616
|
+
if (!route && !accountContext) return undefined;
|
|
617
|
+
const effectiveRoute = route || { harnessId: 'codex-harness', ...(typeof params.model === 'string' ? { model: { id: params.model } } : {}) };
|
|
618
|
+
const id = ALIASES[effectiveRoute.harnessId] || effectiveRoute.harnessId;
|
|
619
|
+
if (!this.runtime.adapters.has(id)) throw new Error(`Unsupported Harness: ${id}`);
|
|
577
620
|
if (params.ephemeral && params.threadSource) throw new Error('Ephemeral background thread is not supported');
|
|
578
|
-
const isWorktree = params.worktree === true || params.options?.worktree === true;
|
|
579
|
-
const
|
|
621
|
+
const isWorktree = params.worktree === true || params.options?.worktree === true;
|
|
622
|
+
const selectedModel = accountContext && typeof params.model === 'string'
|
|
623
|
+
? (await this.runtime.describe(id)).models.find(model => model.id === params.model)
|
|
624
|
+
: await this.resolveModel(id, effectiveRoute.model);
|
|
625
|
+
const created = await this.runtime.createThread({ harnessId: id, cwd: params.cwd, ephemeral: params.ephemeral === true,
|
|
580
626
|
worktree: isWorktree,
|
|
581
627
|
options: {
|
|
582
|
-
model:
|
|
628
|
+
model: selectedModel, thinking: effectiveRoute.thinkingOptionId, permissionMode: effectiveRoute.permissionModeId,
|
|
629
|
+
...(accountContext || {}),
|
|
583
630
|
...(isWorktree ? { worktree: true } : {}),
|
|
584
631
|
} });
|
|
585
632
|
if (created.error) throw new Error(created.error);
|
|
@@ -696,10 +743,14 @@ class NativeProtocol {
|
|
|
696
743
|
return this.configuration(thread);
|
|
697
744
|
}
|
|
698
745
|
// 原地切换 Harness:会话历史保留,下条消息携带一次性上下文信封(/switch 指令的 RPC 等价物)
|
|
699
|
-
if (method === 'codexhost/thread/harness/switch') {
|
|
700
|
-
await this.runtime.switchHarness(thread.id, ALIASES[params.harnessId] || params.harnessId, {
|
|
701
|
-
|
|
702
|
-
|
|
746
|
+
if (method === 'codexhost/thread/harness/switch') {
|
|
747
|
+
await this.runtime.switchHarness(thread.id, ALIASES[params.harnessId] || params.harnessId, {
|
|
748
|
+
note: typeof params.note === 'string' ? params.note : undefined,
|
|
749
|
+
intent: params.intent,
|
|
750
|
+
includes: params.includes,
|
|
751
|
+
});
|
|
752
|
+
return { threadId: thread.id, checkpointId: thread.pendingHandoff.checkpointId };
|
|
753
|
+
}
|
|
703
754
|
if (method === 'thread/fork' || method === 'codexhost/thread/fork') {
|
|
704
755
|
if (params.ephemeral || params.threadSource || params.excludeTurns) {
|
|
705
756
|
throw new Error('Ephemeral fork is not supported');
|
|
@@ -831,6 +882,6 @@ class NativeProtocol {
|
|
|
831
882
|
this.approvals.delete(message.id);
|
|
832
883
|
return true;
|
|
833
884
|
}
|
|
834
|
-
close() { this.unsubscribe(); this.unsubscribeRuntime(); }
|
|
885
|
+
close() { this.codexAccounts.close(); this.unsubscribe(); this.unsubscribeRuntime(); }
|
|
835
886
|
}
|
|
836
887
|
module.exports = { NativeProtocol, decodeRoute, projectItem, externalId, routeModel };
|
|
@@ -8,7 +8,7 @@ const PATTERNS = [
|
|
|
8
8
|
/xox[abpsr]-[A-Za-z0-9-]{10,}/g,
|
|
9
9
|
/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, // JWT
|
|
10
10
|
];
|
|
11
|
-
const SECRET_KEY = /(token|secret|password|passwd|api[_-]?key|authorization|credential|cookie)/i;
|
|
11
|
+
const SECRET_KEY = /(token|secret|password|passwd|api[_-]?key|authorization|credential|cookie|userCode|loginId|verificationUrl)/i;
|
|
12
12
|
const REDACTED = '[redacted]';
|
|
13
13
|
|
|
14
14
|
function redactText(value) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Harness Mix executable bridge, built from this source by build-native.cjs.
|
|
2
2
|
// Rust port of the original shim.cs: identical routing and stdio contract.
|
|
3
3
|
//
|
|
4
|
-
// -
|
|
5
|
-
// -
|
|
4
|
+
// - a plain `app-server` invocation -> node scripts/native-host.cjs
|
|
5
|
+
// - management subcommands and every other invocation -> stock Codex passthrough
|
|
6
6
|
use std::env;
|
|
7
7
|
use std::fs;
|
|
8
8
|
use std::io::{self, Write};
|
|
@@ -103,12 +103,97 @@ fn rotate_log(dir: &Path) {
|
|
|
103
103
|
let _ = fs::rename(&base, dir.join("shim-invocations.log.1"));
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
fn setting(env_name: &str, file: &Path) -> io::Result<String> {
|
|
107
|
-
match env::var(env_name) {
|
|
108
|
-
Ok(value) if !value.is_empty() => Ok(value),
|
|
109
|
-
_ => read_trimmed(file),
|
|
110
|
-
}
|
|
111
|
-
}
|
|
106
|
+
fn setting(env_name: &str, file: &Path) -> io::Result<String> {
|
|
107
|
+
match env::var(env_name) {
|
|
108
|
+
Ok(value) if !value.is_empty() => Ok(value),
|
|
109
|
+
_ => read_trimmed(file),
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Locate the real Codex subcommand without mistaking a prompt or config value
|
|
114
|
+
// for one. Unknown global options deliberately fall back to the stock CLI, so
|
|
115
|
+
// native Codex remains usable when a newer CLI adds syntax unknown to the Shim.
|
|
116
|
+
fn app_server_subcommand_index(args: &[String]) -> Option<usize> {
|
|
117
|
+
const VALUE_OPTIONS: &[&str] = &[
|
|
118
|
+
"-c",
|
|
119
|
+
"--config",
|
|
120
|
+
"--enable",
|
|
121
|
+
"--disable",
|
|
122
|
+
"--profile",
|
|
123
|
+
];
|
|
124
|
+
const FLAG_OPTIONS: &[&str] = &["--search", "--no-alt-screen", "--oss"];
|
|
125
|
+
let mut index = 0;
|
|
126
|
+
while let Some(arg) = args.get(index).map(String::as_str) {
|
|
127
|
+
if arg == "app-server" {
|
|
128
|
+
return Some(index);
|
|
129
|
+
}
|
|
130
|
+
if arg == "--" {
|
|
131
|
+
return None;
|
|
132
|
+
}
|
|
133
|
+
if VALUE_OPTIONS.contains(&arg) {
|
|
134
|
+
args.get(index + 1)?;
|
|
135
|
+
index += 2;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if VALUE_OPTIONS
|
|
139
|
+
.iter()
|
|
140
|
+
.any(|option| arg.strip_prefix(option).is_some_and(|rest| rest.starts_with('=')))
|
|
141
|
+
|| FLAG_OPTIONS.contains(&arg)
|
|
142
|
+
{
|
|
143
|
+
index += 1;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
return None;
|
|
147
|
+
}
|
|
148
|
+
None
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Only the app-server form used as the Desktop protocol endpoint belongs to
|
|
152
|
+
// Harness Mix. `proxy`, `daemon`, and future management forms stay entirely
|
|
153
|
+
// with the official CLI. Unknown app-server options also pass through.
|
|
154
|
+
fn should_start_host_runtime(args: &[String]) -> bool {
|
|
155
|
+
const VALUE_OPTIONS: &[&str] = &[
|
|
156
|
+
"-c",
|
|
157
|
+
"--config",
|
|
158
|
+
"--enable",
|
|
159
|
+
"--disable",
|
|
160
|
+
"--listen",
|
|
161
|
+
"--ws-auth",
|
|
162
|
+
"--ws-token-file",
|
|
163
|
+
"--ws-token-sha256",
|
|
164
|
+
"--ws-shared-secret-file",
|
|
165
|
+
"--ws-issuer",
|
|
166
|
+
"--ws-audience",
|
|
167
|
+
"--ws-max-clock-skew-seconds",
|
|
168
|
+
];
|
|
169
|
+
const FLAG_OPTIONS: &[&str] = &[
|
|
170
|
+
"--strict-config",
|
|
171
|
+
"--stdio",
|
|
172
|
+
"--analytics-default-enabled",
|
|
173
|
+
];
|
|
174
|
+
let Some(mut index) = app_server_subcommand_index(args).map(|index| index + 1) else {
|
|
175
|
+
return false;
|
|
176
|
+
};
|
|
177
|
+
while let Some(arg) = args.get(index).map(String::as_str) {
|
|
178
|
+
if VALUE_OPTIONS.contains(&arg) {
|
|
179
|
+
if args.get(index + 1).is_none() {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
index += 2;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if VALUE_OPTIONS
|
|
186
|
+
.iter()
|
|
187
|
+
.any(|option| arg.strip_prefix(option).is_some_and(|rest| rest.starts_with('=')))
|
|
188
|
+
|| FLAG_OPTIONS.contains(&arg)
|
|
189
|
+
{
|
|
190
|
+
index += 1;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
true
|
|
196
|
+
}
|
|
112
197
|
|
|
113
198
|
#[cfg(not(unix))]
|
|
114
199
|
fn pump<R: Read + Send + 'static, W: Write + Send + 'static>(
|
|
@@ -129,7 +214,7 @@ fn run() -> io::Result<i32> {
|
|
|
129
214
|
// Path.GetFullPath equivalent: normalize without resolving symlinks or adding \\?\.
|
|
130
215
|
let root = std::path::absolute(exe_dir.join("../.."))?;
|
|
131
216
|
let args: Vec<String> = env::args().skip(1).collect();
|
|
132
|
-
let server = args
|
|
217
|
+
let server = should_start_host_runtime(&args);
|
|
133
218
|
|
|
134
219
|
rotate_log(exe_dir);
|
|
135
220
|
// Kill-on-close job: the node host, every harness CLI it spawns and any
|
|
@@ -246,12 +331,70 @@ fn supervise(mut command: Command, exe_dir: &Path) -> io::Result<i32> {
|
|
|
246
331
|
Ok(code)
|
|
247
332
|
}
|
|
248
333
|
|
|
249
|
-
fn main() {
|
|
334
|
+
fn main() {
|
|
250
335
|
match run() {
|
|
251
336
|
Ok(code) => std::process::exit(code),
|
|
252
337
|
Err(error) => {
|
|
253
338
|
eprintln!("Harness Mix Shim: {error}");
|
|
254
339
|
std::process::exit(1);
|
|
255
340
|
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
#[cfg(test)]
|
|
345
|
+
mod tests {
|
|
346
|
+
use super::{app_server_subcommand_index, should_start_host_runtime};
|
|
347
|
+
|
|
348
|
+
fn args(values: &[&str]) -> Vec<String> {
|
|
349
|
+
values.iter().map(|value| (*value).to_string()).collect()
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#[test]
|
|
353
|
+
fn finds_only_the_real_app_server_subcommand() {
|
|
354
|
+
assert_eq!(app_server_subcommand_index(&args(&["app-server"])), Some(0));
|
|
355
|
+
assert_eq!(
|
|
356
|
+
app_server_subcommand_index(&args(&[
|
|
357
|
+
"-c",
|
|
358
|
+
"features.code_mode_host=true",
|
|
359
|
+
"app-server",
|
|
360
|
+
"--stdio",
|
|
361
|
+
])),
|
|
362
|
+
Some(2)
|
|
363
|
+
);
|
|
364
|
+
assert_eq!(
|
|
365
|
+
app_server_subcommand_index(&args(&["exec", "say app-server"])),
|
|
366
|
+
None
|
|
367
|
+
);
|
|
368
|
+
assert_eq!(
|
|
369
|
+
app_server_subcommand_index(&args(&["-c", "app-server", "exec"])),
|
|
370
|
+
None
|
|
371
|
+
);
|
|
372
|
+
assert_eq!(app_server_subcommand_index(&args(&["--", "app-server"])), None);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
#[test]
|
|
376
|
+
fn routes_only_plain_desktop_app_server_to_the_host() {
|
|
377
|
+
assert!(should_start_host_runtime(&args(&["app-server", "--stdio"])));
|
|
378
|
+
assert!(should_start_host_runtime(&args(&[
|
|
379
|
+
"-c",
|
|
380
|
+
"features.code_mode_host=true",
|
|
381
|
+
"app-server",
|
|
382
|
+
"--listen",
|
|
383
|
+
"stdio://",
|
|
384
|
+
"--analytics-default-enabled",
|
|
385
|
+
])));
|
|
386
|
+
assert!(should_start_host_runtime(&args(&[
|
|
387
|
+
"app-server",
|
|
388
|
+
"--listen=ws://127.0.0.1:0",
|
|
389
|
+
"--ws-auth",
|
|
390
|
+
"token",
|
|
391
|
+
])));
|
|
392
|
+
assert!(!should_start_host_runtime(&args(&["app-server", "proxy"])));
|
|
393
|
+
assert!(!should_start_host_runtime(&args(&["app-server", "daemon"])));
|
|
394
|
+
assert!(!should_start_host_runtime(&args(&[
|
|
395
|
+
"app-server",
|
|
396
|
+
"--future-option",
|
|
397
|
+
])));
|
|
398
|
+
assert!(!should_start_host_runtime(&args(&["exec", "app-server"])));
|
|
399
|
+
}
|
|
400
|
+
}
|