@gmickel/gno 1.12.4 → 1.13.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 +57 -30
- package/assets/skill/SKILL.md +5 -0
- package/assets/skill/cli-reference.md +16 -6
- package/assets/skill/mcp-reference.md +22 -3
- package/package.json +2 -1
- package/src/app/constants.ts +43 -10
- package/src/app/index-name.ts +127 -0
- package/src/cli/commands/doctor-activation.ts +151 -0
- package/src/cli/commands/doctor.ts +41 -16
- package/src/cli/commands/get.ts +18 -0
- package/src/cli/commands/mcp/atomic-config-write.ts +118 -0
- package/src/cli/commands/mcp/config-discovery.ts +42 -0
- package/src/cli/commands/mcp/config-editors.ts +432 -0
- package/src/cli/commands/mcp/config.ts +63 -160
- package/src/cli/commands/mcp/install.ts +75 -37
- package/src/cli/commands/mcp/paths.ts +141 -136
- package/src/cli/commands/mcp/server-entry.ts +66 -0
- package/src/cli/commands/mcp/status.ts +189 -57
- package/src/cli/commands/mcp/target-display.ts +30 -0
- package/src/cli/commands/mcp/uninstall.ts +29 -31
- package/src/cli/commands/mcp/yaml-config-editor.ts +257 -0
- package/src/cli/commands/mcp/yaml-layout-scanner.ts +447 -0
- package/src/cli/commands/multi-get.ts +31 -6
- package/src/cli/commands/status.ts +107 -11
- package/src/cli/program.ts +66 -20
- package/src/core/activation-connector-health.ts +19 -0
- package/src/core/activation-probe-plan.ts +321 -0
- package/src/core/activation-probe.ts +138 -0
- package/src/core/activation-receipt-store.ts +39 -0
- package/src/core/activation-status.ts +513 -0
- package/src/core/activation-verifier.ts +416 -0
- package/src/core/connector-environment.ts +68 -0
- package/src/core/connector-policy.ts +233 -0
- package/src/core/connector-verification-target.ts +150 -0
- package/src/core/connector-verifier.ts +497 -0
- package/src/core/indexed-reference.ts +33 -8
- package/src/core/runtime-entrypoint.ts +24 -0
- package/src/mcp/activation-verification-mode.ts +4 -0
- package/src/mcp/server.ts +9 -2
- package/src/sdk/client.ts +7 -0
- package/src/sdk/types.ts +1 -0
- package/src/serve/activation-health.ts +91 -0
- package/src/serve/background-runtime.ts +11 -1
- package/src/serve/connectors.ts +164 -19
- package/src/serve/public/components/BootstrapStatus.tsx +94 -1
- package/src/serve/public/components/FirstRunWizard.tsx +13 -51
- package/src/serve/public/components/HealthCenter.tsx +8 -2
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Connectors.tsx +216 -55
- package/src/serve/public/pages/Dashboard.tsx +1 -0
- package/src/serve/routes/api.ts +152 -8
- package/src/serve/server.ts +44 -9
- package/src/serve/status-model.ts +4 -0
- package/src/serve/status.ts +79 -35
- package/src/store/activation-receipts.ts +390 -0
- package/src/store/index.ts +8 -0
- package/src/store/migrations/012-activation-receipts.ts +38 -0
- package/src/store/migrations/013-fts-sync-marker.ts +39 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +313 -53
- package/src/store/types.ts +118 -0
|
@@ -4,22 +4,31 @@
|
|
|
4
4
|
* @module src/cli/commands/mcp/status
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type { ConnectorWorkspaceEnvironment } from "../../../core/connector-environment";
|
|
8
|
+
import type { McpConnectorVerificationTarget } from "../../../core/connector-verifier";
|
|
9
|
+
import type { StandardMcpEntry } from "./config.js";
|
|
10
|
+
|
|
11
|
+
import { normalizeConnectorWorkspaceEnvironment } from "../../../core/connector-environment";
|
|
12
|
+
import { CliError } from "../../errors.js";
|
|
7
13
|
import { getGlobals } from "../../program.js";
|
|
8
14
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
configPathEntryExists,
|
|
16
|
+
resolveMcpConfigLocation,
|
|
17
|
+
} from "./config-discovery.js";
|
|
18
|
+
import {
|
|
19
|
+
getJsoncServerEntry,
|
|
20
|
+
getTomlServerEntry,
|
|
21
|
+
getYamlServerEntry,
|
|
22
|
+
} from "./config-editors.js";
|
|
23
|
+
import { getServersKey } from "./config.js";
|
|
15
24
|
import {
|
|
25
|
+
getTargetScopes,
|
|
16
26
|
getTargetDisplayName,
|
|
17
|
-
MCP_SERVER_NAME,
|
|
18
27
|
MCP_TARGETS,
|
|
28
|
+
type McpConfigFormat,
|
|
19
29
|
type McpScope,
|
|
20
30
|
type McpTarget,
|
|
21
31
|
resolveMcpConfigPath,
|
|
22
|
-
TARGETS_WITH_PROJECT_SCOPE,
|
|
23
32
|
} from "./paths.js";
|
|
24
33
|
|
|
25
34
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -42,10 +51,40 @@ export interface McpTargetStatus {
|
|
|
42
51
|
scope: McpScope;
|
|
43
52
|
configPath: string;
|
|
44
53
|
configured: boolean;
|
|
45
|
-
serverEntry?: {
|
|
54
|
+
serverEntry?: {
|
|
55
|
+
command: string;
|
|
56
|
+
args: string[];
|
|
57
|
+
env?: ConnectorWorkspaceEnvironment;
|
|
58
|
+
};
|
|
46
59
|
error?: string;
|
|
47
60
|
}
|
|
48
61
|
|
|
62
|
+
/** Verification-only identity; deliberately omitted from status JSON output. */
|
|
63
|
+
const configIdentityByStatus = new WeakMap<McpTargetStatus, string>();
|
|
64
|
+
|
|
65
|
+
/** Project a parsed target status into the read-only activation verifier seam. */
|
|
66
|
+
export function toMcpConnectorVerificationTarget(
|
|
67
|
+
id: string,
|
|
68
|
+
status: McpTargetStatus
|
|
69
|
+
): McpConnectorVerificationTarget {
|
|
70
|
+
const serverEntry = normalizeEntry(status.serverEntry, "standard");
|
|
71
|
+
const configured = status.configured && serverEntry !== null;
|
|
72
|
+
const configIdentity = configIdentityByStatus.get(status);
|
|
73
|
+
return {
|
|
74
|
+
kind: "mcp",
|
|
75
|
+
id,
|
|
76
|
+
target: status.target,
|
|
77
|
+
scope: status.scope,
|
|
78
|
+
configPath: status.configPath,
|
|
79
|
+
configured,
|
|
80
|
+
...(serverEntry ? { serverEntry } : {}),
|
|
81
|
+
...(configIdentity ? { configIdentity } : {}),
|
|
82
|
+
...(status.error || (status.configured && !serverEntry)
|
|
83
|
+
? { configError: true }
|
|
84
|
+
: {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
49
88
|
interface StatusResult {
|
|
50
89
|
targets: McpTargetStatus[];
|
|
51
90
|
summary: {
|
|
@@ -62,14 +101,71 @@ interface StatusResult {
|
|
|
62
101
|
* Normalize entry to standard format for display.
|
|
63
102
|
*/
|
|
64
103
|
function normalizeEntry(
|
|
65
|
-
entry:
|
|
66
|
-
|
|
67
|
-
|
|
104
|
+
entry: unknown,
|
|
105
|
+
configFormat: McpConfigFormat
|
|
106
|
+
): StandardMcpEntry | null {
|
|
107
|
+
if (!entry || typeof entry !== "object") {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const record = entry as Record<string, unknown>;
|
|
111
|
+
if (configFormat === "mcp") {
|
|
68
112
|
// OpenCode format: command is array [command, ...args]
|
|
69
|
-
|
|
70
|
-
|
|
113
|
+
if (
|
|
114
|
+
record.type !== "local" ||
|
|
115
|
+
(record.enabled !== undefined && record.enabled !== true) ||
|
|
116
|
+
!Array.isArray(record.command) ||
|
|
117
|
+
record.command.length === 0 ||
|
|
118
|
+
!record.command.every((part) => typeof part === "string") ||
|
|
119
|
+
Object.keys(record).some(
|
|
120
|
+
(key) =>
|
|
121
|
+
key !== "type" &&
|
|
122
|
+
key !== "command" &&
|
|
123
|
+
key !== "enabled" &&
|
|
124
|
+
key !== "environment"
|
|
125
|
+
)
|
|
126
|
+
) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const [command = "", ...args] = record.command;
|
|
130
|
+
if (!command) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const env = normalizeConnectorWorkspaceEnvironment(record.environment);
|
|
134
|
+
if (env === null) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
command,
|
|
139
|
+
args,
|
|
140
|
+
...(Object.keys(env).length > 0 ? { env } : {}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
typeof record.command !== "string" ||
|
|
145
|
+
record.command.length === 0 ||
|
|
146
|
+
!Array.isArray(record.args) ||
|
|
147
|
+
!record.args.every((argument) => typeof argument === "string") ||
|
|
148
|
+
Object.keys(record).some(
|
|
149
|
+
(key) => key !== "command" && key !== "args" && key !== "env"
|
|
150
|
+
)
|
|
151
|
+
) {
|
|
152
|
+
return null;
|
|
71
153
|
}
|
|
72
|
-
|
|
154
|
+
const env = normalizeConnectorWorkspaceEnvironment(record.env);
|
|
155
|
+
if (env === null) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
command: record.command,
|
|
160
|
+
args: record.args,
|
|
161
|
+
...(Object.keys(env).length > 0 ? { env } : {}),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function entryIdentity(entry: unknown): string {
|
|
166
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
167
|
+
hasher.update(JSON.stringify(entry) ?? "undefined");
|
|
168
|
+
return hasher.digest("hex");
|
|
73
169
|
}
|
|
74
170
|
|
|
75
171
|
export async function checkMcpTargetStatus(
|
|
@@ -79,46 +175,84 @@ export async function checkMcpTargetStatus(
|
|
|
79
175
|
): Promise<McpTargetStatus> {
|
|
80
176
|
const { cwd, homeDir } = options;
|
|
81
177
|
|
|
82
|
-
const
|
|
178
|
+
const resolvedPaths = resolveMcpConfigPath({
|
|
83
179
|
target,
|
|
84
180
|
scope,
|
|
85
181
|
cwd,
|
|
86
182
|
homeDir,
|
|
87
183
|
});
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
const exists = await file.exists();
|
|
91
|
-
|
|
92
|
-
if (!exists) {
|
|
93
|
-
return { target, scope, configPath, configured: false };
|
|
94
|
-
}
|
|
184
|
+
let configPath = resolvedPaths.configPath;
|
|
185
|
+
const { configFormat } = resolvedPaths;
|
|
95
186
|
|
|
96
187
|
try {
|
|
97
|
-
|
|
188
|
+
configPath = await resolveMcpConfigLocation(resolvedPaths);
|
|
189
|
+
if (!(await configPathEntryExists(configPath))) {
|
|
190
|
+
return { target, scope, configPath, configured: false };
|
|
191
|
+
}
|
|
192
|
+
const content = await Bun.file(configPath).text();
|
|
98
193
|
if (!content.trim()) {
|
|
99
194
|
return { target, scope, configPath, configured: false };
|
|
100
195
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
196
|
+
const serversKey =
|
|
197
|
+
configFormat === "codex_toml"
|
|
198
|
+
? "mcp_servers"
|
|
199
|
+
: getServersKey(configFormat);
|
|
200
|
+
const parsed =
|
|
201
|
+
configFormat === "codex_toml"
|
|
202
|
+
? getTomlServerEntry(content, configPath)
|
|
203
|
+
: configFormat === "yaml_standard"
|
|
204
|
+
? getYamlServerEntry(content, configPath, serversKey)
|
|
205
|
+
: getJsoncServerEntry(content, configPath, serversKey);
|
|
206
|
+
if (!parsed.exists) {
|
|
207
|
+
return { target, scope, configPath, configured: false };
|
|
111
208
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
209
|
+
const entry = parsed.entry;
|
|
210
|
+
if (
|
|
211
|
+
configFormat === "mcp" &&
|
|
212
|
+
entry &&
|
|
213
|
+
typeof entry === "object" &&
|
|
214
|
+
(entry as { enabled?: unknown }).enabled === false
|
|
215
|
+
) {
|
|
216
|
+
return { target, scope, configPath, configured: false };
|
|
217
|
+
}
|
|
218
|
+
const configIdentity = entryIdentity(entry);
|
|
219
|
+
const serverEntry = normalizeEntry(entry, configFormat);
|
|
220
|
+
if (!serverEntry) {
|
|
221
|
+
const status: McpTargetStatus = {
|
|
222
|
+
target,
|
|
223
|
+
scope,
|
|
224
|
+
configPath,
|
|
225
|
+
configured: false,
|
|
226
|
+
error: "Malformed MCP server entry",
|
|
227
|
+
};
|
|
228
|
+
configIdentityByStatus.set(status, configIdentity);
|
|
229
|
+
return status;
|
|
230
|
+
}
|
|
231
|
+
const status: McpTargetStatus = {
|
|
232
|
+
target,
|
|
233
|
+
scope,
|
|
234
|
+
configPath,
|
|
235
|
+
configured: true,
|
|
236
|
+
serverEntry,
|
|
237
|
+
};
|
|
238
|
+
configIdentityByStatus.set(status, configIdentity);
|
|
239
|
+
return status;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
const format =
|
|
242
|
+
configFormat === "codex_toml"
|
|
243
|
+
? "TOML"
|
|
244
|
+
: configFormat === "yaml_standard"
|
|
245
|
+
? "YAML"
|
|
246
|
+
: "JSON";
|
|
116
247
|
return {
|
|
117
248
|
target,
|
|
118
249
|
scope,
|
|
119
250
|
configPath,
|
|
120
251
|
configured: false,
|
|
121
|
-
error:
|
|
252
|
+
error:
|
|
253
|
+
error instanceof Error && error.message.startsWith("Ambiguous MCP")
|
|
254
|
+
? error.message
|
|
255
|
+
: `Malformed ${format}`,
|
|
122
256
|
};
|
|
123
257
|
}
|
|
124
258
|
}
|
|
@@ -148,31 +282,29 @@ export async function statusMcp(opts: StatusOptions = {}): Promise<void> {
|
|
|
148
282
|
const globals = safeGetGlobals();
|
|
149
283
|
const json = opts.json ?? globals.json;
|
|
150
284
|
|
|
285
|
+
if (
|
|
286
|
+
targetFilter !== "all" &&
|
|
287
|
+
scopeFilter !== "all" &&
|
|
288
|
+
!getTargetScopes(targetFilter).includes(scopeFilter)
|
|
289
|
+
) {
|
|
290
|
+
throw new CliError(
|
|
291
|
+
"VALIDATION",
|
|
292
|
+
`${getTargetDisplayName(targetFilter)} does not support ${scopeFilter} scope.`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
151
296
|
const targets: McpTarget[] =
|
|
152
297
|
targetFilter === "all" ? MCP_TARGETS : [targetFilter];
|
|
153
298
|
|
|
154
299
|
const results: McpTargetStatus[] = [];
|
|
155
300
|
|
|
156
301
|
for (const target of targets) {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const scopes: McpScope[] =
|
|
162
|
-
scopeFilter === "all" ? ["user", "project"] : [scopeFilter];
|
|
163
|
-
|
|
164
|
-
for (const scope of scopes) {
|
|
165
|
-
results.push(
|
|
166
|
-
await checkMcpTargetStatus(target, scope, {
|
|
167
|
-
cwd: opts.cwd,
|
|
168
|
-
homeDir: opts.homeDir,
|
|
169
|
-
})
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
} else if (scopeFilter === "all" || scopeFilter === "user") {
|
|
173
|
-
// User scope only - skip if filtering by project
|
|
302
|
+
const scopes = getTargetScopes(target).filter(
|
|
303
|
+
(scope) => scopeFilter === "all" || scope === scopeFilter
|
|
304
|
+
);
|
|
305
|
+
for (const scope of scopes) {
|
|
174
306
|
results.push(
|
|
175
|
-
await checkMcpTargetStatus(target,
|
|
307
|
+
await checkMcpTargetStatus(target, scope, {
|
|
176
308
|
cwd: opts.cwd,
|
|
177
309
|
homeDir: opts.homeDir,
|
|
178
310
|
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { McpTarget } from "./paths.js";
|
|
2
|
+
|
|
3
|
+
export function getTargetDisplayName(target: McpTarget): string {
|
|
4
|
+
switch (target) {
|
|
5
|
+
case "claude-desktop":
|
|
6
|
+
return "Claude Desktop";
|
|
7
|
+
case "claude-code":
|
|
8
|
+
return "Claude Code";
|
|
9
|
+
case "codex":
|
|
10
|
+
return "Codex";
|
|
11
|
+
case "cursor":
|
|
12
|
+
return "Cursor";
|
|
13
|
+
case "zed":
|
|
14
|
+
return "Zed";
|
|
15
|
+
case "windsurf":
|
|
16
|
+
return "Windsurf";
|
|
17
|
+
case "opencode":
|
|
18
|
+
return "OpenCode";
|
|
19
|
+
case "amp":
|
|
20
|
+
return "Amp";
|
|
21
|
+
case "lmstudio":
|
|
22
|
+
return "LM Studio";
|
|
23
|
+
case "librechat":
|
|
24
|
+
return "LibreChat";
|
|
25
|
+
default: {
|
|
26
|
+
const exhaustive: never = target;
|
|
27
|
+
throw new Error(`Unknown target: ${String(exhaustive)}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -6,20 +6,24 @@
|
|
|
6
6
|
|
|
7
7
|
import { CliError } from "../../errors.js";
|
|
8
8
|
import { getGlobals } from "../../program.js";
|
|
9
|
+
import { resolveMcpConfigLocation } from "./config-discovery.js";
|
|
9
10
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
removeJsoncServerEntry,
|
|
12
|
+
removeTomlServerEntry,
|
|
13
|
+
removeYamlServerEntry,
|
|
14
|
+
} from "./config-editors.js";
|
|
15
|
+
import {
|
|
16
|
+
getServersKey,
|
|
17
|
+
readMcpConfigText,
|
|
18
|
+
writeMcpConfigText,
|
|
15
19
|
} from "./config.js";
|
|
16
20
|
import {
|
|
21
|
+
getDefaultTargetScope,
|
|
22
|
+
getTargetScopes,
|
|
17
23
|
getTargetDisplayName,
|
|
18
|
-
MCP_SERVER_NAME,
|
|
19
24
|
type McpScope,
|
|
20
25
|
type McpTarget,
|
|
21
26
|
resolveMcpConfigPath,
|
|
22
|
-
TARGETS_WITH_PROJECT_SCOPE,
|
|
23
27
|
} from "./paths.js";
|
|
24
28
|
|
|
25
29
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -60,38 +64,32 @@ async function uninstallFromTarget(
|
|
|
60
64
|
): Promise<UninstallResult> {
|
|
61
65
|
const { cwd, homeDir } = options;
|
|
62
66
|
|
|
63
|
-
const
|
|
67
|
+
const resolvedPaths = resolveMcpConfigPath({
|
|
64
68
|
target,
|
|
65
69
|
scope,
|
|
66
70
|
cwd,
|
|
67
71
|
homeDir,
|
|
68
72
|
});
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
const
|
|
73
|
+
const configPath = await resolveMcpConfigLocation(resolvedPaths);
|
|
74
|
+
const { configFormat } = resolvedPaths;
|
|
75
|
+
const content = await readMcpConfigText(configPath, {
|
|
72
76
|
returnNullOnMissing: true,
|
|
73
|
-
yaml: useYaml,
|
|
74
77
|
});
|
|
75
|
-
|
|
76
|
-
// File doesn't exist
|
|
77
|
-
if (config === null) {
|
|
78
|
+
if (content === null) {
|
|
78
79
|
return { target, scope, configPath, action: "not_found" };
|
|
79
80
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (!removed) {
|
|
81
|
+
const serversKey =
|
|
82
|
+
configFormat === "codex_toml" ? "mcp_servers" : getServersKey(configFormat);
|
|
83
|
+
const result =
|
|
84
|
+
configFormat === "codex_toml"
|
|
85
|
+
? removeTomlServerEntry(content, configPath)
|
|
86
|
+
: configFormat === "yaml_standard"
|
|
87
|
+
? removeYamlServerEntry(content, configPath, serversKey)
|
|
88
|
+
: removeJsoncServerEntry(content, configPath, serversKey);
|
|
89
|
+
if (!result.removed) {
|
|
89
90
|
return { target, scope, configPath, action: "not_found" };
|
|
90
91
|
}
|
|
91
|
-
|
|
92
|
-
// Write back
|
|
93
|
-
await writeMcpConfig(configPath, config as AnyMcpConfig, { yaml: useYaml });
|
|
94
|
-
|
|
92
|
+
await writeMcpConfigText(configPath, result.content);
|
|
95
93
|
return { target, scope, configPath, action: "removed" };
|
|
96
94
|
}
|
|
97
95
|
|
|
@@ -111,16 +109,16 @@ function safeGetGlobals(): { json: boolean; quiet: boolean } {
|
|
|
111
109
|
*/
|
|
112
110
|
export async function uninstallMcp(opts: UninstallOptions = {}): Promise<void> {
|
|
113
111
|
const target = opts.target ?? "claude-desktop";
|
|
114
|
-
const scope = opts.scope ??
|
|
112
|
+
const scope = opts.scope ?? getDefaultTargetScope(target);
|
|
115
113
|
const globals = safeGetGlobals();
|
|
116
114
|
const json = opts.json ?? globals.json;
|
|
117
115
|
const quiet = opts.quiet ?? globals.quiet;
|
|
118
116
|
|
|
119
117
|
// Validate scope - only some targets support project scope
|
|
120
|
-
if (
|
|
118
|
+
if (!getTargetScopes(target).includes(scope)) {
|
|
121
119
|
throw new CliError(
|
|
122
120
|
"VALIDATION",
|
|
123
|
-
`${getTargetDisplayName(target)} does not support
|
|
121
|
+
`${getTargetDisplayName(target)} does not support ${scope} scope.`
|
|
124
122
|
);
|
|
125
123
|
}
|
|
126
124
|
|