@vectle/cli 1.0.0 → 1.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/CHANGELOG.md +15 -0
- package/README.md +15 -8
- package/dist/activity.d.ts +10 -0
- package/dist/activity.js +10 -0
- package/dist/activity.js.map +1 -0
- package/dist/adapters/claude.d.ts +4 -0
- package/dist/adapters/claude.js +279 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/codex.d.ts +16 -0
- package/dist/adapters/codex.js +410 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/common.d.ts +80 -0
- package/dist/adapters/common.js +249 -0
- package/dist/adapters/common.js.map +1 -0
- package/dist/adapters/cursor.d.ts +4 -0
- package/dist/adapters/cursor.js +240 -0
- package/dist/adapters/cursor.js.map +1 -0
- package/dist/adapters/vscode.d.ts +4 -0
- package/dist/adapters/vscode.js +240 -0
- package/dist/adapters/vscode.js.map +1 -0
- package/dist/agent-policy.d.ts +50 -0
- package/dist/agent-policy.js +83 -0
- package/dist/agent-policy.js.map +1 -0
- package/dist/client.d.ts +69 -10
- package/dist/client.js +247 -24
- package/dist/client.js.map +1 -1
- package/dist/constants.d.ts +12 -2
- package/dist/constants.js +7 -1
- package/dist/constants.js.map +1 -1
- package/dist/contracts.d.ts +53 -0
- package/dist/contracts.js +68 -0
- package/dist/contracts.js.map +1 -0
- package/dist/delivery.d.ts +80 -0
- package/dist/delivery.js +304 -0
- package/dist/delivery.js.map +1 -0
- package/dist/findings.d.ts +12 -0
- package/dist/findings.js +41 -0
- package/dist/findings.js.map +1 -0
- package/dist/help.d.ts +1 -0
- package/dist/help.js +53 -0
- package/dist/help.js.map +1 -0
- package/dist/index.js +370 -72
- package/dist/index.js.map +1 -1
- package/dist/integrations.d.ts +126 -0
- package/dist/integrations.js +173 -0
- package/dist/integrations.js.map +1 -0
- package/dist/legacy-compat.d.ts +45 -0
- package/dist/legacy-compat.js +149 -0
- package/dist/legacy-compat.js.map +1 -0
- package/dist/lifecycle.d.ts +50 -0
- package/dist/lifecycle.js +146 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/locator.d.ts +58 -0
- package/dist/locator.js +233 -0
- package/dist/locator.js.map +1 -0
- package/dist/mcp.d.ts +12 -2
- package/dist/mcp.js +194 -48
- package/dist/mcp.js.map +1 -1
- package/dist/modes.d.ts +37 -0
- package/dist/modes.js +32 -0
- package/dist/modes.js.map +1 -0
- package/dist/outbox.d.ts +9 -0
- package/dist/outbox.js +188 -0
- package/dist/outbox.js.map +1 -0
- package/dist/parser.d.ts +10 -0
- package/dist/parser.js +99 -0
- package/dist/parser.js.map +1 -0
- package/dist/pulse.d.ts +122 -0
- package/dist/pulse.js +412 -0
- package/dist/pulse.js.map +1 -0
- package/dist/release-manifest.d.ts +123 -0
- package/dist/release-manifest.js +226 -0
- package/dist/release-manifest.js.map +1 -0
- package/dist/release.d.ts +1 -1
- package/dist/response.d.ts +17 -0
- package/dist/response.js +15 -0
- package/dist/response.js.map +1 -1
- package/dist/rollout.d.ts +8 -0
- package/dist/rollout.js +27 -0
- package/dist/rollout.js.map +1 -0
- package/dist/runtime.d.ts +60 -0
- package/dist/runtime.js +276 -0
- package/dist/runtime.js.map +1 -0
- package/dist/secure-store.d.ts +36 -0
- package/dist/secure-store.js +282 -0
- package/dist/secure-store.js.map +1 -0
- package/dist/setup.d.ts +159 -0
- package/dist/setup.js +629 -0
- package/dist/setup.js.map +1 -0
- package/dist/state.d.ts +98 -0
- package/dist/state.js +226 -0
- package/dist/state.js.map +1 -0
- package/dist/status.d.ts +282 -0
- package/dist/status.js +213 -0
- package/dist/status.js.map +1 -0
- package/dist/store.d.ts +44 -2
- package/dist/store.js +222 -44
- package/dist/store.js.map +1 -1
- package/dist/update.d.ts +33 -1
- package/dist/update.js +88 -16
- package/dist/update.js.map +1 -1
- package/dist/write-journal.d.ts +71 -0
- package/dist/write-journal.js +175 -0
- package/dist/write-journal.js.map +1 -0
- package/package.json +7 -2
package/dist/setup.js
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { closeSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { VectleApiError, VectleClient } from './client.js';
|
|
5
|
+
import { consentPolicyDigest, renderAgentPolicy } from './agent-policy.js';
|
|
6
|
+
import { ensureLegacyOriginBinding, legacySource, writeLegacyCompatibility } from './legacy-compat.js';
|
|
7
|
+
import { chooseClient, integrationBindingDigest, loadIntegrationManifest, saveIntegrationManifest } from './integrations.js';
|
|
8
|
+
import { deleteInstallationCredential, loadConfig, readCredential, readInstallationCredential, readTransientCredential, saveConfig, storeInstallationCredential, storeTransientCredential, deleteTransientCredential } from './store.js';
|
|
9
|
+
import { availableSecureBackend, credentialAccountHandle, installationBackendName } from './secure-store.js';
|
|
10
|
+
import { atomicWriteJson, ensureStateRoot, issueLocalId } from './state.js';
|
|
11
|
+
import { canonicalStateRoot, legacyDefaultRoot, resolveSetupRoot, setupCommitPath, setupLockPath, setupStatePath, validateStateRoot, writeActiveRoot } from './locator.js';
|
|
12
|
+
import { durableRuntimeForSetup } from './runtime.js';
|
|
13
|
+
import { CLI_VERSION } from './constants.js';
|
|
14
|
+
import { compareVersions } from './release.js';
|
|
15
|
+
import { fetchReleaseManifest } from './release-manifest.js';
|
|
16
|
+
import { rolloutDecision } from './rollout.js';
|
|
17
|
+
import { createCodexAdapter } from './adapters/codex.js';
|
|
18
|
+
import { createClaudeCodeAdapter } from './adapters/claude.js';
|
|
19
|
+
import { createCursorAdapter } from './adapters/cursor.js';
|
|
20
|
+
import { createVsCodeAdapter } from './adapters/vscode.js';
|
|
21
|
+
export function setupCommandRequest(command, args) {
|
|
22
|
+
const option = (name) => {
|
|
23
|
+
const index = args.indexOf(name);
|
|
24
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
25
|
+
};
|
|
26
|
+
const positionals = args.slice(1).filter((item, index, values) => !item.startsWith('--') && (index === 0 || !values[index - 1]?.startsWith('--')));
|
|
27
|
+
const supported = new Set(['codex', 'claude_code', 'cursor', 'vscode', 'terminal', 'unknown', 'auto', 'all']);
|
|
28
|
+
const requested = command === 'connect' ? positionals[0] ?? option('--client') : option('--client');
|
|
29
|
+
if (command === 'connect' && (!requested || !['codex', 'claude_code', 'cursor', 'vscode'].includes(requested)))
|
|
30
|
+
throw new Error('connect_client_invalid');
|
|
31
|
+
return {
|
|
32
|
+
client: requested && supported.has(requested) ? requested : 'auto',
|
|
33
|
+
endpoint: option('--endpoint'),
|
|
34
|
+
requestedRoot: option('--state-root'),
|
|
35
|
+
allowCustomStateRoot: args.includes('--allow-custom-state-root'),
|
|
36
|
+
allowFileCredential: args.includes('--allow-file-credential'),
|
|
37
|
+
repair: command === 'connect' || args.includes('--repair'),
|
|
38
|
+
noOpen: args.includes('--no-open'),
|
|
39
|
+
deprecatedAlias: command === 'init' || command === 'connect',
|
|
40
|
+
legacyGlobal: command === 'init' && args.includes('--global'),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function adapterForClient(client, home) {
|
|
44
|
+
if (client === 'codex')
|
|
45
|
+
return createCodexAdapter({ home });
|
|
46
|
+
if (client === 'claude_code')
|
|
47
|
+
return createClaudeCodeAdapter({ home });
|
|
48
|
+
if (client === 'cursor')
|
|
49
|
+
return createCursorAdapter({ home });
|
|
50
|
+
if (client === 'vscode')
|
|
51
|
+
return createVsCodeAdapter({ home });
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const sleepDefault = (milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
55
|
+
function nowIso(now) { return new Date(now()).toISOString(); }
|
|
56
|
+
function setupCommitId() { return `vsc_${randomBytes(16).toString('base64url')}`; }
|
|
57
|
+
function safeRef(prefix) { return `${prefix}-${randomBytes(8).toString('hex')}`; }
|
|
58
|
+
function writeSetupState(state) {
|
|
59
|
+
ensureStateRoot(state.state_root);
|
|
60
|
+
atomicWriteJson(setupStatePath(state.state_root), state);
|
|
61
|
+
}
|
|
62
|
+
function readSetupState(root) {
|
|
63
|
+
try {
|
|
64
|
+
const value = JSON.parse(readFileSync(setupStatePath(root), 'utf8'));
|
|
65
|
+
if (value.schema_version !== 'setup-state-v1' || typeof value.setup_commit_id !== 'string' || !/^vsc_[A-Za-z0-9_-]{22}$/u.test(value.setup_commit_id) || typeof value.state_root !== 'string' || canonicalStateRoot(value.state_root) !== canonicalStateRoot(root) || typeof value.phase !== 'string' || typeof value.status !== 'string' || typeof value.client_family !== 'string' || !Array.isArray(value.completed_steps))
|
|
66
|
+
return null;
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function removeIfExists(path) { try {
|
|
74
|
+
unlinkSync(path);
|
|
75
|
+
}
|
|
76
|
+
catch { /* idempotent cleanup */ } }
|
|
77
|
+
function acquireLock(root, now) {
|
|
78
|
+
ensureStateRoot(root);
|
|
79
|
+
const path = setupLockPath(root);
|
|
80
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
81
|
+
try {
|
|
82
|
+
const fd = openSync(path, 'wx', 0o600);
|
|
83
|
+
writeFileSync(fd, JSON.stringify({ schema_version: 'setup-lock-v1', pid: process.pid, started_at: nowIso(now) }));
|
|
84
|
+
closeSync(fd);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
try {
|
|
89
|
+
const age = now() - statSync(path).mtimeMs;
|
|
90
|
+
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
91
|
+
let live = false;
|
|
92
|
+
if (typeof raw.pid === 'number' && raw.pid > 0 && raw.pid !== process.pid) {
|
|
93
|
+
try {
|
|
94
|
+
process.kill(raw.pid, 0);
|
|
95
|
+
live = true;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
live = false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!live && age > 60_000)
|
|
102
|
+
removeIfExists(path);
|
|
103
|
+
}
|
|
104
|
+
catch { /* a concurrent setup owns the lock */ }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
throw new Error('setup_in_progress');
|
|
108
|
+
}
|
|
109
|
+
function writeCommit(root, commitId, generation, phase) {
|
|
110
|
+
atomicWriteJson(setupCommitPath(root), { schema_version: 'setup-commit-v1', setup_commit_id: commitId, generation, phase, state_root: canonicalStateRoot(root), targets: [] });
|
|
111
|
+
}
|
|
112
|
+
function safeError(error) {
|
|
113
|
+
if (error instanceof VectleApiError && error.body && typeof error.body === 'object' && typeof error.body.error === 'string') {
|
|
114
|
+
const code = String(error.body.error);
|
|
115
|
+
return /^[A-Za-z0-9_.-]{1,80}$/u.test(code) ? code : 'setup_failed';
|
|
116
|
+
}
|
|
117
|
+
if (error instanceof Error && /^[A-Za-z0-9_.-]{1,80}$/u.test(error.message))
|
|
118
|
+
return error.message;
|
|
119
|
+
return 'setup_failed';
|
|
120
|
+
}
|
|
121
|
+
function validAuthorized(value) {
|
|
122
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
123
|
+
return false;
|
|
124
|
+
const record = value;
|
|
125
|
+
const installation = record.installation;
|
|
126
|
+
const setup = record.setup;
|
|
127
|
+
return record.schema_version === 'installation-token-authorized-v2'
|
|
128
|
+
&& record.status === 'authorized'
|
|
129
|
+
&& installation !== null && typeof installation === 'object'
|
|
130
|
+
&& setup !== null && typeof setup === 'object'
|
|
131
|
+
&& typeof setup.setup_attempt_id === 'string'
|
|
132
|
+
&& /^vsa_[A-Za-z0-9_-]{22}$/u.test(String(setup.setup_attempt_id))
|
|
133
|
+
&& typeof setup.setup_report_token === 'string'
|
|
134
|
+
&& /^vctis_[A-Za-z0-9_-]{43}$/u.test(String(setup.setup_report_token));
|
|
135
|
+
}
|
|
136
|
+
function safeChecks(overrides = {}) {
|
|
137
|
+
const codes = ['runtime_durable', 'credential_stored', 'mcp_configured', 'policy_installed', 'approval_persistence', 'lifecycle_hook_configured', 'lifecycle_marker', 'server_smoke', 'client_smoke'];
|
|
138
|
+
return codes.map(code => ({ code, status: overrides[code] ?? (['mcp_configured', 'policy_installed', 'approval_persistence', 'lifecycle_hook_configured', 'lifecycle_marker'].includes(code) ? 'unsupported' : 'passed') }));
|
|
139
|
+
}
|
|
140
|
+
function projection(client, capability, checks, actions = [], limitations = [], serverHealthy = true) {
|
|
141
|
+
const full = capability === 'full_lifecycle';
|
|
142
|
+
const read = full ? 'automatic' : 'on_demand';
|
|
143
|
+
const share = full ? 'automatic_guard' : capability === 'disconnected' || capability === 'conflict' ? 'off' : 'explicit_only';
|
|
144
|
+
return {
|
|
145
|
+
client_family: client,
|
|
146
|
+
integration_capability: capability,
|
|
147
|
+
startup_invocation_policy: full ? 'automatic_guard' : capability === 'disconnected' || capability === 'conflict' ? 'off' : 'on_demand',
|
|
148
|
+
sharing_invocation_policy: share,
|
|
149
|
+
lookup_capability: serverHealthy ? read : 'unavailable',
|
|
150
|
+
updates_capability: serverHealthy ? (full ? 'automatic' : 'on_demand') : 'unavailable',
|
|
151
|
+
submission_capability: capability === 'disconnected' || capability === 'conflict' ? 'unavailable' : 'explicit_only',
|
|
152
|
+
correction_capability: capability === 'disconnected' || capability === 'conflict' ? 'unavailable' : 'explicit_only',
|
|
153
|
+
approval_read: full ? 'promptless' : capability === 'authorized_only' ? 'unavailable' : 'may_prompt',
|
|
154
|
+
approval_write: full ? 'promptless' : capability === 'authorized_only' ? 'unavailable' : 'may_prompt',
|
|
155
|
+
runtime_health: serverHealthy ? 'healthy' : 'server_unavailable',
|
|
156
|
+
actions_required: actions,
|
|
157
|
+
limitations,
|
|
158
|
+
checks,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function terminalProjection(client, serverHealthy) {
|
|
162
|
+
return projection(client, 'authorized_only', safeChecks({ mcp_configured: 'unsupported', policy_installed: 'unsupported', approval_persistence: 'unsupported', lifecycle_hook_configured: 'unsupported', lifecycle_marker: 'unsupported', client_smoke: serverHealthy ? 'passed' : 'failed', server_smoke: serverHealthy ? 'passed' : 'failed' }), [], ['lifecycle_unsupported'], serverHealthy);
|
|
163
|
+
}
|
|
164
|
+
function setupStatusFor(projectionValue) {
|
|
165
|
+
if (projectionValue.runtime_health === 'server_unavailable')
|
|
166
|
+
return 'failed';
|
|
167
|
+
if (projectionValue.actions_required.some(action => action === 'mcp_server_trust'))
|
|
168
|
+
return 'host_trust_required';
|
|
169
|
+
if (projectionValue.actions_required.some(action => action === 'lifecycle_hook_trust'))
|
|
170
|
+
return 'host_trust_required';
|
|
171
|
+
if (projectionValue.actions_required.some(action => action === 'client_restart'))
|
|
172
|
+
return 'ready_after_restart';
|
|
173
|
+
if (projectionValue.integration_capability === 'full_lifecycle' && projectionValue.checks.some(check => check.code === 'lifecycle_marker' && check.status === 'passed'))
|
|
174
|
+
return 'ready';
|
|
175
|
+
if (projectionValue.integration_capability === 'authorized_only' && projectionValue.client_family === 'terminal')
|
|
176
|
+
return 'ready';
|
|
177
|
+
if (projectionValue.limitations.length && projectionValue.actions_required.length === 0 && projectionValue.integration_capability !== 'conflict')
|
|
178
|
+
return 'partial';
|
|
179
|
+
return 'partial';
|
|
180
|
+
}
|
|
181
|
+
function result(stateRoot, selection, status, reasonCode, client, projectionValue, challenge) {
|
|
182
|
+
const actions = projectionValue?.actions_required ?? [];
|
|
183
|
+
const trustTargets = [actions.includes('mcp_server_trust') ? 'the Vectle MCP server' : null, actions.includes('lifecycle_hook_trust') ? 'the Vectle SessionStart hook' : null].filter(Boolean).join(' and ');
|
|
184
|
+
const action = actions.includes('repair')
|
|
185
|
+
? `vectle setup${client && client !== 'unknown' ? ` --client ${client}` : ''} --repair`
|
|
186
|
+
: trustTargets
|
|
187
|
+
? `Approve ${trustTargets} in ${client ?? 'the client'}${actions.includes('client_restart') ? ', then restart it and start a new session' : ''}.`
|
|
188
|
+
: actions.includes('client_restart')
|
|
189
|
+
? `Restart ${client ?? 'the client'} and start a new session.`
|
|
190
|
+
: null;
|
|
191
|
+
return { schema_version: 'setup-result-v1', status, reason_code: reasonCode, state_root: stateRoot, client_family: client, selection, projection: projectionValue ? (({ checks: _checks, ...safe }) => safe)(projectionValue) : null, verification_uri_complete: challenge?.verification_uri_complete ?? null, user_code: challenge?.user_code ?? null, challenge_expires_at: challenge?.expires_at ?? null, next_action: reasonCode === 'client_choice_required' ? 'vectle setup --client <codex|claude_code|cursor|vscode|terminal>' : reasonCode ? 'vectle setup --repair' : action };
|
|
192
|
+
}
|
|
193
|
+
export function canReplaceExpiredChallenge(input) {
|
|
194
|
+
return input.serverStatus === 'expired' && !input.browserPreviewed;
|
|
195
|
+
}
|
|
196
|
+
async function serverSmoke(client, config, token) {
|
|
197
|
+
try {
|
|
198
|
+
await client.anonymousCapabilities(1_000, 'explicit');
|
|
199
|
+
if (!token || config.schema_version !== 'vectle-config-v2')
|
|
200
|
+
return true;
|
|
201
|
+
const session = await client.issueInstallationSession(token);
|
|
202
|
+
const access = session && typeof session === 'object' && typeof session.access_token === 'string' ? String(session.access_token) : null;
|
|
203
|
+
if (!access)
|
|
204
|
+
return false;
|
|
205
|
+
await new VectleClient(config, access).capabilities(1_000);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function verifyReleaseBeforeProvisioning(endpoint, stateRoot) {
|
|
213
|
+
const manifestVersion = compareVersions(CLI_VERSION, '1.1.0');
|
|
214
|
+
if (manifestVersion === null || manifestVersion < 0)
|
|
215
|
+
return null;
|
|
216
|
+
const result = await fetchReleaseManifest(endpoint, stateRoot);
|
|
217
|
+
if (!result.envelope)
|
|
218
|
+
throw new Error('release_manifest_unavailable');
|
|
219
|
+
const manifest = result.envelope.manifest;
|
|
220
|
+
const minimum = compareVersions(CLI_VERSION, manifest.minimum_supported_cli);
|
|
221
|
+
if (minimum === null || minimum < 0)
|
|
222
|
+
throw new Error('cli_version_unsupported');
|
|
223
|
+
if (manifest.blocked_artifacts.some(item => item.artifact_kind === 'cli_package' && item.version === CLI_VERSION && item.platform === 'any' && item.architecture === 'any'))
|
|
224
|
+
throw new Error('release_artifact_blocked');
|
|
225
|
+
const artifact = manifest.artifacts.find(item => item.artifact_kind === 'cli_package' && item.version === CLI_VERSION && item.platform === 'any' && item.architecture === 'any');
|
|
226
|
+
if (!artifact)
|
|
227
|
+
throw new Error('release_artifact_unlisted');
|
|
228
|
+
return manifest;
|
|
229
|
+
}
|
|
230
|
+
const CLIENT_ROLLOUT_FEATURE = {
|
|
231
|
+
codex: 'client_codex_lifecycle',
|
|
232
|
+
claude_code: 'client_claude_lifecycle',
|
|
233
|
+
cursor: 'client_cursor_lifecycle',
|
|
234
|
+
vscode: 'client_vscode_lifecycle',
|
|
235
|
+
};
|
|
236
|
+
const CAPABILITY_RANK = { authorized_only: 0, mcp_only: 1, policy_and_mcp: 2, full_lifecycle: 3 };
|
|
237
|
+
const ADAPTER_CONFIG_SCHEMA_VERSION = 1;
|
|
238
|
+
function installedCapabilityRank(capability) {
|
|
239
|
+
return capability in CAPABILITY_RANK ? CAPABILITY_RANK[capability] : -1;
|
|
240
|
+
}
|
|
241
|
+
export function shouldPreserveInstalledCapability(existing, decision) {
|
|
242
|
+
if (!existing || ['platform_unsupported', 'adapter_record_missing', 'adapter_record_ambiguous', 'client_version_unsupported'].includes(decision.reason))
|
|
243
|
+
return false;
|
|
244
|
+
return installedCapabilityRank(existing.integration_capability) > CAPABILITY_RANK[decision.capability];
|
|
245
|
+
}
|
|
246
|
+
function clientVersionInRange(version, minimum, maximum) {
|
|
247
|
+
const minimumResult = compareVersions(version, minimum);
|
|
248
|
+
const maximumResult = maximum ? compareVersions(version, maximum) : 0;
|
|
249
|
+
return minimumResult !== null && minimumResult >= 0 && (maximum === null || maximumResult !== null && maximumResult <= 0);
|
|
250
|
+
}
|
|
251
|
+
export function detectClientVersion(client) {
|
|
252
|
+
const command = { codex: 'codex', claude_code: 'claude', cursor: 'cursor', vscode: 'code' }[client];
|
|
253
|
+
try {
|
|
254
|
+
const output = execFileSync(command, ['--version'], { encoding: 'utf8', timeout: 1_000, maxBuffer: 4_096, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
255
|
+
return parseClientVersion(output);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
export function parseClientVersion(output) {
|
|
262
|
+
const match = /(?:^|[^0-9A-Za-z.+-])((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)(?=$|[^0-9A-Za-z.+-])/u.exec(output.trim());
|
|
263
|
+
return match?.[1] ?? null;
|
|
264
|
+
}
|
|
265
|
+
export function adapterProvisioningDecision(input) {
|
|
266
|
+
const platform = input.platform ?? process.platform;
|
|
267
|
+
if (platform !== 'darwin' && platform !== 'linux' && platform !== 'win32')
|
|
268
|
+
return { capability: 'authorized_only', lifecycleEligible: false, reason: 'platform_unsupported' };
|
|
269
|
+
const platformRecords = input.manifest.adapters.filter(item => item.client_family === input.client && item.platform === platform && item.config_schema_version === ADAPTER_CONFIG_SCHEMA_VERSION);
|
|
270
|
+
const verifiedVersion = typeof input.clientVersion === 'string' && compareVersions(input.clientVersion, input.clientVersion) === 0 ? input.clientVersion : null;
|
|
271
|
+
if (platformRecords.length === 0)
|
|
272
|
+
return { capability: 'authorized_only', lifecycleEligible: false, reason: 'adapter_record_missing' };
|
|
273
|
+
const records = verifiedVersion ? platformRecords.filter(item => clientVersionInRange(verifiedVersion, item.minimum_client_version, item.maximum_client_version)) : platformRecords;
|
|
274
|
+
if (records.length === 0)
|
|
275
|
+
return { capability: 'authorized_only', lifecycleEligible: false, reason: 'client_version_unsupported' };
|
|
276
|
+
if (records.length !== 1)
|
|
277
|
+
return { capability: 'authorized_only', lifecycleEligible: false, reason: 'adapter_record_ambiguous' };
|
|
278
|
+
const record = records[0];
|
|
279
|
+
const lifecycleEligible = verifiedVersion !== null && rolloutDecision(input.config, CLIENT_ROLLOUT_FEATURE[input.client], input.manifest.rollouts[CLIENT_ROLLOUT_FEATURE[input.client]]);
|
|
280
|
+
const rolloutCeiling = lifecycleEligible ? 'full_lifecycle' : 'policy_and_mcp';
|
|
281
|
+
const capability = CAPABILITY_RANK[record.maximum_capability] <= CAPABILITY_RANK[rolloutCeiling] ? record.maximum_capability : rolloutCeiling;
|
|
282
|
+
return { capability, lifecycleEligible, reason: verifiedVersion === null ? 'client_version_unverified' : capability === record.maximum_capability ? 'manifest_capability' : 'lifecycle_rollout_ineligible' };
|
|
283
|
+
}
|
|
284
|
+
export async function runSetup(options = {}) {
|
|
285
|
+
const now = options.now ?? Date.now;
|
|
286
|
+
const config = { ...loadConfig(), ...(options.config ?? {}) };
|
|
287
|
+
const rootResolution = resolveSetupRoot({
|
|
288
|
+
requestedRoot: options.requestedRoot,
|
|
289
|
+
allowCustomStateRoot: options.allowCustomStateRoot || options.repair,
|
|
290
|
+
accountHome: options.home,
|
|
291
|
+
// VECTLE_CONFIG_DIR is a legacy migration candidate only when the loaded
|
|
292
|
+
// config actually came from that legacy root. It never silently selects a
|
|
293
|
+
// fresh v2 identity.
|
|
294
|
+
legacyRoot: config.config_source === 'legacy_v1' ? legacyDefaultRoot() : undefined,
|
|
295
|
+
});
|
|
296
|
+
const stateRoot = validateStateRoot(rootResolution.stateRoot, { allowCreate: true });
|
|
297
|
+
const selectionResult = chooseClient({ explicit: options.client ?? 'auto', trustedInvocation: options.trustedInvocation, home: options.home, manifest: loadIntegrationManifest(stateRoot) });
|
|
298
|
+
if (selectionResult.selection.status === 'choice_required')
|
|
299
|
+
return result(stateRoot, selectionResult.selection, 'client_choice_required', 'client_choice_required', null, null);
|
|
300
|
+
const requestedClient = options.client;
|
|
301
|
+
const selectedClient = selectionResult.selected?.client ?? (requestedClient === 'terminal' || requestedClient === 'unknown' ? requestedClient : 'terminal');
|
|
302
|
+
const profileLabel = selectionResult.selected?.profileLabel ?? 'default';
|
|
303
|
+
const adapter = options.adapter ?? adapterForClient(selectedClient, options.home);
|
|
304
|
+
const initialState = readSetupState(stateRoot);
|
|
305
|
+
acquireLock(stateRoot, now);
|
|
306
|
+
const resumableState = initialState && (initialState.status === 'in_progress'
|
|
307
|
+
|| (options.repair && initialState.status === 'failed' && Boolean(initialState.challenge_id && initialState.device_code_ref && initialState.refresh_candidate_ref))) ? initialState : null;
|
|
308
|
+
let state = resumableState ?? { schema_version: 'setup-state-v1', setup_commit_id: setupCommitId(), generation: (initialState?.generation ?? 0) + 1, state_root: stateRoot, phase: 'inspect', status: 'in_progress', client_family: selectedClient, profile_label: profileLabel, challenge_id: null, device_code_ref: null, refresh_candidate_ref: null, setup_attempt_id: null, setup_report_ref: null, completed_steps: [], error_code: null, started_at: nowIso(now), updated_at: nowIso(now) };
|
|
309
|
+
const previousToken = readCredential();
|
|
310
|
+
const previousManifest = loadIntegrationManifest(stateRoot);
|
|
311
|
+
let credentialStored = false;
|
|
312
|
+
let credentialAttempted = false;
|
|
313
|
+
let configCommitted = false;
|
|
314
|
+
let manifestWritten = false;
|
|
315
|
+
let storedConfig = null;
|
|
316
|
+
let appliedEntry = null;
|
|
317
|
+
try {
|
|
318
|
+
writeSetupState(state);
|
|
319
|
+
writeCommit(stateRoot, state.setup_commit_id, state.generation, 'preparing');
|
|
320
|
+
const apiConfig = { ...config, endpoint: (options.endpoint ?? config.endpoint).replace(/\/$/u, ''), clientFamily: selectedClient, stateRoot };
|
|
321
|
+
// Resolve signed release eligibility before creating or consuming an
|
|
322
|
+
// authorization challenge. A release-control failure must not mint a
|
|
323
|
+
// credential that setup subsequently rolls back.
|
|
324
|
+
const releaseManifest = await verifyReleaseBeforeProvisioning(apiConfig.endpoint, stateRoot);
|
|
325
|
+
const setupRolloutSeed = config.rollout_seed ?? `seed-${randomBytes(12).toString('hex')}`;
|
|
326
|
+
const supportedClient = ['codex', 'claude_code', 'cursor', 'vscode'].includes(selectedClient) ? selectedClient : null;
|
|
327
|
+
const clientVersion = supportedClient ? (options.clientVersion === undefined ? detectClientVersion(supportedClient) : options.clientVersion) : null;
|
|
328
|
+
const provisioningDecision = releaseManifest && supportedClient
|
|
329
|
+
? adapterProvisioningDecision({ manifest: releaseManifest, config: { rollout_seed: setupRolloutSeed }, client: supportedClient, clientVersion })
|
|
330
|
+
: null;
|
|
331
|
+
if (provisioningDecision && ['platform_unsupported', 'adapter_record_missing', 'adapter_record_ambiguous', 'client_version_unsupported'].includes(provisioningDecision.reason))
|
|
332
|
+
throw new Error(provisioningDecision.reason);
|
|
333
|
+
const client = options.clientApi ?? new VectleClient(apiConfig, readCredential());
|
|
334
|
+
const allowFile = Boolean(options.allowFileCredential) || process.env.VECTLE_ALLOW_FILE_CREDENTIAL === '1';
|
|
335
|
+
let currentToken = readCredential();
|
|
336
|
+
const legacyMigration = Boolean(currentToken && /^vctk_[A-Za-z0-9_-]{43}$/u.test(currentToken) && config.schema_version !== 'vectle-config-v2');
|
|
337
|
+
if (legacyMigration && currentToken) {
|
|
338
|
+
// A legacy credential is never sent until its exact source, selected
|
|
339
|
+
// legacy root, and compiled official origin have been pinned locally.
|
|
340
|
+
ensureLegacyOriginBinding({ token: currentToken, source: legacySource(config.credentialBackend), endpoint: apiConfig.endpoint });
|
|
341
|
+
writeLegacyCompatibility(config, 'legacy_v1');
|
|
342
|
+
}
|
|
343
|
+
let authorized = null;
|
|
344
|
+
let challengeForOutput;
|
|
345
|
+
state.phase = 'install_or_upgrade';
|
|
346
|
+
state.updated_at = nowIso(now);
|
|
347
|
+
writeSetupState(state);
|
|
348
|
+
const v2Installation = config.schema_version === 'vectle-config-v2' && config.installation ? config.installation : null;
|
|
349
|
+
if (v2Installation && currentToken && !options.repair) {
|
|
350
|
+
// A healthy v2 installation can be repaired/configured without asking
|
|
351
|
+
// for consent again. The adapter is the only part that may mutate host
|
|
352
|
+
// configuration, and it receives the frozen state root below.
|
|
353
|
+
authorized = { installation: v2Installation, setup: { setup_attempt_id: `vso_${randomBytes(16).toString('base64url')}`, setup_report_token: '', expires_at: new Date(now() + 30 * 60_000).toISOString() } };
|
|
354
|
+
}
|
|
355
|
+
else if (v2Installation && currentToken && options.repair) {
|
|
356
|
+
const operationId = issueLocalId('vai').replace(/^vai_/u, 'vso_');
|
|
357
|
+
const ref = safeRef('setup-report');
|
|
358
|
+
const candidate = `vctis_${randomBytes(32).toString('base64url')}`;
|
|
359
|
+
storeTransientCredential(candidate, apiConfig, 'setup_report', ref, new Date(now() + 30 * 60_000).toISOString(), allowFile);
|
|
360
|
+
state.phase = 'challenge';
|
|
361
|
+
state.setup_report_ref = ref;
|
|
362
|
+
state.updated_at = nowIso(now);
|
|
363
|
+
writeSetupState(state);
|
|
364
|
+
const attempt = await client.beginInstallationSetupAttempt({ setup_operation_id: operationId, setup_report_candidate: candidate, client_family: selectedClient });
|
|
365
|
+
if (typeof attempt.setup_attempt_id !== 'string')
|
|
366
|
+
throw new Error('setup_attempt_unavailable');
|
|
367
|
+
state.setup_attempt_id = attempt.setup_attempt_id;
|
|
368
|
+
state.phase = 'apply_client_config';
|
|
369
|
+
state.updated_at = nowIso(now);
|
|
370
|
+
writeSetupState(state);
|
|
371
|
+
authorized = { installation: v2Installation, setup: { setup_attempt_id: attempt.setup_attempt_id, setup_report_token: candidate, expires_at: String(attempt.expires_at ?? new Date(now() + 30 * 60_000).toISOString()) } };
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
const challengeKind = v2Installation ? 'reconnect' : config.contributorId && currentToken ? 'upgrade' : 'fresh';
|
|
375
|
+
let deviceRef = state.device_code_ref;
|
|
376
|
+
let candidateRef = state.refresh_candidate_ref;
|
|
377
|
+
let deadline = now() + 30 * 60_000;
|
|
378
|
+
if (!state.challenge_id || !deviceRef || !candidateRef) {
|
|
379
|
+
const refreshCandidate = `vctir_${randomBytes(32).toString('base64url')}`;
|
|
380
|
+
candidateRef = safeRef('refresh');
|
|
381
|
+
storeTransientCredential(refreshCandidate, apiConfig, 'refresh', candidateRef, new Date(now() + 30 * 60_000).toISOString(), allowFile);
|
|
382
|
+
state.refresh_candidate_ref = candidateRef;
|
|
383
|
+
state.phase = 'challenge';
|
|
384
|
+
state.updated_at = nowIso(now);
|
|
385
|
+
writeSetupState(state);
|
|
386
|
+
const challenge = await client.createInstallationChallengeV2({ challenge_kind: challengeKind, client_family: selectedClient, consent_policy_digest: consentPolicyDigest(), automatic_lookup: true, automatic_sharing: true });
|
|
387
|
+
const deviceCode = typeof challenge.device_code === 'string' ? challenge.device_code : null;
|
|
388
|
+
const challengeId = typeof challenge.challenge_id === 'string' ? challenge.challenge_id : null;
|
|
389
|
+
if (!deviceCode || !challengeId)
|
|
390
|
+
throw new Error('challenge_unavailable');
|
|
391
|
+
deviceRef = safeRef('device');
|
|
392
|
+
storeTransientCredential(deviceCode, apiConfig, 'device_code', deviceRef, String(challenge.expires_at), allowFile);
|
|
393
|
+
state.challenge_id = challengeId;
|
|
394
|
+
state.device_code_ref = deviceRef;
|
|
395
|
+
state.phase = 'wait_for_approval';
|
|
396
|
+
state.updated_at = nowIso(now);
|
|
397
|
+
writeSetupState(state);
|
|
398
|
+
challengeForOutput = { verification_uri_complete: typeof challenge.verification_uri_complete === 'string' ? challenge.verification_uri_complete : undefined, user_code: typeof challenge.user_code === 'string' ? challenge.user_code : undefined, expires_at: typeof challenge.expires_at === 'string' ? challenge.expires_at : undefined };
|
|
399
|
+
if (challengeForOutput.verification_uri_complete && !options.noOpen)
|
|
400
|
+
options.openBrowser?.(challengeForOutput.verification_uri_complete);
|
|
401
|
+
deadline = Date.parse(String(challenge.expires_at));
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
state.phase = 'wait_for_approval';
|
|
405
|
+
state.updated_at = nowIso(now);
|
|
406
|
+
writeSetupState(state);
|
|
407
|
+
}
|
|
408
|
+
const wait = options.sleep ?? sleepDefault;
|
|
409
|
+
while (now() < deadline) {
|
|
410
|
+
await wait(Math.max(2_000, Math.min(10_000, 5_000)));
|
|
411
|
+
const device = readTransientCredential(apiConfig, 'device_code', deviceRef);
|
|
412
|
+
const candidate = readTransientCredential(apiConfig, 'refresh', candidateRef);
|
|
413
|
+
if (!device || !candidate)
|
|
414
|
+
throw new Error('setup_secret_unavailable');
|
|
415
|
+
const polled = await client.pollInstallationChallengeV2(device, candidate);
|
|
416
|
+
if (polled.status === 'authorization_pending' || polled.status === 'authorization_processing') {
|
|
417
|
+
if (typeof polled.expires_at === 'string' && Number.isFinite(Date.parse(polled.expires_at)))
|
|
418
|
+
deadline = Date.parse(polled.expires_at);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (!validAuthorized(polled)) {
|
|
422
|
+
const serverStatus = typeof polled.status === 'string' ? polled.status : 'invalid_device_code';
|
|
423
|
+
throw new Error(serverStatus === 'setup_declined' ? 'setup_declined' : serverStatus === 'expired' ? 'challenge_expired' : 'challenge_consume_conflict');
|
|
424
|
+
}
|
|
425
|
+
authorized = { installation: polled.installation, setup: polled.setup };
|
|
426
|
+
state.setup_attempt_id = polled.setup.setup_attempt_id;
|
|
427
|
+
state.setup_report_ref = safeRef('setup-report');
|
|
428
|
+
storeTransientCredential(polled.setup.setup_report_token, apiConfig, 'setup_report', state.setup_report_ref, polled.setup.expires_at, allowFile);
|
|
429
|
+
deleteTransientCredential(apiConfig, 'device_code', deviceRef);
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
if (!authorized)
|
|
433
|
+
throw new Error('challenge_expired');
|
|
434
|
+
}
|
|
435
|
+
if (!authorized)
|
|
436
|
+
throw new Error('setup_authorization_missing');
|
|
437
|
+
state.phase = 'store_credential';
|
|
438
|
+
state.updated_at = nowIso(now);
|
|
439
|
+
writeSetupState(state);
|
|
440
|
+
writeCommit(stateRoot, state.setup_commit_id, state.generation, 'applying');
|
|
441
|
+
const pendingRefresh = state.refresh_candidate_ref ? readTransientCredential(apiConfig, 'refresh', state.refresh_candidate_ref) : null;
|
|
442
|
+
const serverInstallation = authorized.installation;
|
|
443
|
+
const supportCode = typeof serverInstallation.support_code === 'string' ? serverInstallation.support_code : '';
|
|
444
|
+
const approvedOrigin = typeof serverInstallation.approved_api_origin === 'string' ? serverInstallation.approved_api_origin : apiConfig.endpoint;
|
|
445
|
+
if (!/^vci_[A-Za-z0-9_-]{22}$/u.test(supportCode))
|
|
446
|
+
throw new Error('invalid_installation_binding');
|
|
447
|
+
const accountHandle = credentialAccountHandle(approvedOrigin, supportCode);
|
|
448
|
+
const selectedBackend = pendingRefresh
|
|
449
|
+
? availableSecureBackend({ allowFileFallback: allowFile })
|
|
450
|
+
: null;
|
|
451
|
+
if (pendingRefresh && (!selectedBackend || selectedBackend === 'environment'))
|
|
452
|
+
throw new Error('secure_store_unavailable');
|
|
453
|
+
const installationBackend = v2Installation?.credential_backend ?? (selectedBackend ? installationBackendName(selectedBackend) : 'file');
|
|
454
|
+
const installation = {
|
|
455
|
+
support_code: supportCode,
|
|
456
|
+
approved_api_origin: approvedOrigin,
|
|
457
|
+
credential_backend: installationBackend,
|
|
458
|
+
credential_account_handle: accountHandle,
|
|
459
|
+
consent_version: 2,
|
|
460
|
+
consented_scopes: Array.isArray(serverInstallation.consented_scopes) ? [...serverInstallation.consented_scopes].sort() : [],
|
|
461
|
+
consent_policy_digest: typeof serverInstallation.consent_policy_digest === 'string' ? serverInstallation.consent_policy_digest : '',
|
|
462
|
+
approved_at: typeof serverInstallation.approved_at === 'string' ? serverInstallation.approved_at : '',
|
|
463
|
+
automatic_lookup: serverInstallation.automatic_lookup === true,
|
|
464
|
+
automatic_sharing: serverInstallation.automatic_sharing === true,
|
|
465
|
+
};
|
|
466
|
+
if (installation.consented_scopes.length !== 6 || !/^sha256:[0-9a-f]{64}$/u.test(installation.consent_policy_digest) || !Number.isFinite(Date.parse(installation.approved_at)))
|
|
467
|
+
throw new Error('invalid_installation_binding');
|
|
468
|
+
const nextConfig = { ...apiConfig, endpoint: approvedOrigin, schema_version: 'vectle-config-v2', installation, mode: config.mode === 'paused' || config.mode === 'read-only' ? config.mode : 'active', rollout_seed: setupRolloutSeed, config_source: 'v2' };
|
|
469
|
+
storedConfig = nextConfig;
|
|
470
|
+
if (pendingRefresh) {
|
|
471
|
+
credentialAttempted = true;
|
|
472
|
+
const backend = storeInstallationCredential(pendingRefresh, nextConfig, allowFile);
|
|
473
|
+
nextConfig.installation = { ...nextConfig.installation, credential_backend: backend };
|
|
474
|
+
currentToken = readInstallationCredential(nextConfig);
|
|
475
|
+
if (!currentToken)
|
|
476
|
+
throw new Error('secure_store_round_trip_failed');
|
|
477
|
+
credentialStored = true;
|
|
478
|
+
}
|
|
479
|
+
state.completed_steps.push('credential_stored');
|
|
480
|
+
state.phase = 'apply_client_config';
|
|
481
|
+
state.updated_at = nowIso(now);
|
|
482
|
+
writeSetupState(state);
|
|
483
|
+
let entry = null;
|
|
484
|
+
const runtime = durableRuntimeForSetup(stateRoot);
|
|
485
|
+
let checks = safeChecks({ runtime_durable: runtime.descriptor.durable ? 'passed' : 'failed', mcp_configured: selectedClient === 'terminal' || selectedClient === 'unknown' ? 'unsupported' : 'pending', policy_installed: selectedClient === 'terminal' || selectedClient === 'unknown' ? 'unsupported' : 'pending', client_smoke: selectedClient === 'terminal' || selectedClient === 'unknown' ? 'passed' : 'pending' });
|
|
486
|
+
let capability = selectedClient === 'terminal' || selectedClient === 'unknown' ? 'authorized_only' : 'authorized_only';
|
|
487
|
+
let actions = [];
|
|
488
|
+
let limitations = selectedClient === 'terminal' || selectedClient === 'unknown' ? ['lifecycle_unsupported'] : runtime.descriptor.durable ? ['lifecycle_unsupported'] : ['runtime_not_durable'];
|
|
489
|
+
let releaseVersionUnverified = false;
|
|
490
|
+
if (adapter && runtime.descriptor.durable && supportedClient) {
|
|
491
|
+
const decision = provisioningDecision
|
|
492
|
+
?? (releaseManifest
|
|
493
|
+
? adapterProvisioningDecision({ manifest: releaseManifest, config: nextConfig, client: supportedClient, clientVersion })
|
|
494
|
+
: { capability: 'full_lifecycle', lifecycleEligible: true, reason: 'legacy_cli' });
|
|
495
|
+
releaseVersionUnverified = decision.reason === 'client_version_unverified';
|
|
496
|
+
const adapterEntryId = selectionResult.selected?.adapterEntryId ?? issueLocalId('vai');
|
|
497
|
+
const existingEntry = previousManifest.entries.find(item => item.adapter_entry_id === adapterEntryId && item.client_family === supportedClient) ?? null;
|
|
498
|
+
if (shouldPreserveInstalledCapability(existingEntry, decision)) {
|
|
499
|
+
// A lower threshold or signed capability ceiling prevents new installs
|
|
500
|
+
// and widening. It never removes an already installed integration.
|
|
501
|
+
entry = existingEntry;
|
|
502
|
+
}
|
|
503
|
+
else if (decision.capability !== 'authorized_only') {
|
|
504
|
+
const binding = integrationBindingDigest(nextConfig, adapterEntryId);
|
|
505
|
+
const policyChecksum = renderAgentPolicy({ mode: nextConfig.mode ?? 'active', clientFamily: selectedClient, integrationCapability: decision.capability, approvedBehaviorCeiling: { consentVersion: 2, startupPulse: nextConfig.installation?.automatic_lookup ? 'automatic' : 'on_demand', sharingAndCorrection: nextConfig.installation?.automatic_sharing ? 'automatic' : 'explicit_only' }, approvalPersistence: { read: 'promptless', write: 'promptless' }, tools: { startupPulse: true, shareFinding: true, correctFinding: true } }).checksum;
|
|
506
|
+
const applied = await adapter.apply({ config: nextConfig, stateRoot, adapterEntryId, integrationBindingDigest: binding, policyChecksum, targetCapability: decision.capability });
|
|
507
|
+
entry = applied.entry;
|
|
508
|
+
checks = applied.checks;
|
|
509
|
+
actions = applied.actions ?? [];
|
|
510
|
+
limitations = applied.limitations ?? [];
|
|
511
|
+
capability = applied.capability ?? 'mcp_only';
|
|
512
|
+
appliedEntry = entry;
|
|
513
|
+
state.completed_steps.push('client_configured');
|
|
514
|
+
if (decision.capability !== 'mcp_only')
|
|
515
|
+
state.completed_steps.push('policy_installed');
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (supportedClient && provisioningDecision?.capability === 'authorized_only' && !entry) {
|
|
519
|
+
checks = safeChecks({ runtime_durable: runtime.descriptor.durable ? 'passed' : 'failed', credential_stored: 'passed', mcp_configured: 'unsupported', policy_installed: 'unsupported', approval_persistence: 'unsupported', lifecycle_hook_configured: 'unsupported', lifecycle_marker: 'unsupported', client_smoke: 'passed' });
|
|
520
|
+
capability = 'authorized_only';
|
|
521
|
+
limitations = ['lifecycle_unsupported'];
|
|
522
|
+
}
|
|
523
|
+
state.phase = 'client_smoke_test';
|
|
524
|
+
state.updated_at = nowIso(now);
|
|
525
|
+
writeSetupState(state);
|
|
526
|
+
let verification = { clientSmoke: selectedClient === 'terminal' || selectedClient === 'unknown' || capability === 'authorized_only' ? 'passed' : 'failed', marker: 'unsupported', lifecycle: capability, actions: runtime.descriptor.durable ? [] : ['repair'], limitations };
|
|
527
|
+
if (adapter && entry)
|
|
528
|
+
verification = await adapter.verify({ config: nextConfig, entry });
|
|
529
|
+
capability = verification.lifecycle;
|
|
530
|
+
actions = verification.actions ?? actions;
|
|
531
|
+
limitations = verification.limitations ?? limitations;
|
|
532
|
+
if (releaseVersionUnverified && !limitations.includes('client_version_unverified'))
|
|
533
|
+
limitations.push('client_version_unverified');
|
|
534
|
+
checks = checks.map(check => check.code === 'client_smoke' ? { ...check, status: verification.clientSmoke } : check);
|
|
535
|
+
if (verification.marker !== 'unsupported')
|
|
536
|
+
checks = checks.map(check => check.code === 'lifecycle_marker' ? { ...check, status: verification.marker } : check);
|
|
537
|
+
const smokeClient = new VectleClient(nextConfig, currentToken ?? null);
|
|
538
|
+
state.phase = 'server_smoke_test';
|
|
539
|
+
state.updated_at = nowIso(now);
|
|
540
|
+
writeSetupState(state);
|
|
541
|
+
const serverHealthy = await serverSmoke(smokeClient, nextConfig, currentToken ?? pendingRefresh ?? null);
|
|
542
|
+
checks = checks.map(check => check.code === 'server_smoke' ? { ...check, status: serverHealthy ? 'passed' : 'failed' } : check);
|
|
543
|
+
const clientProjection = projection(selectedClient, capability, checks, actions, limitations, serverHealthy);
|
|
544
|
+
const status = setupStatusFor(clientProjection);
|
|
545
|
+
if (entry) {
|
|
546
|
+
const manifest = loadIntegrationManifest(stateRoot);
|
|
547
|
+
saveIntegrationManifest({ schema_version: 'integrations-v1', entries: [...manifest.entries.filter(item => item.adapter_entry_id !== entry.adapter_entry_id), entry] }, stateRoot);
|
|
548
|
+
manifestWritten = true;
|
|
549
|
+
}
|
|
550
|
+
state.phase = 'report_ready';
|
|
551
|
+
state.status = status;
|
|
552
|
+
state.updated_at = nowIso(now);
|
|
553
|
+
writeSetupState(state);
|
|
554
|
+
const reportRef = state.setup_report_ref;
|
|
555
|
+
const reportToken = reportRef ? readTransientCredential(apiConfig, 'setup_report', reportRef) : null;
|
|
556
|
+
if (reportToken && reportRef && state.setup_attempt_id) {
|
|
557
|
+
const report = { schema_version: 'installation-setup-report-v1', setup_attempt_id: state.setup_attempt_id, status, client: clientProjection };
|
|
558
|
+
const response = await smokeClient.reportInstallationSetup(reportToken, report);
|
|
559
|
+
if (!response || typeof response !== 'object')
|
|
560
|
+
throw new Error('setup_report_unavailable');
|
|
561
|
+
deleteTransientCredential(apiConfig, 'setup_report', reportRef);
|
|
562
|
+
}
|
|
563
|
+
if (state.refresh_candidate_ref)
|
|
564
|
+
deleteTransientCredential(apiConfig, 'refresh', state.refresh_candidate_ref);
|
|
565
|
+
if (legacyMigration)
|
|
566
|
+
writeLegacyCompatibility(config, 'upgraded_v2');
|
|
567
|
+
writeActiveRoot(stateRoot, { accountHome: options.home });
|
|
568
|
+
saveConfig(nextConfig);
|
|
569
|
+
configCommitted = true;
|
|
570
|
+
writeCommit(stateRoot, state.setup_commit_id, state.generation, 'committed');
|
|
571
|
+
state.phase = 'done';
|
|
572
|
+
state.status = status;
|
|
573
|
+
state.completed_steps.push('server_smoke', 'client_smoke', 'reported');
|
|
574
|
+
state.updated_at = nowIso(now);
|
|
575
|
+
writeSetupState(state);
|
|
576
|
+
removeIfExists(setupCommitPath(stateRoot));
|
|
577
|
+
removeIfExists(setupStatePath(stateRoot));
|
|
578
|
+
return result(stateRoot, selectionResult.selection, status, status === 'failed' ? 'service_unavailable' : null, selectedClient, clientProjection, challengeForOutput);
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
if (!configCommitted) {
|
|
582
|
+
if (adapter?.rollback && appliedEntry) {
|
|
583
|
+
try {
|
|
584
|
+
await adapter.rollback({ config: storedConfig ?? config, stateRoot, entry: appliedEntry });
|
|
585
|
+
}
|
|
586
|
+
catch { /* leave the safe repair state for the next explicit attempt */ }
|
|
587
|
+
}
|
|
588
|
+
if (manifestWritten) {
|
|
589
|
+
try {
|
|
590
|
+
saveIntegrationManifest(previousManifest, stateRoot);
|
|
591
|
+
}
|
|
592
|
+
catch { /* repair can reconcile the manifest */ }
|
|
593
|
+
}
|
|
594
|
+
if ((credentialStored || credentialAttempted) && storedConfig?.installation) {
|
|
595
|
+
try {
|
|
596
|
+
if (previousToken && config.schema_version === 'vectle-config-v2' && config.installation) {
|
|
597
|
+
storeInstallationCredential(previousToken, config, true);
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
deleteInstallationCredential(storedConfig);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
catch { /* never turn a setup error into a credential-handling crash */ }
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
state.phase = 'rollback';
|
|
607
|
+
state.status = 'failed';
|
|
608
|
+
state.error_code = safeError(error);
|
|
609
|
+
state.updated_at = nowIso(now);
|
|
610
|
+
try {
|
|
611
|
+
writeSetupState(state);
|
|
612
|
+
}
|
|
613
|
+
catch { /* preserve primary failure */ }
|
|
614
|
+
return result(stateRoot, selectionResult.selection, 'failed', state.error_code, selectedClient, null, undefined);
|
|
615
|
+
}
|
|
616
|
+
finally {
|
|
617
|
+
removeIfExists(setupLockPath(stateRoot));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
export function recoverSetup(root) {
|
|
621
|
+
const canonical = validateStateRoot(root, { allowCreate: false });
|
|
622
|
+
const state = readSetupState(canonical);
|
|
623
|
+
if (!state)
|
|
624
|
+
return { status: 'none', recovered: false };
|
|
625
|
+
removeIfExists(setupLockPath(canonical));
|
|
626
|
+
return { status: state.status, recovered: true, error_code: state.error_code };
|
|
627
|
+
}
|
|
628
|
+
export function setupStateForTests(root) { return readSetupState(root); }
|
|
629
|
+
//# sourceMappingURL=setup.js.map
|