@gmickel/gno 1.22.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -12
- package/assets/skill/SKILL.md +41 -19
- package/package.json +1 -1
- package/spec/cli.md +127 -20
- package/spec/evals-agentic.md +35 -0
- package/spec/evals.md +6 -0
- package/spec/mcp.md +18 -0
- package/spec/output-schemas/query-diagnose-v1.schema.json +123 -0
- package/spec/output-schemas/query-diagnose.schema.json +89 -2
- package/spec/output-schemas/setup-activation-result.schema.json +456 -0
- package/spec/output-schemas/setup-command-result.schema.json +93 -0
- package/spec/output-schemas/setup-receipt.schema.json +258 -0
- package/spec/output-schemas/setup-semantic-receipt.schema.json +195 -0
- package/src/app/context-runtime-types.ts +3 -0
- package/src/app/context-runtime.ts +1 -0
- package/src/app/context-surface.ts +4 -2
- package/src/cli/commands/ask.ts +31 -20
- package/src/cli/commands/completion/scripts.ts +2 -0
- package/src/cli/commands/context-build.ts +17 -7
- package/src/cli/commands/embed.ts +7 -2
- package/src/cli/commands/query.ts +58 -37
- package/src/cli/commands/search.ts +29 -19
- package/src/cli/commands/setup-activation.ts +324 -0
- package/src/cli/commands/setup-semantic.ts +591 -0
- package/src/cli/commands/setup.ts +410 -0
- package/src/cli/commands/vsearch.ts +31 -22
- package/src/cli/options.ts +39 -0
- package/src/cli/program.ts +112 -0
- package/src/cli/setup-semantic-worker.ts +177 -0
- package/src/config/defaults.ts +10 -1
- package/src/config/types.ts +71 -0
- package/src/core/config-mutation.ts +94 -64
- package/src/core/file-lock.ts +70 -31
- package/src/core/folder-setup-planning.ts +453 -0
- package/src/core/folder-setup.ts +490 -0
- package/src/core/project-affinity-surface.ts +114 -0
- package/src/core/project-affinity.ts +330 -0
- package/src/core/setup-activation.ts +309 -0
- package/src/core/setup-receipt.ts +321 -0
- package/src/core/validation.ts +20 -1
- package/src/mcp/tools/ask.ts +10 -1
- package/src/mcp/tools/context.ts +18 -0
- package/src/mcp/tools/index.ts +13 -2
- package/src/mcp/tools/query.ts +12 -0
- package/src/mcp/tools/search.ts +7 -0
- package/src/mcp/tools/vsearch.ts +7 -0
- package/src/pipeline/diagnose.ts +48 -3
- package/src/pipeline/explain.ts +54 -13
- package/src/pipeline/hybrid.ts +100 -59
- package/src/pipeline/project-affinity.ts +162 -0
- package/src/pipeline/search.ts +76 -10
- package/src/pipeline/types.ts +9 -0
- package/src/pipeline/vsearch.ts +117 -91
- package/src/sdk/client.ts +80 -20
- package/src/sdk/index.ts +2 -0
- package/src/sdk/types.ts +20 -7
- package/src/serve/connectors.ts +29 -2
- package/src/serve/context-capsule.ts +18 -1
- package/src/serve/routes/api.ts +69 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trusted project-root resolution for project-aware retrieval affinity.
|
|
3
|
+
*
|
|
4
|
+
* This module resolves local roots only. Remote hints remain opaque and never
|
|
5
|
+
* trigger filesystem probes. Returned metadata is path-redacted; ranking is a
|
|
6
|
+
* separate concern.
|
|
7
|
+
*
|
|
8
|
+
* @module src/core/project-affinity
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// node:fs/promises for directory stat/realpath (no Bun directory equivalent)
|
|
12
|
+
import { realpath, stat } from "node:fs/promises";
|
|
13
|
+
// node:path for path utilities (no Bun path utils)
|
|
14
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
Collection,
|
|
18
|
+
ProjectAffinityInput,
|
|
19
|
+
ProjectAffinityRoot,
|
|
20
|
+
TrustedProjectRootSource,
|
|
21
|
+
} from "../config/types";
|
|
22
|
+
|
|
23
|
+
import { isCanonicalPathContained } from "./validation";
|
|
24
|
+
|
|
25
|
+
export type ProjectAffinityZeroReason =
|
|
26
|
+
| "no_collection_match"
|
|
27
|
+
| "root_unavailable"
|
|
28
|
+
| "untrusted_remote_hint";
|
|
29
|
+
|
|
30
|
+
export type ProjectAffinityRelation =
|
|
31
|
+
| "collection_contains_root"
|
|
32
|
+
| "exact"
|
|
33
|
+
| "root_contains_collection";
|
|
34
|
+
|
|
35
|
+
export interface ProjectAffinityMatch {
|
|
36
|
+
collection: string;
|
|
37
|
+
collectionAlias: string;
|
|
38
|
+
distance: number;
|
|
39
|
+
relation: ProjectAffinityRelation;
|
|
40
|
+
rootAlias: string;
|
|
41
|
+
source: TrustedProjectRootSource;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ProjectAffinityRootMetadata {
|
|
45
|
+
collectionAliases: string[];
|
|
46
|
+
reason: ProjectAffinityZeroReason | null;
|
|
47
|
+
repositoryRootDiscovered: boolean;
|
|
48
|
+
rootAlias: string;
|
|
49
|
+
source: ProjectAffinityRoot["source"];
|
|
50
|
+
status: "matched" | "zero";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ProjectAffinityResolution {
|
|
54
|
+
matches: ProjectAffinityMatch[];
|
|
55
|
+
roots: ProjectAffinityRootMetadata[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ProjectAffinityResolverDependencies {
|
|
59
|
+
canonicalizePath: (path: string) => Promise<string>;
|
|
60
|
+
discoverRepositoryRoot: (path: string) => Promise<string | null>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Trust is supplied by the invoking surface, never inferred from payload data.
|
|
65
|
+
*/
|
|
66
|
+
export interface ProjectAffinityResolverContext {
|
|
67
|
+
channel: "local" | "remote";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface CanonicalCollection {
|
|
71
|
+
alias: string;
|
|
72
|
+
name: string;
|
|
73
|
+
path: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface CanonicalTrustedRoot {
|
|
77
|
+
path: string;
|
|
78
|
+
repositoryRootDiscovered: boolean;
|
|
79
|
+
rootAlias: string;
|
|
80
|
+
source: TrustedProjectRootSource;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const alias = (namespace: "collection" | "root", value: string): string => {
|
|
84
|
+
const hash = new Bun.CryptoHasher("sha256")
|
|
85
|
+
.update(`${namespace}\0${value}`)
|
|
86
|
+
.digest("hex")
|
|
87
|
+
.slice(0, 12);
|
|
88
|
+
return `${namespace}_${hash}`;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const defaultCanonicalizePath = (path: string): Promise<string> =>
|
|
92
|
+
realpath(path);
|
|
93
|
+
|
|
94
|
+
const isDirectoryOrFile = async (path: string): Promise<boolean> => {
|
|
95
|
+
try {
|
|
96
|
+
await stat(path);
|
|
97
|
+
return true;
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const defaultDiscoverRepositoryRoot = async (
|
|
104
|
+
startingPath: string
|
|
105
|
+
): Promise<string | null> => {
|
|
106
|
+
let current = startingPath;
|
|
107
|
+
while (true) {
|
|
108
|
+
if (await isDirectoryOrFile(join(current, ".git"))) {
|
|
109
|
+
return current;
|
|
110
|
+
}
|
|
111
|
+
const parent = dirname(current);
|
|
112
|
+
if (parent === current) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
current = parent;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const pathDistance = (parent: string, child: string): number => {
|
|
120
|
+
const nestedPath = relative(parent, child);
|
|
121
|
+
if (nestedPath === "") {
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
return nestedPath.split(sep).filter(Boolean).length;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const describeRelationship = (
|
|
128
|
+
collectionPath: string,
|
|
129
|
+
rootPath: string
|
|
130
|
+
): Pick<ProjectAffinityMatch, "distance" | "relation"> | null => {
|
|
131
|
+
if (collectionPath === rootPath) {
|
|
132
|
+
return { distance: 0, relation: "exact" };
|
|
133
|
+
}
|
|
134
|
+
if (isCanonicalPathContained(collectionPath, rootPath)) {
|
|
135
|
+
return {
|
|
136
|
+
distance: pathDistance(collectionPath, rootPath),
|
|
137
|
+
relation: "collection_contains_root",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (isCanonicalPathContained(rootPath, collectionPath)) {
|
|
141
|
+
return {
|
|
142
|
+
distance: pathDistance(rootPath, collectionPath),
|
|
143
|
+
relation: "root_contains_collection",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const canonicalizeCollections = async (
|
|
150
|
+
collections: readonly Collection[],
|
|
151
|
+
canonicalizePath: ProjectAffinityResolverDependencies["canonicalizePath"]
|
|
152
|
+
): Promise<CanonicalCollection[]> => {
|
|
153
|
+
const resolved = await Promise.all(
|
|
154
|
+
collections.map(async (collection): Promise<CanonicalCollection | null> => {
|
|
155
|
+
try {
|
|
156
|
+
const path = await canonicalizePath(collection.path);
|
|
157
|
+
return {
|
|
158
|
+
alias: alias("collection", collection.name),
|
|
159
|
+
name: collection.name,
|
|
160
|
+
path,
|
|
161
|
+
};
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
return resolved
|
|
168
|
+
.filter((collection): collection is CanonicalCollection =>
|
|
169
|
+
Boolean(collection)
|
|
170
|
+
)
|
|
171
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const canonicalizeTrustedRoot = async (
|
|
175
|
+
root: Extract<ProjectAffinityRoot, { path: string }>,
|
|
176
|
+
dependencies: ProjectAffinityResolverDependencies
|
|
177
|
+
): Promise<CanonicalTrustedRoot | null> => {
|
|
178
|
+
let canonicalPath: string;
|
|
179
|
+
try {
|
|
180
|
+
canonicalPath = await dependencies.canonicalizePath(root.path);
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let repositoryRootDiscovered = false;
|
|
186
|
+
if (root.source === "cli_cwd" || root.source === "cli_worktree") {
|
|
187
|
+
let discovered: string | null = null;
|
|
188
|
+
try {
|
|
189
|
+
discovered = await dependencies.discoverRepositoryRoot(canonicalPath);
|
|
190
|
+
} catch {
|
|
191
|
+
// Repository discovery is opportunistic; the trusted canonical cwd
|
|
192
|
+
// remains a valid affinity root when discovery is unavailable.
|
|
193
|
+
}
|
|
194
|
+
if (discovered) {
|
|
195
|
+
try {
|
|
196
|
+
canonicalPath = await dependencies.canonicalizePath(discovered);
|
|
197
|
+
repositoryRootDiscovered = true;
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
path: canonicalPath,
|
|
206
|
+
repositoryRootDiscovered,
|
|
207
|
+
rootAlias: alias("root", canonicalPath),
|
|
208
|
+
source: root.source,
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const zeroMetadata = (
|
|
213
|
+
root: ProjectAffinityRoot,
|
|
214
|
+
reason: ProjectAffinityZeroReason,
|
|
215
|
+
source: ProjectAffinityRoot["source"] = root.source
|
|
216
|
+
): ProjectAffinityRootMetadata => ({
|
|
217
|
+
collectionAliases: [],
|
|
218
|
+
reason,
|
|
219
|
+
repositoryRootDiscovered: false,
|
|
220
|
+
rootAlias: alias(
|
|
221
|
+
"root",
|
|
222
|
+
root.source === "remote_hint" ? root.hint : root.path
|
|
223
|
+
),
|
|
224
|
+
source,
|
|
225
|
+
status: "zero",
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const relationOrder: Record<ProjectAffinityRelation, number> = {
|
|
229
|
+
exact: 0,
|
|
230
|
+
collection_contains_root: 1,
|
|
231
|
+
root_contains_collection: 2,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const compareMatches = (
|
|
235
|
+
left: ProjectAffinityMatch,
|
|
236
|
+
right: ProjectAffinityMatch
|
|
237
|
+
): number =>
|
|
238
|
+
relationOrder[left.relation] - relationOrder[right.relation] ||
|
|
239
|
+
left.distance - right.distance ||
|
|
240
|
+
left.collection.localeCompare(right.collection) ||
|
|
241
|
+
left.rootAlias.localeCompare(right.rootAlias);
|
|
242
|
+
|
|
243
|
+
export async function resolveProjectAffinity(
|
|
244
|
+
input: ProjectAffinityInput,
|
|
245
|
+
collections: readonly Collection[],
|
|
246
|
+
context: ProjectAffinityResolverContext,
|
|
247
|
+
overrides: Partial<ProjectAffinityResolverDependencies> = {}
|
|
248
|
+
): Promise<ProjectAffinityResolution> {
|
|
249
|
+
if (context.channel === "remote") {
|
|
250
|
+
return {
|
|
251
|
+
matches: [],
|
|
252
|
+
roots: input.roots.map((root) =>
|
|
253
|
+
zeroMetadata(root, "untrusted_remote_hint", "remote_hint")
|
|
254
|
+
),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const dependencies: ProjectAffinityResolverDependencies = {
|
|
259
|
+
canonicalizePath: overrides.canonicalizePath ?? defaultCanonicalizePath,
|
|
260
|
+
discoverRepositoryRoot:
|
|
261
|
+
overrides.discoverRepositoryRoot ?? defaultDiscoverRepositoryRoot,
|
|
262
|
+
};
|
|
263
|
+
const trustedRoots = input.roots.filter(
|
|
264
|
+
(root): root is Extract<ProjectAffinityRoot, { path: string }> =>
|
|
265
|
+
root.source !== "remote_hint"
|
|
266
|
+
);
|
|
267
|
+
const canonicalCollections =
|
|
268
|
+
trustedRoots.length === 0
|
|
269
|
+
? []
|
|
270
|
+
: await canonicalizeCollections(
|
|
271
|
+
collections,
|
|
272
|
+
dependencies.canonicalizePath
|
|
273
|
+
);
|
|
274
|
+
const matches: ProjectAffinityMatch[] = [];
|
|
275
|
+
const roots: ProjectAffinityRootMetadata[] = [];
|
|
276
|
+
|
|
277
|
+
for (const root of input.roots) {
|
|
278
|
+
if (root.source === "remote_hint") {
|
|
279
|
+
roots.push(zeroMetadata(root, "untrusted_remote_hint"));
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const canonicalRoot = await canonicalizeTrustedRoot(root, dependencies);
|
|
284
|
+
if (!canonicalRoot) {
|
|
285
|
+
roots.push(zeroMetadata(root, "root_unavailable"));
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const rootMatches = canonicalCollections
|
|
290
|
+
.map((collection): ProjectAffinityMatch | null => {
|
|
291
|
+
const relationship = describeRelationship(
|
|
292
|
+
collection.path,
|
|
293
|
+
canonicalRoot.path
|
|
294
|
+
);
|
|
295
|
+
return relationship
|
|
296
|
+
? {
|
|
297
|
+
collection: collection.name,
|
|
298
|
+
collectionAlias: collection.alias,
|
|
299
|
+
rootAlias: canonicalRoot.rootAlias,
|
|
300
|
+
source: canonicalRoot.source,
|
|
301
|
+
...relationship,
|
|
302
|
+
}
|
|
303
|
+
: null;
|
|
304
|
+
})
|
|
305
|
+
.filter((match): match is ProjectAffinityMatch => Boolean(match))
|
|
306
|
+
.sort(compareMatches);
|
|
307
|
+
|
|
308
|
+
if (rootMatches.length === 0) {
|
|
309
|
+
roots.push({
|
|
310
|
+
...zeroMetadata(root, "no_collection_match"),
|
|
311
|
+
repositoryRootDiscovered: canonicalRoot.repositoryRootDiscovered,
|
|
312
|
+
rootAlias: canonicalRoot.rootAlias,
|
|
313
|
+
});
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
matches.push(...rootMatches);
|
|
318
|
+
roots.push({
|
|
319
|
+
collectionAliases: rootMatches.map((match) => match.collectionAlias),
|
|
320
|
+
reason: null,
|
|
321
|
+
repositoryRootDiscovered: canonicalRoot.repositoryRootDiscovered,
|
|
322
|
+
rootAlias: canonicalRoot.rootAlias,
|
|
323
|
+
source: root.source,
|
|
324
|
+
status: "matched",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
matches.sort(compareMatches);
|
|
329
|
+
return { matches, roots };
|
|
330
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActivationVerificationCode,
|
|
3
|
+
ActivationVerificationReceipt,
|
|
4
|
+
StorePort,
|
|
5
|
+
StoreResult,
|
|
6
|
+
} from "../store/types";
|
|
7
|
+
import type { ConnectorVerificationCode } from "./connector-policy";
|
|
8
|
+
|
|
9
|
+
import { getConnectorVerificationRemediation } from "./connector-policy";
|
|
10
|
+
|
|
11
|
+
export const SETUP_ACTIVATION_SCHEMA_VERSION = "1.0" as const;
|
|
12
|
+
|
|
13
|
+
export type SetupConnectorInstallation = "installed" | "reused" | "failed";
|
|
14
|
+
export type SetupConnectorVerification =
|
|
15
|
+
| "passed"
|
|
16
|
+
| "failed"
|
|
17
|
+
| "skipped"
|
|
18
|
+
| "not_run";
|
|
19
|
+
export type SetupConnectorCode =
|
|
20
|
+
| "connector_verified"
|
|
21
|
+
| "connector_configuration_unavailable"
|
|
22
|
+
| "connector_install_failed"
|
|
23
|
+
| "connector_verification_failed"
|
|
24
|
+
| ConnectorVerificationCode;
|
|
25
|
+
|
|
26
|
+
export interface SetupConnectorDefinition {
|
|
27
|
+
id: string;
|
|
28
|
+
kind: "skill" | "mcp";
|
|
29
|
+
target: string;
|
|
30
|
+
scope: "user" | "project";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SetupConnectorState extends SetupConnectorDefinition {
|
|
34
|
+
installed: boolean;
|
|
35
|
+
configurationError: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SetupConnectorResult {
|
|
39
|
+
connectorId: string;
|
|
40
|
+
kind: "skill" | "mcp";
|
|
41
|
+
target: string;
|
|
42
|
+
scope: "user" | "project";
|
|
43
|
+
installation: SetupConnectorInstallation;
|
|
44
|
+
verification: SetupConnectorVerification;
|
|
45
|
+
code: SetupConnectorCode;
|
|
46
|
+
remediation: string;
|
|
47
|
+
receipt: ActivationVerificationReceipt | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SetupConnectorComposition {
|
|
51
|
+
status: "completed" | "completed_with_actions";
|
|
52
|
+
connectors: SetupConnectorResult[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SetupConnectorInstallContext {
|
|
56
|
+
indexName: string;
|
|
57
|
+
configPath: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SetupConnectorCompositionDeps {
|
|
61
|
+
getStates: () => Promise<SetupConnectorState[]>;
|
|
62
|
+
install: (
|
|
63
|
+
connectorId: string,
|
|
64
|
+
context: SetupConnectorInstallContext
|
|
65
|
+
) => Promise<SetupConnectorState>;
|
|
66
|
+
verify: (
|
|
67
|
+
connectorId: string,
|
|
68
|
+
store: StorePort,
|
|
69
|
+
collection: string
|
|
70
|
+
) => Promise<StoreResult<ActivationVerificationReceipt>>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ComposeSetupConnectorsOptions {
|
|
74
|
+
connectorIds: string[];
|
|
75
|
+
definitions: SetupConnectorDefinition[];
|
|
76
|
+
collection: string;
|
|
77
|
+
store: StorePort;
|
|
78
|
+
installContext: SetupConnectorInstallContext;
|
|
79
|
+
deps: SetupConnectorCompositionDeps;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const completedRemediation = "No action required.";
|
|
83
|
+
const installFailureRemediation =
|
|
84
|
+
"Repair the selected connector configuration or permissions, then rerun the same setup command.";
|
|
85
|
+
const configurationFailureRemediation =
|
|
86
|
+
"Repair the selected connector configuration without overwriting it, then rerun the same setup command.";
|
|
87
|
+
const verificationFailureRemediation =
|
|
88
|
+
"Retry the same setup command; if verification still fails, run gno doctor.";
|
|
89
|
+
|
|
90
|
+
function isConnectorVerificationCode(
|
|
91
|
+
code: ActivationVerificationCode | undefined
|
|
92
|
+
): code is ConnectorVerificationCode {
|
|
93
|
+
return (
|
|
94
|
+
code === "connector_not_configured" ||
|
|
95
|
+
code === "connector_probe_unavailable" ||
|
|
96
|
+
code === "connector_unsupported_config" ||
|
|
97
|
+
code === "connector_start_failed" ||
|
|
98
|
+
code === "connector_timeout" ||
|
|
99
|
+
code === "connector_missing_tools" ||
|
|
100
|
+
code === "connector_status_failed" ||
|
|
101
|
+
code === "connector_search_failed" ||
|
|
102
|
+
code === "connector_result_mismatch" ||
|
|
103
|
+
code === "target_runtime_unverifiable"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function dedupeConnectorIds(connectorIds: string[]): string[] {
|
|
108
|
+
const seen = new Set<string>();
|
|
109
|
+
return connectorIds.filter((connectorId) => {
|
|
110
|
+
if (seen.has(connectorId)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
seen.add(connectorId);
|
|
114
|
+
return true;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function failedResult(
|
|
119
|
+
definition: SetupConnectorDefinition,
|
|
120
|
+
input: {
|
|
121
|
+
installation: SetupConnectorInstallation;
|
|
122
|
+
code:
|
|
123
|
+
| "connector_configuration_unavailable"
|
|
124
|
+
| "connector_install_failed"
|
|
125
|
+
| "connector_verification_failed";
|
|
126
|
+
remediation: string;
|
|
127
|
+
}
|
|
128
|
+
): SetupConnectorResult {
|
|
129
|
+
return {
|
|
130
|
+
connectorId: definition.id,
|
|
131
|
+
kind: definition.kind,
|
|
132
|
+
target: definition.target,
|
|
133
|
+
scope: definition.scope,
|
|
134
|
+
installation: input.installation,
|
|
135
|
+
verification:
|
|
136
|
+
input.code === "connector_verification_failed" ? "failed" : "not_run",
|
|
137
|
+
code: input.code,
|
|
138
|
+
remediation: input.remediation,
|
|
139
|
+
receipt: null,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function receiptResult(
|
|
144
|
+
definition: SetupConnectorDefinition,
|
|
145
|
+
installation: SetupConnectorInstallation,
|
|
146
|
+
receipt: ActivationVerificationReceipt
|
|
147
|
+
): SetupConnectorResult {
|
|
148
|
+
const connectorStage = receipt.stages.connector;
|
|
149
|
+
const verification =
|
|
150
|
+
connectorStage.status === "passed" ||
|
|
151
|
+
connectorStage.status === "failed" ||
|
|
152
|
+
connectorStage.status === "skipped"
|
|
153
|
+
? connectorStage.status
|
|
154
|
+
: "failed";
|
|
155
|
+
const code =
|
|
156
|
+
connectorStage.status === "passed"
|
|
157
|
+
? "connector_verified"
|
|
158
|
+
: isConnectorVerificationCode(connectorStage.code)
|
|
159
|
+
? connectorStage.code
|
|
160
|
+
: "connector_verification_failed";
|
|
161
|
+
const remediation =
|
|
162
|
+
code === "connector_verified"
|
|
163
|
+
? completedRemediation
|
|
164
|
+
: code === "connector_verification_failed"
|
|
165
|
+
? verificationFailureRemediation
|
|
166
|
+
: getConnectorVerificationRemediation(code, definition.target);
|
|
167
|
+
return {
|
|
168
|
+
connectorId: definition.id,
|
|
169
|
+
kind: definition.kind,
|
|
170
|
+
target: definition.target,
|
|
171
|
+
scope: definition.scope,
|
|
172
|
+
installation,
|
|
173
|
+
verification,
|
|
174
|
+
code,
|
|
175
|
+
remediation,
|
|
176
|
+
receipt,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function unavailableSetupConnectorComposition(
|
|
181
|
+
definitions: SetupConnectorDefinition[]
|
|
182
|
+
): SetupConnectorComposition {
|
|
183
|
+
return {
|
|
184
|
+
status: "completed_with_actions",
|
|
185
|
+
connectors: definitions.map((definition) => ({
|
|
186
|
+
connectorId: definition.id,
|
|
187
|
+
kind: definition.kind,
|
|
188
|
+
target: definition.target,
|
|
189
|
+
scope: definition.scope,
|
|
190
|
+
installation: "failed",
|
|
191
|
+
verification: "not_run",
|
|
192
|
+
code: "connector_verification_failed",
|
|
193
|
+
remediation: verificationFailureRemediation,
|
|
194
|
+
receipt: null,
|
|
195
|
+
})),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function currentStateAfterInstallFailure(
|
|
200
|
+
connectorId: string,
|
|
201
|
+
deps: SetupConnectorCompositionDeps
|
|
202
|
+
): Promise<SetupConnectorState | null> {
|
|
203
|
+
try {
|
|
204
|
+
const states = await deps.getStates();
|
|
205
|
+
return states.find(({ id }) => id === connectorId) ?? null;
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function composeOneConnector(
|
|
212
|
+
definition: SetupConnectorDefinition,
|
|
213
|
+
initialState: SetupConnectorState | null,
|
|
214
|
+
options: ComposeSetupConnectorsOptions
|
|
215
|
+
): Promise<SetupConnectorResult> {
|
|
216
|
+
if (!initialState || initialState.configurationError) {
|
|
217
|
+
return failedResult(definition, {
|
|
218
|
+
installation: "failed",
|
|
219
|
+
code: "connector_configuration_unavailable",
|
|
220
|
+
remediation: configurationFailureRemediation,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let installation: SetupConnectorInstallation = "reused";
|
|
225
|
+
if (!initialState.installed) {
|
|
226
|
+
try {
|
|
227
|
+
const installed = await options.deps.install(
|
|
228
|
+
definition.id,
|
|
229
|
+
options.installContext
|
|
230
|
+
);
|
|
231
|
+
if (!installed.installed || installed.configurationError) {
|
|
232
|
+
return failedResult(definition, {
|
|
233
|
+
installation: "failed",
|
|
234
|
+
code: "connector_install_failed",
|
|
235
|
+
remediation: installFailureRemediation,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
installation = "installed";
|
|
239
|
+
} catch {
|
|
240
|
+
const racedState = await currentStateAfterInstallFailure(
|
|
241
|
+
definition.id,
|
|
242
|
+
options.deps
|
|
243
|
+
);
|
|
244
|
+
if (!racedState?.installed || racedState.configurationError) {
|
|
245
|
+
return failedResult(definition, {
|
|
246
|
+
installation: "failed",
|
|
247
|
+
code: "connector_install_failed",
|
|
248
|
+
remediation: installFailureRemediation,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
installation = "reused";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const verification = await options.deps.verify(
|
|
257
|
+
definition.id,
|
|
258
|
+
options.store,
|
|
259
|
+
options.collection
|
|
260
|
+
);
|
|
261
|
+
if (!verification.ok) {
|
|
262
|
+
return failedResult(definition, {
|
|
263
|
+
installation,
|
|
264
|
+
code: "connector_verification_failed",
|
|
265
|
+
remediation: verificationFailureRemediation,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return receiptResult(definition, installation, verification.value);
|
|
269
|
+
} catch {
|
|
270
|
+
return failedResult(definition, {
|
|
271
|
+
installation,
|
|
272
|
+
code: "connector_verification_failed",
|
|
273
|
+
remediation: verificationFailureRemediation,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Compose requested connector install/verification after lexical setup.
|
|
280
|
+
* Callers own store lifecycle and validation of connector IDs.
|
|
281
|
+
*/
|
|
282
|
+
export async function composeSetupConnectors(
|
|
283
|
+
options: ComposeSetupConnectorsOptions
|
|
284
|
+
): Promise<SetupConnectorComposition> {
|
|
285
|
+
const requestedIds = dedupeConnectorIds(options.connectorIds);
|
|
286
|
+
let states: SetupConnectorState[] = [];
|
|
287
|
+
try {
|
|
288
|
+
states = await options.deps.getStates();
|
|
289
|
+
} catch {
|
|
290
|
+
// Per-target bounded results below deliberately hide the unbounded error.
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const results: SetupConnectorResult[] = [];
|
|
294
|
+
for (const connectorId of requestedIds) {
|
|
295
|
+
const definition = options.definitions.find(({ id }) => id === connectorId);
|
|
296
|
+
if (!definition) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const state = states.find(({ id }) => id === connectorId) ?? null;
|
|
300
|
+
results.push(await composeOneConnector(definition, state, options));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
status: results.every(({ verification }) => verification === "passed")
|
|
305
|
+
? "completed"
|
|
306
|
+
: "completed_with_actions",
|
|
307
|
+
connectors: results,
|
|
308
|
+
};
|
|
309
|
+
}
|