agents-can-communicate 0.5.3 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/docs/CLI.md +7 -4
- package/docs/CONFIGURATION.md +7 -3
- package/docs/GETTING_STARTED.md +12 -3
- package/docs/TROUBLESHOOTING.md +7 -7
- package/docs/UPGRADING.md +7 -4
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +5 -2
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/live-permissions.mjs +37 -12
- package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/service-setup.mjs +135 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +10 -5
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +3 -0
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +15 -71
- package/node_modules/@agents-can-communicate/cli/src/install-delivery-consent.mjs +152 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +6 -1
- package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +22 -5
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +6 -2
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +11 -0
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +6 -1
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +10 -2
- package/node_modules/@agents-can-communicate/installer/src/service-setup.mjs +30 -0
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/package.json +1 -1
|
@@ -6,13 +6,16 @@ import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
|
|
|
6
6
|
import { createGeminiCliAdapter } from "@agents-can-communicate/adapter-gemini-cli";
|
|
7
7
|
import { createGrokAdapter } from "@agents-can-communicate/adapter-grok";
|
|
8
8
|
import { createKimiAdapter } from "@agents-can-communicate/adapter-kimi";
|
|
9
|
-
import {
|
|
9
|
+
import { applyPlan, detectInstallation,
|
|
10
10
|
loadOwnership, planInstallation, rcFileFor, shellOf, shimDirFor }
|
|
11
11
|
from "@agents-can-communicate/installer";
|
|
12
12
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
13
13
|
|
|
14
14
|
import { platformPaths } from "./platform-paths.mjs";
|
|
15
15
|
import { installManaged, uninstallManaged } from "./managed-runtime/install.mjs";
|
|
16
|
+
import { decideDelivery } from "./install-delivery-consent.mjs";
|
|
17
|
+
|
|
18
|
+
export { decideDelivery } from "./install-delivery-consent.mjs";
|
|
16
19
|
|
|
17
20
|
// Kept cohesive above 300 lines because this is the install command boundary:
|
|
18
21
|
// detection, consent, planning, application, and reporting share one context
|
|
@@ -180,10 +183,11 @@ export function describeOutcome({ action, acted, failed = [], skipped = [],
|
|
|
180
183
|
* what a malformed `~/.claude/settings.json` produced: the adapter refused,
|
|
181
184
|
* correctly, and the script that ran the installer was told it had worked.
|
|
182
185
|
*/
|
|
183
|
-
export function failureOf({ action, acted, failed = [] }) {
|
|
186
|
+
export function failureOf({ action, acted, failed = [], operations = [], skipped = [], home }) {
|
|
184
187
|
if (failed.length === 0) return null;
|
|
185
188
|
return new AccError(EXIT.DATA,
|
|
186
|
-
describeOutcome({ action, acted, failed
|
|
189
|
+
[describeOutcome({ action, acted, failed, operations, skipped, home }),
|
|
190
|
+
...reloadAdvice(operations)].join("\n"), { failed });
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
/**
|
|
@@ -202,70 +206,6 @@ export function actedOn(result) {
|
|
|
202
206
|
+ (operation.changes?.length ?? 0) > 0)).length;
|
|
203
207
|
}
|
|
204
208
|
|
|
205
|
-
const isInteractive = runtime => typeof runtime.isInteractive === "function"
|
|
206
|
-
&& runtime.isInteractive() === true;
|
|
207
|
-
|
|
208
|
-
// One default-No question per supported client. Disclose token use and any
|
|
209
|
-
// repeated channel prompt; declining only promises a verified fallback.
|
|
210
|
-
function questionFor(entry) {
|
|
211
|
-
const bootstrap = entry.nativeDelivery.activationPlan?.mechanisms
|
|
212
|
-
.find(mechanism => mechanism.kind === "shell-bootstrap") ?? null;
|
|
213
|
-
const warns = (bootstrap?.prefixArgs ?? []).some(argument => argument.startsWith("--"));
|
|
214
|
-
return [
|
|
215
|
-
`Let ${entry.displayName} answer peer requests while idle? (experimental)`,
|
|
216
|
-
" Yes: automatic turns can spend tokens without waiting for you.",
|
|
217
|
-
...(entry.outgoingDelivery?.setup ? [` ${entry.outgoingDelivery.setup}`] : []),
|
|
218
|
-
...(warns ? [" Allow development channels when prompted; check the client's startup notice."] : []),
|
|
219
|
-
...(entry.nativeDelivery.state !== "eligible"
|
|
220
|
-
? [" Save consent now; delivery waits for an available client service."] : []),
|
|
221
|
-
entry.capabilities?.delivery?.nextTurn === true
|
|
222
|
-
? " No: use next-turn hooks (when enabled) or acc inbox."
|
|
223
|
-
: " No: read messages with acc inbox; automatic next-turn delivery is unavailable.",
|
|
224
|
-
].join("\n");
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Which live policy each selected client gets.
|
|
229
|
-
*
|
|
230
|
-
* An explicit --delivery applies uniformly and never prompts. Otherwise a
|
|
231
|
-
* recorded opt-in is kept as it was consented to, a fresh or never-activated
|
|
232
|
-
* client is off unless a person answers yes here, and a dry run or a
|
|
233
|
-
* non-interactive stdin/stdout makes no interactive choice at all.
|
|
234
|
-
*/
|
|
235
|
-
export async function decideDelivery({ options, detected, recorded, runtime, dryRun }) {
|
|
236
|
-
const explicit = options.delivery;
|
|
237
|
-
if (explicit !== undefined && !LIVE_POLICIES.includes(explicit)) {
|
|
238
|
-
throw new AccError(EXIT.USAGE, `unknown delivery policy: ${explicit}`, { delivery: explicit });
|
|
239
|
-
}
|
|
240
|
-
const recordedById = new Map(recorded.map(install => [install.adapterId, install]));
|
|
241
|
-
const deliveryByAdapter = {};
|
|
242
|
-
const asked = [];
|
|
243
|
-
let withheld = 0;
|
|
244
|
-
for (const entry of detected) {
|
|
245
|
-
const eligible = entry.nativeDelivery?.state === "eligible"
|
|
246
|
-
|| entry.nativeDelivery?.consentAvailable === true;
|
|
247
|
-
const previous = livePolicyOf(recordedById.get(entry.adapterId));
|
|
248
|
-
if (explicit !== undefined) {
|
|
249
|
-
deliveryByAdapter[entry.adapterId] = explicit;
|
|
250
|
-
} else if (previous !== "off") {
|
|
251
|
-
deliveryByAdapter[entry.adapterId] = previous;
|
|
252
|
-
} else if (!eligible || dryRun || !isInteractive(runtime)) {
|
|
253
|
-
deliveryByAdapter[entry.adapterId] = "off";
|
|
254
|
-
if (eligible) withheld += 1;
|
|
255
|
-
} else {
|
|
256
|
-
const yes = await runtime.confirm(questionFor(entry),
|
|
257
|
-
{ input: runtime.input, output: runtime.output }) === true;
|
|
258
|
-
deliveryByAdapter[entry.adapterId] = yes ? "actionable" : "off";
|
|
259
|
-
asked.push(entry.adapterId);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
const notes = explicit === undefined && dryRun && withheld > 0
|
|
263
|
-
? ["interactive choices were not made: this preview keeps native delivery off for "
|
|
264
|
-
+ `${withheld} eligible client(s) without a recorded opt-in`]
|
|
265
|
-
: [];
|
|
266
|
-
return { deliveryByAdapter, asked, notes };
|
|
267
|
-
}
|
|
268
|
-
|
|
269
209
|
// Said once, after the first PATH block is written: a running shell and a
|
|
270
210
|
// running client know nothing about it.
|
|
271
211
|
function reloadAdvice(operations) {
|
|
@@ -315,7 +255,9 @@ export async function runInstallCommand({ options, runtime, action = "install" }
|
|
|
315
255
|
: { deliveryByAdapter: {}, asked: [], notes: [] };
|
|
316
256
|
const plan = planInstallation({ adapters, detected, context, action, recorded,
|
|
317
257
|
accVersion, allowDowngrade: options.downgrade === true, requested,
|
|
318
|
-
deliveryByAdapter: decided.deliveryByAdapter
|
|
258
|
+
deliveryByAdapter: decided.deliveryByAdapter,
|
|
259
|
+
deliveryDecisionByAdapter: decided.deliveryDecisionByAdapter,
|
|
260
|
+
allowServiceSetup: action === "install" });
|
|
319
261
|
const apply = (paths = {}) => applyPlan({ plan, adapters, context: { ...context, ...paths },
|
|
320
262
|
dataHome, dryRun, accVersion, activation: { bootstrap: paths.bootstrap } });
|
|
321
263
|
const result = action === "install" && !dryRun && runtime.packageRoot
|
|
@@ -328,16 +270,18 @@ export async function runInstallCommand({ options, runtime, action = "install" }
|
|
|
328
270
|
|
|
329
271
|
const acted = actedOn(result);
|
|
330
272
|
if (dryRun) {
|
|
331
|
-
return { data: { ...result, plan, dataHome, deliveryByAdapter: decided.deliveryByAdapter
|
|
273
|
+
return { data: { ...result, plan, dataHome, deliveryByAdapter: decided.deliveryByAdapter,
|
|
274
|
+
deliveryDecisionByAdapter: decided.deliveryDecisionByAdapter },
|
|
332
275
|
text: [`would ${action}:`, ...plan.operations.flatMap(operation => operation.summary),
|
|
333
276
|
...plan.skipped.map(entry => `skip ${entry.adapterId}: ${entry.reason}`),
|
|
334
277
|
...decided.notes].join("\n") };
|
|
335
278
|
}
|
|
336
279
|
|
|
337
280
|
return { data: { ...result, plan, dataHome, deliveryByAdapter: decided.deliveryByAdapter,
|
|
338
|
-
asked: decided.asked },
|
|
281
|
+
deliveryDecisionByAdapter: decided.deliveryDecisionByAdapter, asked: decided.asked },
|
|
339
282
|
text: [describeOutcome({ action, acted, failed: result.failed,
|
|
340
283
|
skipped: plan.skipped, operations: result.operations, home }),
|
|
341
284
|
...reloadAdvice(result.operations)].join("\n"),
|
|
342
|
-
error: failureOf({ action, acted, failed: result.failed
|
|
285
|
+
error: failureOf({ action, acted, failed: result.failed, operations: result.operations,
|
|
286
|
+
skipped: plan.skipped, home }) };
|
|
343
287
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { LIVE_POLICIES, livePolicyOf } from "@agents-can-communicate/installer";
|
|
2
|
+
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
3
|
+
|
|
4
|
+
const DECISION_SOURCES = new Set([
|
|
5
|
+
"interactive-accepted",
|
|
6
|
+
"interactive-declined",
|
|
7
|
+
"explicit-option",
|
|
8
|
+
"noninteractive-default",
|
|
9
|
+
"unsupported-default",
|
|
10
|
+
"legacy-unknown",
|
|
11
|
+
]);
|
|
12
|
+
const DELIBERATE_SOURCES = new Set([
|
|
13
|
+
"interactive-accepted", "interactive-declined", "explicit-option",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const legacyDecision = () => ({ source: "legacy-unknown", completeSetup: false });
|
|
17
|
+
|
|
18
|
+
function validDecision(record) {
|
|
19
|
+
const decision = record?.deliveryDecision;
|
|
20
|
+
if (!(decision !== null && typeof decision === "object"
|
|
21
|
+
&& DECISION_SOURCES.has(decision.source)
|
|
22
|
+
&& typeof decision.completeSetup === "boolean")) return false;
|
|
23
|
+
|
|
24
|
+
const enabled = livePolicyOf(record) !== "off";
|
|
25
|
+
if (decision.source === "interactive-accepted") return decision.completeSetup && enabled;
|
|
26
|
+
if (decision.source === "interactive-declined") return !decision.completeSetup && !enabled;
|
|
27
|
+
if (decision.source === "explicit-option") return decision.completeSetup === enabled;
|
|
28
|
+
if (["noninteractive-default", "unsupported-default"].includes(decision.source)) {
|
|
29
|
+
return !decision.completeSetup && !enabled;
|
|
30
|
+
}
|
|
31
|
+
return decision.completeSetup === false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function decisionOf(record) {
|
|
35
|
+
return validDecision(record)
|
|
36
|
+
? { source: record.deliveryDecision.source,
|
|
37
|
+
completeSetup: record.deliveryDecision.completeSetup }
|
|
38
|
+
: legacyDecision();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isEligible = entry => entry.nativeDelivery?.state === "eligible"
|
|
42
|
+
|| entry.nativeDelivery?.consentAvailable === true;
|
|
43
|
+
|
|
44
|
+
const needsExpandedSetup = entry => ["needed", "blocked"]
|
|
45
|
+
.includes(entry.nativeServiceSetup?.state);
|
|
46
|
+
|
|
47
|
+
const isInteractive = runtime => typeof runtime.isInteractive === "function"
|
|
48
|
+
&& runtime.isInteractive() === true;
|
|
49
|
+
|
|
50
|
+
function questionFor(entries) {
|
|
51
|
+
const names = entries.map(entry => entry.displayName ?? entry.adapterId).join(", ");
|
|
52
|
+
const setup = [...new Set(entries.map(entry => entry.outgoingDelivery?.setup)
|
|
53
|
+
.filter(value => typeof value === "string"))];
|
|
54
|
+
const channels = entries.some(entry => (entry.nativeDelivery?.activationPlan?.mechanisms ?? [])
|
|
55
|
+
.some(mechanism => mechanism.kind === "shell-bootstrap"
|
|
56
|
+
&& (mechanism.prefixArgs ?? []).some(argument => argument.startsWith("--"))));
|
|
57
|
+
return [
|
|
58
|
+
`Complete automatic peer-request setup for ${names}? (experimental)`,
|
|
59
|
+
" Yes: automatic turns can spend tokens without waiting for you.",
|
|
60
|
+
" Configure the required local permission grants.",
|
|
61
|
+
...(entries.some(entry => entry.adapterId === "codex")
|
|
62
|
+
? [" Start a missing supported Codex service."] : []),
|
|
63
|
+
...setup.map(note => ` ${note}`),
|
|
64
|
+
...(channels ? [" Allow Claude Code development Channels when prompted at startup."] : []),
|
|
65
|
+
" No: keep current delivery policies and use available next-turn hooks or acc inbox.",
|
|
66
|
+
].join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Decide delivery once for all clients whose complete setup has no recorded answer. */
|
|
70
|
+
export async function decideDelivery({ options, detected, recorded, runtime, dryRun }) {
|
|
71
|
+
const explicit = options.delivery;
|
|
72
|
+
if (explicit !== undefined && !LIVE_POLICIES.includes(explicit)) {
|
|
73
|
+
throw new AccError(EXIT.USAGE, `unknown delivery policy: ${explicit}`, { delivery: explicit });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const recordedById = new Map(recorded.map(install => [install.adapterId, install]));
|
|
77
|
+
const deliveryByAdapter = {};
|
|
78
|
+
const deliveryDecisionByAdapter = {};
|
|
79
|
+
const candidates = [];
|
|
80
|
+
let withheld = 0;
|
|
81
|
+
|
|
82
|
+
for (const entry of detected) {
|
|
83
|
+
const record = recordedById.get(entry.adapterId);
|
|
84
|
+
const previous = livePolicyOf(record);
|
|
85
|
+
const eligible = isEligible(entry);
|
|
86
|
+
|
|
87
|
+
if (explicit !== undefined) {
|
|
88
|
+
deliveryByAdapter[entry.adapterId] = explicit;
|
|
89
|
+
deliveryDecisionByAdapter[entry.adapterId] = {
|
|
90
|
+
source: "explicit-option", completeSetup: explicit !== "off",
|
|
91
|
+
};
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
deliveryByAdapter[entry.adapterId] = previous;
|
|
96
|
+
if (record !== undefined) {
|
|
97
|
+
deliveryDecisionByAdapter[entry.adapterId] = decisionOf(record);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!eligible) {
|
|
101
|
+
deliveryDecisionByAdapter[entry.adapterId] ??= {
|
|
102
|
+
source: "unsupported-default", completeSetup: false,
|
|
103
|
+
};
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const alreadyDecided = validDecision(record)
|
|
108
|
+
&& DELIBERATE_SOURCES.has(record.deliveryDecision.source);
|
|
109
|
+
const expandsLegacyOptIn = previous !== "off" && needsExpandedSetup(entry)
|
|
110
|
+
&& !decisionOf(record).completeSetup;
|
|
111
|
+
const freshDecision = previous === "off" && !alreadyDecided;
|
|
112
|
+
if (!freshDecision && !expandsLegacyOptIn) {
|
|
113
|
+
deliveryDecisionByAdapter[entry.adapterId] ??= legacyDecision();
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (dryRun || !isInteractive(runtime)) {
|
|
118
|
+
if (record === undefined) {
|
|
119
|
+
deliveryDecisionByAdapter[entry.adapterId] = {
|
|
120
|
+
source: "noninteractive-default", completeSetup: false,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (dryRun && freshDecision) withheld += 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
candidates.push(entry);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const asked = candidates.map(entry => entry.adapterId);
|
|
130
|
+
if (candidates.length > 0) {
|
|
131
|
+
const yes = await runtime.confirm(questionFor(candidates),
|
|
132
|
+
{ input: runtime.input, output: runtime.output }) === true;
|
|
133
|
+
for (const entry of candidates) {
|
|
134
|
+
const record = recordedById.get(entry.adapterId);
|
|
135
|
+
const previous = livePolicyOf(record);
|
|
136
|
+
deliveryByAdapter[entry.adapterId] = yes
|
|
137
|
+
? (previous === "off" ? "actionable" : previous)
|
|
138
|
+
: previous;
|
|
139
|
+
deliveryDecisionByAdapter[entry.adapterId] = yes
|
|
140
|
+
? { source: "interactive-accepted", completeSetup: true }
|
|
141
|
+
: previous === "off"
|
|
142
|
+
? { source: "interactive-declined", completeSetup: false }
|
|
143
|
+
: decisionOf(record);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const notes = explicit === undefined && dryRun && withheld > 0
|
|
148
|
+
? ["interactive choices were not made: this preview keeps native delivery off for "
|
|
149
|
+
+ `${withheld} eligible client(s) without a recorded opt-in`]
|
|
150
|
+
: [];
|
|
151
|
+
return { deliveryByAdapter, deliveryDecisionByAdapter, asked, notes };
|
|
152
|
+
}
|
|
@@ -18,11 +18,16 @@ export async function prepareRefresh({ control, root, env = process.env, callerP
|
|
|
18
18
|
const detected = await detectInstallation({ adapters, context, probeTimeoutMs: probeTimeout(env) });
|
|
19
19
|
const deliveryByAdapter = Object.fromEntries(adapters.map(adapter => [adapter.id,
|
|
20
20
|
livePolicyOf(recorded.find(record => record.adapterId === adapter.id))]));
|
|
21
|
+
const deliveryDecisionByAdapter = Object.fromEntries(adapters.flatMap(adapter => {
|
|
22
|
+
const decision = recorded.find(record => record.adapterId === adapter.id)?.deliveryDecision;
|
|
23
|
+
return decision === undefined ? [] : [[adapter.id, decision]];
|
|
24
|
+
}));
|
|
21
25
|
// Refresh configuration even when the client's service is temporarily absent.
|
|
22
26
|
// The installer preserves consent and existing activation; readiness stays a
|
|
23
27
|
// separate, current probe result rather than an update prerequisite.
|
|
24
28
|
const plan = planInstallation({ adapters, detected, context, recorded,
|
|
25
|
-
accVersion: control.pending.version, requested: control.targets, deliveryByAdapter
|
|
29
|
+
accVersion: control.pending.version, requested: control.targets, deliveryByAdapter,
|
|
30
|
+
deliveryDecisionByAdapter });
|
|
26
31
|
if (plan.skipped.length || plan.operations.length !== wanted.size) {
|
|
27
32
|
throw new Error("candidate did not plan every installed integration");
|
|
28
33
|
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { describeNativeReason } from "@agents-can-communicate/installer";
|
|
2
2
|
|
|
3
|
+
export function describeDeliveryDecision(decision) {
|
|
4
|
+
if (decision?.source === "interactive-declined") return "declined during interactive setup";
|
|
5
|
+
if (decision?.source === "explicit-option") return "disabled by explicit option";
|
|
6
|
+
if (decision?.source === "noninteractive-default") return "defaulted off in a noninteractive run";
|
|
7
|
+
if (decision?.source === "unsupported-default") return "defaulted off because delivery was unsupported";
|
|
8
|
+
return "origin unknown (legacy install)";
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
export function describeNative(native, { clientVersion } = {}) {
|
|
4
12
|
const differs = native.runtime === "active" && native.sessionPolicy
|
|
5
13
|
&& native.sessionPolicy !== native.policy;
|
|
@@ -20,24 +28,33 @@ export function nativeRemediation(entry) {
|
|
|
20
28
|
const native = entry.nativeDelivery;
|
|
21
29
|
if (!(entry.present || entry.installed) || native.eligibility === "unsupported") return [];
|
|
22
30
|
const steps = [];
|
|
31
|
+
const service = entry.nativeServiceSetup;
|
|
23
32
|
if (!native.configured && native.runtime !== "active") {
|
|
24
33
|
steps.push(`acc install --adapter ${entry.adapterId} --delivery actionable`
|
|
25
34
|
+ " # opt in to automatic peer requests; may spend tokens");
|
|
26
35
|
} else if (native.configured && native.activation === "missing") {
|
|
27
36
|
steps.push(`acc install --adapter ${entry.adapterId}`
|
|
28
37
|
+ " # complete the missing native launch setup from a supported shell");
|
|
29
|
-
} else if (native.configured && native.runtime !== "active"
|
|
38
|
+
} else if (native.configured && native.runtime !== "active"
|
|
39
|
+
&& !["needed", "blocked"].includes(service?.state)) {
|
|
30
40
|
steps.push(`${entry.displayName}: no verified live channel in this workspace; `
|
|
31
41
|
+ "open a new terminal, start a new client session and check its integration/channel prompts; "
|
|
32
42
|
+ "then run acc doctor here");
|
|
33
43
|
}
|
|
34
44
|
if (native.reasonCode === "native_endpoint_unavailable") {
|
|
35
|
-
steps.push(
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
if (service?.state === "blocked") steps.push(service.diagnostic);
|
|
46
|
+
else if (service?.state === "needed" && native.configured) {
|
|
47
|
+
steps.push(entry.deliveryDecision?.completeSetup === true
|
|
48
|
+
? `acc install --adapter ${entry.adapterId} # prepare the missing supported local service`
|
|
49
|
+
: `acc install --adapter ${entry.adapterId} --delivery actionable`
|
|
50
|
+
+ " # approve complete automatic peer-request setup");
|
|
51
|
+
} else if (service?.state !== "needed" && service?.state !== "ready") {
|
|
52
|
+
steps.push(entry.nativeSetup ?? `${entry.displayName}: check the client's local delivery service setup`);
|
|
53
|
+
}
|
|
54
|
+
} else if (native.reasonCode === "native_session_unavailable" && native.runtime !== "active") {
|
|
38
55
|
steps.push(`${entry.displayName}: open a session connected to the client's local delivery service`);
|
|
39
56
|
}
|
|
40
|
-
return steps;
|
|
57
|
+
return [...new Set(steps)];
|
|
41
58
|
}
|
|
42
59
|
|
|
43
60
|
// One closed native-delivery report per adapter, built only from detection,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyServiceSetup } from "./service-setup.mjs";
|
|
1
2
|
import { applyNativeActivation, deactivateNative } from "./native-activation.mjs";
|
|
2
3
|
import { finalizeRemoval, missingArtifactParents, recordInstall,
|
|
3
4
|
removeEmptyOwnedDirectories, removeOwnedArtifacts } from "./ownership.mjs";
|
|
@@ -68,7 +69,8 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
68
69
|
await recordInstall({ dataHome, adapterId: adapter.id,
|
|
69
70
|
version: operation.clientVersion ?? null, accVersion,
|
|
70
71
|
artifacts: operation.artifacts, createdDirectories,
|
|
71
|
-
deliveryPolicy: operation.livePolicy,
|
|
72
|
+
deliveryPolicy: operation.livePolicy, deliveryDecision: operation.deliveryDecision,
|
|
73
|
+
nativeActivation: native });
|
|
72
74
|
results.operations.push({ ...operation, applied: true, appendedRcBlock,
|
|
73
75
|
needsAction: outcome.needsAction ?? [],
|
|
74
76
|
changes: outcome.changes ?? [], diagnostics: [
|
|
@@ -77,6 +79,8 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
77
79
|
...(outcome.diagnostics ?? []),
|
|
78
80
|
...notes,
|
|
79
81
|
] });
|
|
82
|
+
await applyServiceSetup({ adapter, context: installContext,
|
|
83
|
+
operation: results.operations.at(-1), results });
|
|
80
84
|
} else {
|
|
81
85
|
// Config ownership can veto removal. Ask before teardown or recorded
|
|
82
86
|
// artifact deletion; an adapter-local check would run after those writes.
|
|
@@ -131,7 +135,7 @@ function describeTeardown(report) {
|
|
|
131
135
|
for (const service of report.services) {
|
|
132
136
|
lines.push(service.outcome === "stopped" ? `stopped the ${service.serviceId} service`
|
|
133
137
|
: `retained the ${service.serviceId} service (${service.outcome === "retained_pre_existing"
|
|
134
|
-
? "it
|
|
138
|
+
? "it was reused at activation" : "no vendor teardown exists"})`);
|
|
135
139
|
}
|
|
136
140
|
return lines;
|
|
137
141
|
}
|
|
@@ -145,6 +145,17 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
|
|
|
145
145
|
: unsupported(adapter.nativeDelivery === undefined
|
|
146
146
|
? "native_delivery_unsupported" : "version_unavailable");
|
|
147
147
|
|
|
148
|
+
if (entry.present && typeof adapter.inspectNativeServiceSetup === "function") {
|
|
149
|
+
try {
|
|
150
|
+
entry.nativeServiceSetup = await withTimeout(Promise.resolve().then(() =>
|
|
151
|
+
adapter.inspectNativeServiceSetup({ ...context, platform, clientVersion: entry.version })),
|
|
152
|
+
probeTimeoutMs, `${adapter.id} service setup probe`);
|
|
153
|
+
} catch {
|
|
154
|
+
entry.nativeServiceSetup = { state: "blocked", reasonCode: "service_inspection_failed",
|
|
155
|
+
diagnostic: `${adapter.displayName} service preparation could not be inspected; check the vendor service and retry install` };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
148
159
|
try {
|
|
149
160
|
const detected = await adapter.detect({ ...context, clientVersion: entry.version,
|
|
150
161
|
platform, nativeDelivery: entry.nativeDelivery });
|
|
@@ -110,7 +110,8 @@ async function saveOwnership({ dataHome, record }) {
|
|
|
110
110
|
* runtime, and leaves the bundle inside the client exactly where it was.
|
|
111
111
|
*/
|
|
112
112
|
export async function recordInstall({ dataHome, adapterId, version, accVersion = null,
|
|
113
|
-
artifacts, createdDirectories = [], deliveryPolicy,
|
|
113
|
+
artifacts, createdDirectories = [], deliveryPolicy, deliveryDecision,
|
|
114
|
+
nativeActivation = null }) {
|
|
114
115
|
const stamped = await Promise.all(artifacts.map(async artifact => ({
|
|
115
116
|
path: artifact.path,
|
|
116
117
|
kind: artifact.kind ?? "file",
|
|
@@ -120,6 +121,7 @@ export async function recordInstall({ dataHome, adapterId, version, accVersion =
|
|
|
120
121
|
})));
|
|
121
122
|
const record = await loadOwnership({ dataHome });
|
|
122
123
|
const previous = record.installs.find(install => install.adapterId === adapterId);
|
|
124
|
+
const retainedDeliveryDecision = deliveryDecision ?? previous?.deliveryDecision;
|
|
123
125
|
const directories = [...new Set([
|
|
124
126
|
...(previous?.createdDirectories ?? []), ...createdDirectories,
|
|
125
127
|
])].sort((left, right) => left.split(path.sep).length - right.split(path.sep).length
|
|
@@ -129,6 +131,9 @@ export async function recordInstall({ dataHome, adapterId, version, accVersion =
|
|
|
129
131
|
{ adapterId, version, accVersion, artifacts: stamped,
|
|
130
132
|
...(directories.length === 0 ? {} : { createdDirectories: directories }),
|
|
131
133
|
...(deliveryPolicy === undefined ? {} : { deliveryPolicy }),
|
|
134
|
+
...(retainedDeliveryDecision === undefined ? {} : {
|
|
135
|
+
deliveryDecision: retainedDeliveryDecision,
|
|
136
|
+
}),
|
|
132
137
|
// Present only for a consented native activation. A record without it
|
|
133
138
|
// - every 0.2 install - reads as live policy off and is never rewritten
|
|
134
139
|
// merely to add the field.
|
|
@@ -14,7 +14,7 @@ import { LIVE_POLICIES, describeActivation, describeDeactivation, planActivation
|
|
|
14
14
|
*/
|
|
15
15
|
export function planInstallation({ adapters, detected, context, action = "install",
|
|
16
16
|
recorded = [], accVersion = null, allowDowngrade = false, requested = [],
|
|
17
|
-
deliveryByAdapter = {} }) {
|
|
17
|
+
deliveryByAdapter = {}, deliveryDecisionByAdapter = {}, allowServiceSetup = false }) {
|
|
18
18
|
if (!["install", "uninstall"].includes(action)) {
|
|
19
19
|
throw new AccError(EXIT.USAGE, `unknown installation action: ${action}`, { action });
|
|
20
20
|
}
|
|
@@ -104,6 +104,9 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
104
104
|
// and so what will be removed. Asking the adapter instead would describe an
|
|
105
105
|
// install for a machine this one no longer is.
|
|
106
106
|
const delivery = deliveryByAdapter[entry.adapterId] ?? "off";
|
|
107
|
+
const deliveryDecision = deliveryDecisionByAdapter[entry.adapterId]
|
|
108
|
+
?? recordedById.get(entry.adapterId)?.deliveryDecision
|
|
109
|
+
?? { source: "legacy-unknown", completeSetup: false };
|
|
107
110
|
const native = entry.nativeDelivery ?? null;
|
|
108
111
|
const liveDeliverySupported = native?.state === "eligible"
|
|
109
112
|
&& native.activationPlan?.eligible === true;
|
|
@@ -122,8 +125,10 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
122
125
|
? describeInstallDelivery(entry, delivery, effectiveLivePolicy) : null;
|
|
123
126
|
const installContext = { ...context, requestedLivePolicy: delivery,
|
|
124
127
|
livePolicy: configuredLivePolicy, clientVersion: entry.version, platform: entry.platform };
|
|
128
|
+
const nativeServiceSetup = action === "install" && allowServiceSetup && delivery !== "off"
|
|
129
|
+
&& deliveryDecision.completeSetup === true ? entry.nativeServiceSetup : undefined;
|
|
125
130
|
const setupNotes = action === "install" && delivery !== "off"
|
|
126
|
-
? [entry.outgoingDelivery?.setup, entry.nativeSetup].filter(note => typeof note === "string") : [];
|
|
131
|
+
? [entry.outgoingDelivery?.setup, nativeServiceSetup ? null : entry.nativeSetup].filter(note => typeof note === "string") : [];
|
|
127
132
|
// A consented activation that this run keeps, activates, or takes back.
|
|
128
133
|
// Only an explicit off or an uninstall removes one; an absent record never
|
|
129
134
|
// creates one.
|
|
@@ -154,11 +159,13 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
154
159
|
alreadyInstalled: entry.installed === true,
|
|
155
160
|
livePolicy: delivery,
|
|
156
161
|
effectiveLivePolicy,
|
|
162
|
+
...(action === "install" ? { deliveryDecision } : {}),
|
|
157
163
|
...(retainedActivation ? { configuredLivePolicy, retainedNativeActivation: retainedActivation } : {}),
|
|
158
164
|
...(deliveryDiagnostic === null ? {} : { deliveryDiagnostic }),
|
|
159
165
|
...(deliverySummary === null ? {} : { deliverySummary }),
|
|
160
166
|
...(setupNotes.length ? { setupNotes } : {}),
|
|
161
167
|
...(nativeActivation === null ? {} : { nativeActivation }),
|
|
168
|
+
...(nativeServiceSetup ? { nativeServiceSetup } : {}),
|
|
162
169
|
...(deactivation === null ? {} : { deactivation }),
|
|
163
170
|
artifacts,
|
|
164
171
|
// Said in the operator's terms, not in paths: which files ACC creates
|
|
@@ -169,6 +176,7 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
169
176
|
...(deliverySummary === null ? [] : [deliverySummary]),
|
|
170
177
|
...(retainedActivation ? ["keep existing native delivery setup; current readiness is unverified"] : []),
|
|
171
178
|
...setupNotes,
|
|
179
|
+
...(nativeServiceSetup ? [nativeServiceSetup.diagnostic] : []),
|
|
172
180
|
...artifacts.filter(a => a.kind === "tree")
|
|
173
181
|
.map(a => `${action === "install" ? "create" : "remove"} ${a.path}`),
|
|
174
182
|
...artifacts.filter(a => a.kind === "merge")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Invoked only after adapter configuration and ownership/consent persistence.
|
|
2
|
+
// A failed service command must not erase the successful adapter operation.
|
|
3
|
+
export async function applyServiceSetup({ adapter, context, operation, results }) {
|
|
4
|
+
const plan = operation.nativeServiceSetup;
|
|
5
|
+
if (!plan || operation.livePolicy === "off" || operation.deliveryDecision?.completeSetup !== true) return;
|
|
6
|
+
let setup = plan;
|
|
7
|
+
if (["needed", "ready"].includes(plan.state)) {
|
|
8
|
+
try {
|
|
9
|
+
setup = await adapter.prepareNativeServiceSetup({ context, plan });
|
|
10
|
+
if (!setup || !["ready", "failed", "blocked"].includes(setup.state)) throw new Error("invalid setup result");
|
|
11
|
+
} catch {
|
|
12
|
+
setup = { state: "failed", started: false, reasonCode: "service_setup_failed",
|
|
13
|
+
diagnostic: `${adapter.displayName} service preparation failed; check the vendor service and retry acc install` };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
operation.nativeServiceSetup = setup;
|
|
17
|
+
if (setup.state === "ready") {
|
|
18
|
+
const stale = new Set([operation.deliverySummary, operation.deliveryDiagnostic, plan.diagnostic]);
|
|
19
|
+
operation.summary = (operation.summary ?? []).filter(line => !stale.has(line));
|
|
20
|
+
operation.summary.push(setup.diagnostic);
|
|
21
|
+
operation.diagnostics = (operation.diagnostics ?? []).filter(line => !stale.has(line));
|
|
22
|
+
operation.deliverySummary = setup.diagnostic;
|
|
23
|
+
operation.deliveryDiagnostic = setup.diagnostic;
|
|
24
|
+
} else {
|
|
25
|
+
operation.needsAction.push(setup.diagnostic);
|
|
26
|
+
if (setup.state === "failed") results.failed.push({ adapterId: operation.adapterId,
|
|
27
|
+
error: setup.diagnostic, reasonCode: setup.reasonCode });
|
|
28
|
+
}
|
|
29
|
+
operation.diagnostics.push(setup.diagnostic);
|
|
30
|
+
}
|