@dobby.ai/dobby 0.1.0 → 0.1.1
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/.env.example +0 -1
- package/AGENTS.md +7 -7
- package/README.md +64 -32
- package/config/gateway.example.json +10 -6
- package/dist/plugins/connector-discord/src/mapper.js +75 -0
- package/dist/src/cli/commands/doctor.js +81 -2
- package/dist/src/cli/commands/extension.js +3 -1
- package/dist/src/cli/commands/init.js +43 -173
- package/dist/src/cli/commands/topology.js +38 -14
- package/dist/src/cli/program.js +15 -131
- package/dist/src/cli/shared/config-io.js +3 -31
- package/dist/src/cli/shared/config-mutators.js +33 -9
- package/dist/src/cli/shared/configure-sections.js +52 -12
- package/dist/src/cli/shared/init-catalog.js +89 -46
- package/dist/src/cli/shared/local-extension-specs.js +85 -0
- package/dist/src/cli/shared/schema-prompts.js +26 -2
- package/dist/src/cli/tests/config-io.test.js +5 -5
- package/dist/src/cli/tests/discord-mapper.test.js +90 -0
- package/dist/src/cli/tests/doctor.test.js +145 -0
- package/dist/src/cli/tests/init-catalog.test.js +108 -61
- package/dist/src/cli/tests/program-options.test.js +14 -28
- package/dist/src/cli/tests/routing-config.test.js +59 -4
- package/dist/src/core/gateway.js +3 -1
- package/dist/src/core/routing.js +53 -38
- package/dist/src/main.js +0 -0
- package/dist/src/shared/dobby-repo.js +40 -0
- package/docs/RUNBOOK.md +28 -27
- package/package.json +3 -2
- package/plugins/connector-discord/package-lock.json +2 -2
- package/plugins/connector-discord/package.json +1 -1
- package/plugins/connector-discord/src/connector.ts +0 -5
- package/plugins/connector-discord/src/mapper.ts +3 -4
- package/plugins/connector-feishu/package-lock.json +2 -2
- package/plugins/connector-feishu/package.json +1 -1
- package/plugins/plugin-sdk/package-lock.json +2 -2
- package/plugins/plugin-sdk/package.json +1 -1
- package/plugins/provider-claude/package-lock.json +2 -2
- package/plugins/provider-claude/package.json +1 -1
- package/plugins/provider-claude-cli/package-lock.json +2 -2
- package/plugins/provider-claude-cli/package.json +1 -1
- package/plugins/provider-pi/package-lock.json +2 -2
- package/plugins/provider-pi/package.json +1 -1
- package/plugins/provider-pi/src/contribution.ts +139 -9
- package/src/cli/commands/doctor.ts +103 -2
- package/src/cli/commands/extension.ts +3 -1
- package/src/cli/commands/init.ts +45 -230
- package/src/cli/commands/topology.ts +48 -16
- package/src/cli/program.ts +16 -167
- package/src/cli/shared/config-io.ts +3 -35
- package/src/cli/shared/config-mutators.ts +39 -9
- package/src/cli/shared/config-types.ts +10 -2
- package/src/cli/shared/configure-sections.ts +55 -11
- package/src/cli/shared/init-catalog.ts +126 -66
- package/src/cli/shared/local-extension-specs.ts +108 -0
- package/src/cli/shared/schema-prompts.ts +30 -1
- package/src/cli/tests/config-io.test.ts +5 -5
- package/src/cli/tests/discord-mapper.test.ts +128 -0
- package/src/cli/tests/doctor.test.ts +149 -0
- package/src/cli/tests/init-catalog.test.ts +112 -64
- package/src/cli/tests/program-options.test.ts +14 -32
- package/src/cli/tests/routing-config.test.ts +76 -4
- package/src/core/gateway.ts +3 -1
- package/src/core/routing.ts +70 -45
- package/src/core/types.ts +8 -2
- package/src/shared/dobby-repo.ts +48 -0
- package/config/models.custom.example.json +0 -27
- package/dist/src/agent/tests/event-forwarder.test.js +0 -113
- package/dist/src/cli/shared/config-path.js +0 -207
- package/dist/src/cli/shared/init-models-file.js +0 -65
- package/dist/src/cli/shared/presets.js +0 -86
- package/dist/src/cli/tests/config-path.test.js +0 -21
- package/dist/src/cli/tests/discord-config.test.js +0 -23
- package/dist/src/cli/tests/presets.test.js +0 -41
- package/dist/src/cli/tests/routing-legacy.test.js +0 -191
- package/dist/src/core/tests/gateway-update-strategy.test.js +0 -167
- package/src/cli/shared/init-models-file.ts +0 -77
|
@@ -42,10 +42,32 @@ type RuntimeTool = NonNullable<CreateAgentSessionOptions["tools"]>[number];
|
|
|
42
42
|
interface PiProviderConfig {
|
|
43
43
|
provider: string;
|
|
44
44
|
model: string;
|
|
45
|
+
baseUrl: string;
|
|
46
|
+
apiKey: string;
|
|
47
|
+
api: Api;
|
|
48
|
+
headers?: Record<string, string>;
|
|
49
|
+
authHeader: boolean;
|
|
50
|
+
models: PiProviderModelConfig[];
|
|
45
51
|
thinkingLevel: ThinkingLevel;
|
|
46
52
|
agentDir?: string;
|
|
47
53
|
authFile?: string;
|
|
48
|
-
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface PiProviderModelConfig {
|
|
57
|
+
id: string;
|
|
58
|
+
name: string;
|
|
59
|
+
api?: Api;
|
|
60
|
+
reasoning: boolean;
|
|
61
|
+
input: Array<"text" | "image">;
|
|
62
|
+
cost: {
|
|
63
|
+
input: number;
|
|
64
|
+
output: number;
|
|
65
|
+
cacheRead: number;
|
|
66
|
+
cacheWrite: number;
|
|
67
|
+
};
|
|
68
|
+
contextWindow: number;
|
|
69
|
+
maxTokens: number;
|
|
70
|
+
headers?: Record<string, string>;
|
|
49
71
|
}
|
|
50
72
|
|
|
51
73
|
interface BuiltTools {
|
|
@@ -53,15 +75,100 @@ interface BuiltTools {
|
|
|
53
75
|
customTools: ToolDefinition[];
|
|
54
76
|
}
|
|
55
77
|
|
|
78
|
+
const DEFAULT_PI_PROVIDER_NAME = "custom-openai";
|
|
79
|
+
const DEFAULT_PI_PROVIDER_API = "openai-completions";
|
|
80
|
+
const DEFAULT_PI_MODEL_COST = {
|
|
81
|
+
input: 0,
|
|
82
|
+
output: 0,
|
|
83
|
+
cacheRead: 0,
|
|
84
|
+
cacheWrite: 0,
|
|
85
|
+
} as const;
|
|
86
|
+
|
|
87
|
+
const PI_PROVIDER_APIS = [
|
|
88
|
+
"anthropic-messages",
|
|
89
|
+
"openai-completions",
|
|
90
|
+
"mistral-conversations",
|
|
91
|
+
"openai-responses",
|
|
92
|
+
"azure-openai-responses",
|
|
93
|
+
"openai-codex-responses",
|
|
94
|
+
"google-generative-ai",
|
|
95
|
+
"google-gemini-cli",
|
|
96
|
+
"google-vertex",
|
|
97
|
+
"bedrock-converse-stream",
|
|
98
|
+
] as const;
|
|
99
|
+
|
|
100
|
+
const piProviderApiSchema = z.enum(PI_PROVIDER_APIS);
|
|
101
|
+
|
|
102
|
+
const piProviderModelSchema = z.object({
|
|
103
|
+
id: z.string().min(1),
|
|
104
|
+
name: z.string().min(1).optional(),
|
|
105
|
+
api: piProviderApiSchema.optional(),
|
|
106
|
+
reasoning: z.boolean().default(false),
|
|
107
|
+
input: z.array(z.enum(["text", "image"])).min(1).default(["text"]),
|
|
108
|
+
cost: z.object({
|
|
109
|
+
input: z.number().default(0),
|
|
110
|
+
output: z.number().default(0),
|
|
111
|
+
cacheRead: z.number().default(0),
|
|
112
|
+
cacheWrite: z.number().default(0),
|
|
113
|
+
}).default(DEFAULT_PI_MODEL_COST),
|
|
114
|
+
contextWindow: z.number().int().positive().default(128000),
|
|
115
|
+
maxTokens: z.number().int().positive().default(16384),
|
|
116
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
117
|
+
});
|
|
118
|
+
|
|
56
119
|
const piProviderConfigSchema = z.object({
|
|
57
|
-
provider: z.string().min(1),
|
|
120
|
+
provider: z.string().min(1).optional(),
|
|
58
121
|
model: z.string().min(1),
|
|
59
|
-
|
|
122
|
+
baseUrl: z.string().min(1),
|
|
123
|
+
apiKey: z.string().min(1),
|
|
124
|
+
api: piProviderApiSchema.optional(),
|
|
125
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
126
|
+
authHeader: z.boolean().optional(),
|
|
127
|
+
models: z.array(piProviderModelSchema).min(1).optional(),
|
|
128
|
+
thinkingLevel: z.enum(["off", "minimal", "low", "medium", "high", "xhigh"]).optional(),
|
|
60
129
|
agentDir: z.string().optional(),
|
|
61
130
|
authFile: z.string().optional(),
|
|
62
|
-
modelsFile: z.string().default("./models.custom.json"),
|
|
63
131
|
});
|
|
64
132
|
|
|
133
|
+
function buildImplicitModelConfig(modelId: string): PiProviderModelConfig {
|
|
134
|
+
return {
|
|
135
|
+
id: modelId,
|
|
136
|
+
name: modelId,
|
|
137
|
+
reasoning: false,
|
|
138
|
+
input: ["text"],
|
|
139
|
+
cost: { ...DEFAULT_PI_MODEL_COST },
|
|
140
|
+
contextWindow: 128000,
|
|
141
|
+
maxTokens: 16384,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizePiProviderModels(
|
|
146
|
+
activeModelId: string,
|
|
147
|
+
models: Array<z.infer<typeof piProviderModelSchema>> | undefined,
|
|
148
|
+
): PiProviderModelConfig[] {
|
|
149
|
+
if (!models || models.length === 0) {
|
|
150
|
+
return [buildImplicitModelConfig(activeModelId)];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const normalizedModels = models.map((model) => ({
|
|
154
|
+
id: model.id,
|
|
155
|
+
name: model.name ?? model.id,
|
|
156
|
+
...(model.api ? { api: model.api } : {}),
|
|
157
|
+
reasoning: model.reasoning,
|
|
158
|
+
input: model.input,
|
|
159
|
+
cost: model.cost,
|
|
160
|
+
contextWindow: model.contextWindow,
|
|
161
|
+
maxTokens: model.maxTokens,
|
|
162
|
+
...(model.headers ? { headers: model.headers } : {}),
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
if (!normalizedModels.some((model) => model.id === activeModelId)) {
|
|
166
|
+
throw new Error(`Configured model '${activeModelId}' is not present in provider.pi models`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return normalizedModels;
|
|
170
|
+
}
|
|
171
|
+
|
|
65
172
|
const IMAGE_MIME_TYPES: Record<string, string> = {
|
|
66
173
|
".jpg": "image/jpeg",
|
|
67
174
|
".jpeg": "image/jpeg",
|
|
@@ -222,7 +329,26 @@ class PiProviderInstanceImpl implements ProviderInstance {
|
|
|
222
329
|
private readonly logger: ProviderInstanceCreateOptions["host"]["logger"],
|
|
223
330
|
) {
|
|
224
331
|
this.authStorage = AuthStorage.create(providerConfig.authFile);
|
|
225
|
-
|
|
332
|
+
// Point the registry at a never-created file so provider.pi relies solely on inline config.
|
|
333
|
+
this.modelRegistry = new ModelRegistry(this.authStorage, join(dataConfig.stateDir, "__dobby_provider_pi_inline_models__", `${safeSegment(id)}.json`));
|
|
334
|
+
this.modelRegistry.registerProvider(providerConfig.provider, {
|
|
335
|
+
baseUrl: providerConfig.baseUrl,
|
|
336
|
+
apiKey: providerConfig.apiKey,
|
|
337
|
+
api: providerConfig.api,
|
|
338
|
+
...(providerConfig.headers ? { headers: providerConfig.headers } : {}),
|
|
339
|
+
...(providerConfig.authHeader ? { authHeader: true } : {}),
|
|
340
|
+
models: providerConfig.models.map((model) => ({
|
|
341
|
+
id: model.id,
|
|
342
|
+
name: model.name,
|
|
343
|
+
...(model.api ? { api: model.api } : {}),
|
|
344
|
+
reasoning: model.reasoning,
|
|
345
|
+
input: model.input,
|
|
346
|
+
cost: model.cost,
|
|
347
|
+
contextWindow: model.contextWindow,
|
|
348
|
+
maxTokens: model.maxTokens,
|
|
349
|
+
...(model.headers ? { headers: model.headers } : {}),
|
|
350
|
+
})),
|
|
351
|
+
});
|
|
226
352
|
|
|
227
353
|
const model = this.modelRegistry.find(providerConfig.provider, providerConfig.model);
|
|
228
354
|
if (!model) {
|
|
@@ -458,15 +584,19 @@ export const providerPiContribution: ProviderContributionModule = {
|
|
|
458
584
|
const parsed = piProviderConfigSchema.parse(options.config);
|
|
459
585
|
const agentDir = normalizeMaybePath(options.host.configBaseDir, parsed.agentDir);
|
|
460
586
|
const authFile = normalizeMaybePath(options.host.configBaseDir, parsed.authFile);
|
|
461
|
-
const modelsFile = normalizeMaybePath(options.host.configBaseDir, parsed.modelsFile);
|
|
462
587
|
|
|
463
588
|
const normalizedConfig: PiProviderConfig = {
|
|
464
|
-
provider: parsed.provider,
|
|
589
|
+
provider: parsed.provider ?? DEFAULT_PI_PROVIDER_NAME,
|
|
465
590
|
model: parsed.model,
|
|
466
|
-
|
|
591
|
+
baseUrl: parsed.baseUrl,
|
|
592
|
+
apiKey: parsed.apiKey,
|
|
593
|
+
api: parsed.api ?? DEFAULT_PI_PROVIDER_API,
|
|
594
|
+
...(parsed.headers ? { headers: parsed.headers } : {}),
|
|
595
|
+
authHeader: parsed.authHeader ?? false,
|
|
596
|
+
models: normalizePiProviderModels(parsed.model, parsed.models),
|
|
597
|
+
thinkingLevel: parsed.thinkingLevel ?? "off",
|
|
467
598
|
...(agentDir ? { agentDir } : {}),
|
|
468
599
|
...(authFile ? { authFile } : {}),
|
|
469
|
-
...(modelsFile ? { modelsFile } : {}),
|
|
470
600
|
};
|
|
471
601
|
|
|
472
602
|
return new PiProviderInstanceImpl(options.instanceId, normalizedConfig, options.data, options.host.logger);
|
|
@@ -16,6 +16,45 @@ interface DoctorIssue {
|
|
|
16
16
|
message: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
interface PlaceholderHit {
|
|
20
|
+
path: string;
|
|
21
|
+
value: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isPlaceholderValue(value: unknown): value is string {
|
|
25
|
+
if (typeof value !== "string") {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const normalized = value.trim().toUpperCase();
|
|
29
|
+
return normalized.includes("REPLACE_WITH_") || normalized.includes("YOUR_");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isCredentialLikeKey(key: string): boolean {
|
|
33
|
+
return /(?:token|secret|api[-_]?key|appid|appsecret)/i.test(key);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function walkPlaceholders(value: unknown, path: string): PlaceholderHit[] {
|
|
37
|
+
if (isPlaceholderValue(value)) {
|
|
38
|
+
return [{ path, value }];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
return value.flatMap((item, index) => walkPlaceholders(item, `${path}[${index}]`));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!value || typeof value !== "object") {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return Object.entries(value).flatMap(([key, nested]) => walkPlaceholders(nested, `${path}.${key}`));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function lastPathSegment(path: string): string {
|
|
53
|
+
const withoutIndexes = path.replaceAll(/\[\d+\]/g, "");
|
|
54
|
+
const segments = withoutIndexes.split(".");
|
|
55
|
+
return segments[segments.length - 1] ?? withoutIndexes;
|
|
56
|
+
}
|
|
57
|
+
|
|
19
58
|
function expandHome(value: string): string {
|
|
20
59
|
if (value === "~") {
|
|
21
60
|
return homedir();
|
|
@@ -102,6 +141,17 @@ export async function runDoctorCommand(options: {
|
|
|
102
141
|
message: `providers.items['${instanceId}'] references missing contribution '${instance.type}'`,
|
|
103
142
|
});
|
|
104
143
|
}
|
|
144
|
+
|
|
145
|
+
for (const hit of walkPlaceholders(instance, `providers.items['${instanceId}']`)) {
|
|
146
|
+
if (hit.path.endsWith(".type")) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
issues.push({
|
|
151
|
+
level: isCredentialLikeKey(lastPathSegment(hit.path)) ? "error" : "warning",
|
|
152
|
+
message: `${hit.path} still uses placeholder value '${hit.value}'`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
105
155
|
}
|
|
106
156
|
|
|
107
157
|
for (const [instanceId, instance] of Object.entries(normalized.connectors.items)) {
|
|
@@ -112,6 +162,17 @@ export async function runDoctorCommand(options: {
|
|
|
112
162
|
});
|
|
113
163
|
}
|
|
114
164
|
|
|
165
|
+
for (const hit of walkPlaceholders(instance, `connectors.items['${instanceId}']`)) {
|
|
166
|
+
if (hit.path.endsWith(".type")) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
issues.push({
|
|
171
|
+
level: isCredentialLikeKey(lastPathSegment(hit.path)) ? "error" : "warning",
|
|
172
|
+
message: `${hit.path} still uses placeholder value '${hit.value}'`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
115
176
|
if (instance.type === DISCORD_CONNECTOR_CONTRIBUTION_ID) {
|
|
116
177
|
const botName = typeof instance.botName === "string" ? instance.botName.trim() : "";
|
|
117
178
|
const botToken = typeof instance.botToken === "string" ? instance.botToken.trim() : "";
|
|
@@ -139,18 +200,51 @@ export async function runDoctorCommand(options: {
|
|
|
139
200
|
}
|
|
140
201
|
}
|
|
141
202
|
|
|
203
|
+
if (normalized.routes.default.projectRoot && isPlaceholderValue(normalized.routes.default.projectRoot)) {
|
|
204
|
+
issues.push({
|
|
205
|
+
level: "warning",
|
|
206
|
+
message: `routes.default.projectRoot still uses placeholder value '${normalized.routes.default.projectRoot}'`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
142
210
|
for (const [routeId, route] of Object.entries(normalized.routes.items)) {
|
|
211
|
+
const effectiveProjectRoot = route.projectRoot ?? normalized.routes.default.projectRoot;
|
|
212
|
+
const projectRootSource = route.projectRoot ? `routes.items['${routeId}'].projectRoot` : "routes.default.projectRoot";
|
|
213
|
+
|
|
214
|
+
if (!effectiveProjectRoot) {
|
|
215
|
+
issues.push({
|
|
216
|
+
level: "error",
|
|
217
|
+
message: `routes.items['${routeId}'].projectRoot is required when routes.default.projectRoot is not set`,
|
|
218
|
+
});
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (isPlaceholderValue(effectiveProjectRoot)) {
|
|
223
|
+
issues.push({
|
|
224
|
+
level: "warning",
|
|
225
|
+
message: `${projectRootSource} still uses placeholder value '${effectiveProjectRoot}'`,
|
|
226
|
+
});
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
143
230
|
try {
|
|
144
|
-
const projectRootPath = resolveRouteProjectRoot(configPath,
|
|
231
|
+
const projectRootPath = resolveRouteProjectRoot(configPath, effectiveProjectRoot);
|
|
145
232
|
await access(projectRootPath);
|
|
146
233
|
} catch {
|
|
147
234
|
issues.push({
|
|
148
235
|
level: "warning",
|
|
149
|
-
message:
|
|
236
|
+
message: `${projectRootSource} does not exist: ${effectiveProjectRoot}`,
|
|
150
237
|
});
|
|
151
238
|
}
|
|
152
239
|
}
|
|
153
240
|
|
|
241
|
+
if (normalized.bindings.default && !normalized.routes.items[normalized.bindings.default.route]) {
|
|
242
|
+
issues.push({
|
|
243
|
+
level: "error",
|
|
244
|
+
message: `bindings.default.route references unknown route '${normalized.bindings.default.route}'`,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
154
248
|
const seenBindingSources = new Map<string, string>();
|
|
155
249
|
for (const [bindingId, binding] of Object.entries(normalized.bindings.items)) {
|
|
156
250
|
if (!normalized.connectors.items[binding.connector]) {
|
|
@@ -166,6 +260,13 @@ export async function runDoctorCommand(options: {
|
|
|
166
260
|
});
|
|
167
261
|
}
|
|
168
262
|
|
|
263
|
+
if (isPlaceholderValue(binding.source.id)) {
|
|
264
|
+
issues.push({
|
|
265
|
+
level: "warning",
|
|
266
|
+
message: `bindings.items['${bindingId}'].source.id still uses placeholder value '${binding.source.id}'`,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
169
270
|
const bindingKey = `${binding.connector}:${binding.source.type}:${binding.source.id}`;
|
|
170
271
|
const existingBindingId = seenBindingSources.get(bindingKey);
|
|
171
272
|
if (existingBindingId) {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "../shared/config-mutators.js";
|
|
13
13
|
import { readRawConfig, requireRawConfig, resolveConfigPath, resolveDataRootDir, writeConfigWithValidation } from "../shared/config-io.js";
|
|
14
14
|
import type { RawGatewayConfig } from "../shared/config-types.js";
|
|
15
|
+
import { resolveExtensionInstallSpecs } from "../shared/local-extension-specs.js";
|
|
15
16
|
import { createLogger } from "../shared/runtime.js";
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -42,7 +43,8 @@ export async function runExtensionInstallCommand(options: {
|
|
|
42
43
|
|
|
43
44
|
const rawConfig = (await readRawConfig(configPath)) ?? {};
|
|
44
45
|
const manager = new ExtensionStoreManager(logger, extensionStoreDirFromRaw(configPath, rawConfig));
|
|
45
|
-
const
|
|
46
|
+
const [resolvedSpec] = await resolveExtensionInstallSpecs([options.spec]);
|
|
47
|
+
const installed = await manager.install(resolvedSpec ?? options.spec);
|
|
46
48
|
|
|
47
49
|
if (!options.enable) {
|
|
48
50
|
const templates = buildContributionTemplates(installed.manifest.contributions);
|