@blogic-cz/agent-tools 0.18.1 → 1.0.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 +23 -6
- package/dist/az-tool/config.d.ts +60 -12
- package/dist/az-tool/config.d.ts.map +1 -1
- package/dist/az-tool/errors.d.ts +10 -1
- package/dist/az-tool/errors.d.ts.map +1 -1
- package/dist/az-tool/profile.d.ts +16 -0
- package/dist/az-tool/profile.d.ts.map +1 -0
- package/dist/az-tool/security.d.ts +20 -3
- package/dist/az-tool/security.d.ts.map +1 -1
- package/dist/az-tool/service.d.ts +9 -4
- package/dist/az-tool/service.d.ts.map +1 -1
- package/dist/az-tool/types.d.ts +10 -60
- package/dist/az-tool/types.d.ts.map +1 -1
- package/dist/{az-tool → azdo-tool}/build.d.ts +9 -9
- package/dist/azdo-tool/build.d.ts.map +1 -0
- package/dist/azdo-tool/config.d.ts +13 -0
- package/dist/azdo-tool/config.d.ts.map +1 -0
- package/dist/azdo-tool/errors.d.ts +43 -0
- package/dist/azdo-tool/errors.d.ts.map +1 -0
- package/dist/azdo-tool/extract-option-value.d.ts.map +1 -0
- package/dist/azdo-tool/index.d.ts +3 -0
- package/dist/azdo-tool/index.d.ts.map +1 -0
- package/dist/azdo-tool/security.d.ts +4 -0
- package/dist/azdo-tool/security.d.ts.map +1 -0
- package/dist/azdo-tool/service.d.ts +15 -0
- package/dist/azdo-tool/service.d.ts.map +1 -0
- package/dist/azdo-tool/transformers.d.ts.map +1 -0
- package/dist/azdo-tool/types.d.ts +64 -0
- package/dist/azdo-tool/types.d.ts.map +1 -0
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/types.d.ts +18 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/credential-guard/index.d.ts.map +1 -1
- package/dist/shared/azure-credentials.d.ts +20 -0
- package/dist/shared/azure-credentials.d.ts.map +1 -0
- package/dist/shared/binary-preflight.d.ts +1 -1
- package/package.json +8 -2
- package/schemas/agent-tools.schema.json +42 -1
- package/src/az-tool/config.ts +135 -25
- package/src/az-tool/errors.ts +14 -1
- package/src/az-tool/index.ts +67 -134
- package/src/az-tool/profile.ts +48 -0
- package/src/az-tool/security.ts +248 -89
- package/src/az-tool/service.ts +137 -242
- package/src/az-tool/types.ts +11 -64
- package/src/{az-tool → azdo-tool}/build.ts +11 -11
- package/src/azdo-tool/config.ts +33 -0
- package/src/azdo-tool/errors.ts +41 -0
- package/src/azdo-tool/index.ts +222 -0
- package/src/azdo-tool/security.ts +157 -0
- package/src/azdo-tool/service.ts +322 -0
- package/src/azdo-tool/types.ts +67 -0
- package/src/config/index.ts +1 -0
- package/src/config/loader.ts +10 -1
- package/src/config/types.ts +19 -1
- package/src/credential-guard/index.ts +7 -2
- package/src/shared/azure-credentials.ts +50 -0
- package/src/shared/binary-preflight.ts +1 -1
- package/dist/az-tool/build.d.ts.map +0 -1
- package/dist/az-tool/extract-option-value.d.ts.map +0 -1
- package/dist/az-tool/transformers.d.ts.map +0 -1
- /package/dist/{az-tool → azdo-tool}/extract-option-value.d.ts +0 -0
- /package/dist/{az-tool → azdo-tool}/transformers.d.ts +0 -0
- /package/src/{az-tool → azdo-tool}/extract-option-value.ts +0 -0
- /package/src/{az-tool → azdo-tool}/transformers.ts +0 -0
package/src/az-tool/service.ts
CHANGED
|
@@ -1,50 +1,112 @@
|
|
|
1
1
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
2
2
|
import { Context, Effect, Layer, Option, Stream } from "effect";
|
|
3
3
|
|
|
4
|
-
import type {
|
|
5
|
-
import type { AzureConfig } from "#config/types";
|
|
4
|
+
import type { AzurePlatformConfig } from "#config/types";
|
|
6
5
|
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
6
|
+
import type { AzParseError } from "./errors";
|
|
7
|
+
|
|
8
|
+
import { AzCommandError, AzProfileError, AzSecurityError, AzTimeoutError } from "./errors";
|
|
9
|
+
import { isProductionProfile, selectAzProfileName } from "./profile";
|
|
10
|
+
import { isAzCommandAllowed } from "./security";
|
|
11
11
|
import { ConfigService, getToolConfig } from "#config";
|
|
12
|
-
import {
|
|
12
|
+
import { missingBinaryFromSpawnFailure } from "#shared/binary-preflight";
|
|
13
|
+
import { renderCommandLine } from "#shared/exec";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_TIMEOUT_MS = 60000;
|
|
16
|
+
|
|
17
|
+
const hasOutputFlag = (argv: readonly string[]): boolean =>
|
|
18
|
+
argv.some((arg) => arg === "-o" || arg === "--output" || arg.startsWith("--output="));
|
|
19
|
+
|
|
20
|
+
export type AzCommandResult = {
|
|
21
|
+
readonly command: string;
|
|
22
|
+
readonly subscription: string | undefined;
|
|
23
|
+
readonly data: unknown;
|
|
24
|
+
};
|
|
13
25
|
|
|
14
26
|
export class AzService extends Context.Service<
|
|
15
27
|
AzService,
|
|
16
28
|
{
|
|
17
29
|
readonly runCommand: (
|
|
18
30
|
cmd: string,
|
|
19
|
-
|
|
20
|
-
) => Effect.Effect<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
profile?: string,
|
|
32
|
+
) => Effect.Effect<
|
|
33
|
+
AzCommandResult,
|
|
34
|
+
AzSecurityError | AzCommandError | AzTimeoutError | AzParseError | AzProfileError
|
|
35
|
+
>;
|
|
36
|
+
readonly renderCommand: (
|
|
37
|
+
cmd: string,
|
|
38
|
+
profile?: string,
|
|
39
|
+
) => Effect.Effect<string, AzSecurityError | AzCommandError | AzProfileError>;
|
|
24
40
|
}
|
|
25
41
|
>()("@agent-tools/AzService") {
|
|
26
42
|
static readonly layer = Layer.effect(
|
|
27
43
|
AzService,
|
|
28
44
|
Effect.gen(function* () {
|
|
29
45
|
const config = yield* ConfigService;
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
const executor = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
47
|
+
|
|
48
|
+
const resolveConfig = (profile?: string) =>
|
|
49
|
+
Effect.gen(function* () {
|
|
50
|
+
const azConfig = getToolConfig<AzurePlatformConfig>(config, "azurePlatform", profile);
|
|
51
|
+
|
|
52
|
+
if (!azConfig) {
|
|
53
|
+
return yield* new AzCommandError({
|
|
54
|
+
message:
|
|
55
|
+
"No Azure platform configuration found. Add an 'azurePlatform' section to agent-tools.json5.",
|
|
56
|
+
command: "unknown",
|
|
57
|
+
exitCode: -1,
|
|
58
|
+
hint: "azurePlatform pins the subscription every command runs against, so it is required.",
|
|
59
|
+
nextCommand:
|
|
60
|
+
"echo '{ azurePlatform: { default: { subscription: \"<subscription-id>\" } } }' > agent-tools.json5",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// A production subscription must be named, never inherited from
|
|
65
|
+
// auto-selection or the "default" key.
|
|
66
|
+
const profileName = selectAzProfileName(config?.azurePlatform, profile);
|
|
67
|
+
if (!profile && isProductionProfile(profileName, azConfig)) {
|
|
68
|
+
const label = profileName ?? "default";
|
|
69
|
+
return yield* new AzProfileError({
|
|
70
|
+
message: `Implicit production access blocked. Profile '${label}' is a production profile but --profile was not passed explicitly.`,
|
|
71
|
+
profile: label,
|
|
72
|
+
hint: `Pass --profile ${label} explicitly to confirm production access, or set production: false on that profile.`,
|
|
73
|
+
nextCommand: `agent-tools-az cmd --profile ${label} --cmd "group list"`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return azConfig;
|
|
38
78
|
});
|
|
39
|
-
return {
|
|
40
|
-
runCommand: (_cmd: string, _project?: string) => Effect.fail(noConfigError),
|
|
41
|
-
runInvoke: (_params: InvokeParams) => Effect.fail(noConfigError),
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
79
|
|
|
45
|
-
|
|
80
|
+
/** Security gate plus profile scoping. Shared by dry-run rendering and execution. */
|
|
81
|
+
const buildArgv = (cmd: string, profile?: string) =>
|
|
82
|
+
Effect.gen(function* () {
|
|
83
|
+
const azConfig = yield* resolveConfig(profile);
|
|
84
|
+
const securityCheck = isAzCommandAllowed(cmd, {
|
|
85
|
+
allowedResourceGroups: azConfig.allowedResourceGroups,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!securityCheck.allowed || !securityCheck.argv) {
|
|
89
|
+
return yield* new AzSecurityError({
|
|
90
|
+
message: securityCheck.reason ?? "Command not allowed",
|
|
91
|
+
command: cmd,
|
|
92
|
+
hint:
|
|
93
|
+
securityCheck.hint ??
|
|
94
|
+
"Only read-only Azure platform commands are allowed. Use azdo-tool for Azure DevOps.",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const argv = [...securityCheck.argv, "--only-show-errors"];
|
|
99
|
+
|
|
100
|
+
if (!hasOutputFlag(securityCheck.argv)) {
|
|
101
|
+
argv.push("--output", "json");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
argv.push("--subscription", azConfig.subscription);
|
|
46
105
|
|
|
47
|
-
|
|
106
|
+
return { argv, azConfig };
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const spawnAz = (argv: readonly string[], timeoutMs: number, renderedCommand: string) =>
|
|
48
110
|
Effect.scoped(
|
|
49
111
|
Effect.gen(function* () {
|
|
50
112
|
const command = ChildProcess.make("az", argv, {
|
|
@@ -54,89 +116,52 @@ export class AzService extends Context.Service<
|
|
|
54
116
|
const process = yield* executor.spawn(command);
|
|
55
117
|
|
|
56
118
|
const stdoutChunk = yield* process.stdout.pipe(Stream.decodeText(), Stream.runCollect);
|
|
57
|
-
const stdout = stdoutChunk.join("");
|
|
58
|
-
|
|
59
119
|
const stderrChunk = yield* process.stderr.pipe(Stream.decodeText(), Stream.runCollect);
|
|
60
|
-
const stderr = stderrChunk.join("");
|
|
61
|
-
|
|
62
120
|
const exitCode = yield* process.exitCode;
|
|
63
121
|
|
|
64
|
-
return { stdout, stderr, exitCode };
|
|
122
|
+
return { stdout: stdoutChunk.join(""), stderr: stderrChunk.join(""), exitCode };
|
|
65
123
|
}),
|
|
66
124
|
).pipe(
|
|
67
125
|
Effect.timeoutOption(timeoutMs),
|
|
68
|
-
Effect.mapError(
|
|
69
|
-
(platformError)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
126
|
+
Effect.mapError((platformError) => {
|
|
127
|
+
const missing = missingBinaryFromSpawnFailure("az", String(platformError));
|
|
128
|
+
return new AzCommandError({
|
|
129
|
+
message: `Command execution failed: ${String(platformError)}`,
|
|
130
|
+
command: renderedCommand,
|
|
131
|
+
exitCode: -1,
|
|
132
|
+
stderr: String(platformError),
|
|
133
|
+
hint: missing?.hint ?? "Check that the az CLI is installed and authenticated",
|
|
134
|
+
nextCommand: "az login",
|
|
135
|
+
retryable: true,
|
|
136
|
+
});
|
|
137
|
+
}),
|
|
79
138
|
);
|
|
80
139
|
|
|
81
|
-
const
|
|
140
|
+
const renderCommand = Effect.fn("AzService.renderCommand")(function* (
|
|
141
|
+
cmd: string,
|
|
142
|
+
profile?: string,
|
|
143
|
+
) {
|
|
144
|
+
const { argv } = yield* buildArgv(cmd, profile);
|
|
145
|
+
return renderCommandLine(["az", ...argv]);
|
|
146
|
+
});
|
|
82
147
|
|
|
83
148
|
const runCommand = Effect.fn("AzService.runCommand")(function* (
|
|
84
149
|
cmd: string,
|
|
85
|
-
|
|
150
|
+
profile?: string,
|
|
86
151
|
) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
const securityCheck = isCommandAllowed(cmd);
|
|
90
|
-
if (!securityCheck.allowed) {
|
|
91
|
-
return yield* new AzSecurityError({
|
|
92
|
-
message: securityCheck.reason ?? "Command not allowed",
|
|
93
|
-
command: cmd,
|
|
94
|
-
hint: "Only read-only az devops commands are allowed. Use build helpers for common operations.",
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const invokeParams = parseInvokeFromCommand(cmd);
|
|
99
|
-
if (invokeParams) {
|
|
100
|
-
const invokeResult = yield* runInvoke({
|
|
101
|
-
...invokeParams,
|
|
102
|
-
project: projectName,
|
|
103
|
-
});
|
|
104
|
-
return JSON.stringify(invokeResult);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const cmdWords = cmd.trim().split(/\s+/);
|
|
108
|
-
const firstWord = cmdWords[0]?.toLowerCase() ?? "";
|
|
109
|
-
const isDirectCommand = DIRECT_AZ_COMMANDS.includes(
|
|
110
|
-
firstWord as (typeof DIRECT_AZ_COMMANDS)[number],
|
|
111
|
-
);
|
|
112
|
-
const isStandaloneCommand = STANDALONE_AZ_COMMANDS.includes(
|
|
113
|
-
firstWord as (typeof STANDALONE_AZ_COMMANDS)[number],
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
const cmdArgv = tokenizeCommandLine(cmd);
|
|
117
|
-
const scopeArgv = [
|
|
118
|
-
"--organization",
|
|
119
|
-
azConfig.organization,
|
|
120
|
-
"--project",
|
|
121
|
-
projectName,
|
|
122
|
-
] as const;
|
|
123
|
-
|
|
124
|
-
const argv = isStandaloneCommand
|
|
125
|
-
? cmdArgv
|
|
126
|
-
: isDirectCommand
|
|
127
|
-
? [...cmdArgv, ...scopeArgv]
|
|
128
|
-
: ["devops", ...cmdArgv, ...scopeArgv];
|
|
129
|
-
|
|
152
|
+
const { argv, azConfig } = yield* buildArgv(cmd, profile);
|
|
153
|
+
const timeoutMs = azConfig.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
130
154
|
const fullCommand = renderCommandLine(["az", ...argv]);
|
|
131
|
-
|
|
155
|
+
|
|
156
|
+
const resultOption = yield* spawnAz(argv, timeoutMs, fullCommand);
|
|
132
157
|
|
|
133
158
|
if (Option.isNone(resultOption)) {
|
|
134
159
|
return yield* new AzTimeoutError({
|
|
135
|
-
message: `Command timed out after ${
|
|
160
|
+
message: `Command timed out after ${timeoutMs}ms`,
|
|
136
161
|
command: fullCommand,
|
|
137
|
-
timeoutMs
|
|
162
|
+
timeoutMs,
|
|
138
163
|
retryable: true,
|
|
139
|
-
hint: "The command took too long. Retry or increase timeoutMs in
|
|
164
|
+
hint: "The command took too long. Retry or increase timeoutMs in azurePlatform config.",
|
|
140
165
|
});
|
|
141
166
|
}
|
|
142
167
|
|
|
@@ -152,165 +177,35 @@ export class AzService extends Context.Service<
|
|
|
152
177
|
}
|
|
153
178
|
|
|
154
179
|
const output = result.stdout.trim();
|
|
155
|
-
const transformed = transformCmdOutput(output);
|
|
156
|
-
|
|
157
|
-
if (typeof transformed === "string") {
|
|
158
|
-
return transformed;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return JSON.stringify(transformed);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
const runInvoke = Effect.fn("AzService.runInvoke")(function* (params: InvokeParams) {
|
|
165
|
-
const securityCheck = isInvokeAllowed(params);
|
|
166
|
-
if (!securityCheck.allowed) {
|
|
167
|
-
return yield* new AzSecurityError({
|
|
168
|
-
message: securityCheck.reason ?? "Invoke not allowed",
|
|
169
|
-
command: `invoke --area ${params.area} --resource ${params.resource}`,
|
|
170
|
-
hint: "Only allowed invoke areas/resources can be used. Check az-tool security config.",
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const argv = ["devops", "invoke", "--area", params.area, "--resource", params.resource];
|
|
175
180
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const routeParameters = {
|
|
179
|
-
project: projectName,
|
|
180
|
-
...params.routeParameters,
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
if (Object.keys(routeParameters).length > 0) {
|
|
184
|
-
argv.push(
|
|
185
|
-
"--route-parameters",
|
|
186
|
-
...Object.entries(routeParameters).map(([k, v]) => `${k}=${v}`),
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (params.queryParameters) {
|
|
191
|
-
argv.push(
|
|
192
|
-
"--query-parameters",
|
|
193
|
-
...Object.entries(params.queryParameters).map(([k, v]) => `${k}=${v}`),
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
argv.push("--organization", azConfig.organization, "--output", "json");
|
|
198
|
-
|
|
199
|
-
const fullCommand = renderCommandLine(["az", ...argv]);
|
|
200
|
-
const resultOption = yield* runAzCommand(argv, azConfig.timeoutMs ?? 60000);
|
|
201
|
-
|
|
202
|
-
if (Option.isNone(resultOption)) {
|
|
203
|
-
return yield* new AzTimeoutError({
|
|
204
|
-
message: `Invoke timed out after ${azConfig.timeoutMs ?? 60000}ms`,
|
|
181
|
+
if (output.length === 0) {
|
|
182
|
+
return {
|
|
205
183
|
command: fullCommand,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
});
|
|
184
|
+
subscription: azConfig.subscription,
|
|
185
|
+
data: null,
|
|
186
|
+
} satisfies AzCommandResult;
|
|
210
187
|
}
|
|
211
188
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
189
|
+
// --output json is injected unless the caller picked a format, so non-JSON
|
|
190
|
+
// output here is a deliberate table/tsv request and is passed through as text.
|
|
191
|
+
const parsed = ((): unknown => {
|
|
192
|
+
try {
|
|
193
|
+
return JSON.parse(output) as unknown;
|
|
194
|
+
} catch {
|
|
195
|
+
return output;
|
|
196
|
+
}
|
|
197
|
+
})();
|
|
222
198
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
rawOutput: result.stdout.slice(0, 500),
|
|
229
|
-
hint: "The az CLI returned non-JSON output. Ensure --output json is used.",
|
|
230
|
-
}),
|
|
231
|
-
});
|
|
232
|
-
return jsonData;
|
|
199
|
+
return {
|
|
200
|
+
command: fullCommand,
|
|
201
|
+
subscription: azConfig.subscription,
|
|
202
|
+
data: parsed,
|
|
203
|
+
} satisfies AzCommandResult;
|
|
233
204
|
});
|
|
234
205
|
|
|
235
|
-
return { runCommand,
|
|
206
|
+
return { runCommand, renderCommand };
|
|
236
207
|
}),
|
|
237
208
|
);
|
|
238
209
|
}
|
|
239
210
|
|
|
240
211
|
export const AzServiceLayer = AzService.layer;
|
|
241
|
-
|
|
242
|
-
function parseInvokeFromCommand(cmd: string): InvokeParams | undefined {
|
|
243
|
-
const words = cmd.trim().split(/\s+/);
|
|
244
|
-
const loweredWords = words.map((word) => word.toLowerCase());
|
|
245
|
-
|
|
246
|
-
if (!loweredWords.includes("invoke")) {
|
|
247
|
-
return undefined;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const area = extractOptionValue(words, "--area");
|
|
251
|
-
const resource = extractOptionValue(words, "--resource");
|
|
252
|
-
|
|
253
|
-
if (!area || !resource) {
|
|
254
|
-
return undefined;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const routeParameters = extractParametersOption(words, "--route-parameters");
|
|
258
|
-
const queryParameters = extractParametersOption(words, "--query-parameters");
|
|
259
|
-
const apiVersion = extractOptionValue(words, "--api-version");
|
|
260
|
-
|
|
261
|
-
const mergedQueryParameters = apiVersion
|
|
262
|
-
? {
|
|
263
|
-
...queryParameters,
|
|
264
|
-
"api-version": apiVersion,
|
|
265
|
-
}
|
|
266
|
-
: queryParameters;
|
|
267
|
-
|
|
268
|
-
return {
|
|
269
|
-
area,
|
|
270
|
-
resource,
|
|
271
|
-
...(routeParameters ? { routeParameters } : {}),
|
|
272
|
-
...(mergedQueryParameters ? { queryParameters: mergedQueryParameters } : {}),
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function extractOptionValue(args: readonly string[], optionName: string): string | undefined {
|
|
277
|
-
const optionIndex = args.findIndex((arg) => arg.toLowerCase() === optionName.toLowerCase());
|
|
278
|
-
|
|
279
|
-
if (optionIndex === -1) {
|
|
280
|
-
return undefined;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return args[optionIndex + 1];
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function extractParametersOption(
|
|
287
|
-
args: readonly string[],
|
|
288
|
-
optionName: string,
|
|
289
|
-
): Record<string, string | number> | undefined {
|
|
290
|
-
const optionIndex = args.findIndex((arg) => arg.toLowerCase() === optionName.toLowerCase());
|
|
291
|
-
|
|
292
|
-
if (optionIndex === -1) {
|
|
293
|
-
return undefined;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const result: Record<string, string | number> = {};
|
|
297
|
-
|
|
298
|
-
for (let i = optionIndex + 1; i < args.length; i++) {
|
|
299
|
-
const token = args[i];
|
|
300
|
-
if (!token || token.startsWith("--")) {
|
|
301
|
-
break;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const equalsIndex = token.indexOf("=");
|
|
305
|
-
if (equalsIndex === -1) {
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const key = token.slice(0, equalsIndex);
|
|
310
|
-
const rawValue = token.slice(equalsIndex + 1);
|
|
311
|
-
const parsedNumber = Number(rawValue);
|
|
312
|
-
result[key] = Number.isNaN(parsedNumber) ? rawValue : parsedNumber;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return Object.keys(result).length > 0 ? result : undefined;
|
|
316
|
-
}
|
package/src/az-tool/types.ts
CHANGED
|
@@ -1,67 +1,14 @@
|
|
|
1
|
-
export type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
reason?: string;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
export type InvokeParams = {
|
|
8
|
-
area: string;
|
|
9
|
-
resource: string;
|
|
10
|
-
project?: string;
|
|
11
|
-
routeParameters?: Record<string, string | number>;
|
|
12
|
-
queryParameters?: Record<string, string | number>;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export type BuildJob = {
|
|
16
|
-
id: string;
|
|
17
|
-
parentId?: string | null;
|
|
18
|
-
type: "Job" | "Stage" | "Task" | "Phase" | "Checkpoint";
|
|
19
|
-
name: string;
|
|
20
|
-
state: "pending" | "inProgress" | "completed";
|
|
21
|
-
result?: "succeeded" | "failed" | "canceled" | "skipped" | null;
|
|
22
|
-
startTime?: string | null;
|
|
23
|
-
finishTime?: string | null;
|
|
24
|
-
errorCount?: number | null;
|
|
25
|
-
warningCount?: number | null;
|
|
26
|
-
log?: { id: number; url: string } | null;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export type BuildTimeline = {
|
|
30
|
-
records: BuildJob[];
|
|
31
|
-
id: string;
|
|
32
|
-
changeId: number;
|
|
33
|
-
lastChangedBy: string;
|
|
34
|
-
lastChangedOn: string;
|
|
35
|
-
url: string;
|
|
1
|
+
export type AzSecurityCheckOptions = {
|
|
2
|
+
/** Empty or absent allows every resource group in the subscription. */
|
|
3
|
+
allowedResourceGroups?: readonly string[];
|
|
36
4
|
};
|
|
37
5
|
|
|
38
|
-
export type
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
count: number;
|
|
47
|
-
value: BuildLog[];
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export type JobSummary = {
|
|
51
|
-
name: string;
|
|
52
|
-
state: string;
|
|
53
|
-
result?: string;
|
|
54
|
-
stage?: string;
|
|
55
|
-
duration?: string;
|
|
56
|
-
logId?: number;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export type PipelineRun = {
|
|
60
|
-
id: number;
|
|
61
|
-
buildNumber: string;
|
|
62
|
-
status: string;
|
|
63
|
-
result?: string;
|
|
64
|
-
sourceBranch: string;
|
|
65
|
-
startTime?: string;
|
|
66
|
-
finishTime?: string;
|
|
6
|
+
export type AzSecurityCheckResult = {
|
|
7
|
+
allowed: boolean;
|
|
8
|
+
command: string;
|
|
9
|
+
argv?: string[];
|
|
10
|
+
verb?: string;
|
|
11
|
+
resourceGroup?: string;
|
|
12
|
+
reason?: string;
|
|
13
|
+
hint?: string;
|
|
67
14
|
};
|
|
@@ -2,15 +2,15 @@ import { Effect, Schema } from "effect";
|
|
|
2
2
|
|
|
3
3
|
import type { BuildJob, BuildLogs, BuildTimeline, JobSummary, PipelineRun } from "./types";
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { AzdoParseError } from "./errors";
|
|
6
|
+
import { AzdoService } from "./service";
|
|
7
7
|
import { transformBuildLogContent, transformTimeline } from "./transformers";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Get build timeline with all records (jobs, stages, tasks, etc.)
|
|
11
11
|
*/
|
|
12
12
|
export const getBuildTimeline = Effect.fn("Build.getBuildTimeline")(function* (buildId: number) {
|
|
13
|
-
const az = yield*
|
|
13
|
+
const az = yield* AzdoService;
|
|
14
14
|
|
|
15
15
|
const result = yield* az.runInvoke({
|
|
16
16
|
area: "build",
|
|
@@ -52,7 +52,7 @@ export const getBuildTimeline = Effect.fn("Build.getBuildTimeline")(function* (b
|
|
|
52
52
|
)(result).pipe(
|
|
53
53
|
Effect.mapError(
|
|
54
54
|
(e) =>
|
|
55
|
-
new
|
|
55
|
+
new AzdoParseError({
|
|
56
56
|
message: `Failed to parse build timeline: ${String(e)}`,
|
|
57
57
|
rawOutput: JSON.stringify(result).slice(0, 500),
|
|
58
58
|
hint: "The Azure DevOps API returned an unexpected response format for build timeline",
|
|
@@ -85,7 +85,7 @@ export const getBuildJobs = Effect.fn("Build.getBuildJobs")(function* (buildId:
|
|
|
85
85
|
* Get list of build logs
|
|
86
86
|
*/
|
|
87
87
|
export const getBuildLogs = Effect.fn("Build.getBuildLogs")(function* (buildId: number) {
|
|
88
|
-
const az = yield*
|
|
88
|
+
const az = yield* AzdoService;
|
|
89
89
|
|
|
90
90
|
const result = yield* az.runInvoke({
|
|
91
91
|
area: "build",
|
|
@@ -109,7 +109,7 @@ export const getBuildLogs = Effect.fn("Build.getBuildLogs")(function* (buildId:
|
|
|
109
109
|
)(result).pipe(
|
|
110
110
|
Effect.mapError(
|
|
111
111
|
(e) =>
|
|
112
|
-
new
|
|
112
|
+
new AzdoParseError({
|
|
113
113
|
message: `Failed to parse build logs: ${String(e)}`,
|
|
114
114
|
rawOutput: JSON.stringify(result).slice(0, 500),
|
|
115
115
|
hint: "The Azure DevOps API returned an unexpected response format for build logs",
|
|
@@ -127,7 +127,7 @@ export const getBuildLogContent = Effect.fn("Build.getBuildLogContent")(function
|
|
|
127
127
|
buildId: number,
|
|
128
128
|
logId: number,
|
|
129
129
|
) {
|
|
130
|
-
const az = yield*
|
|
130
|
+
const az = yield* AzdoService;
|
|
131
131
|
|
|
132
132
|
const result = yield* az.runInvoke({
|
|
133
133
|
area: "build",
|
|
@@ -146,7 +146,7 @@ export const getBuildLogContent = Effect.fn("Build.getBuildLogContent")(function
|
|
|
146
146
|
)(result).pipe(
|
|
147
147
|
Effect.mapError(
|
|
148
148
|
(e) =>
|
|
149
|
-
new
|
|
149
|
+
new AzdoParseError({
|
|
150
150
|
message: `Failed to parse log content: ${String(e)}`,
|
|
151
151
|
rawOutput: String(result).slice(0, 500),
|
|
152
152
|
hint: "The Azure DevOps API returned an unexpected format for log content",
|
|
@@ -228,7 +228,7 @@ export const listPipelineRuns = Effect.fn("Build.listPipelineRuns")(function* (o
|
|
|
228
228
|
pipelineId?: number;
|
|
229
229
|
top?: number;
|
|
230
230
|
}) {
|
|
231
|
-
const az = yield*
|
|
231
|
+
const az = yield* AzdoService;
|
|
232
232
|
|
|
233
233
|
const parts = ["pipelines", "runs", "list", "--output", "json"];
|
|
234
234
|
|
|
@@ -249,7 +249,7 @@ export const listPipelineRuns = Effect.fn("Build.listPipelineRuns")(function* (o
|
|
|
249
249
|
const jsonData = yield* Effect.try({
|
|
250
250
|
try: () => JSON.parse(rawResult) as unknown,
|
|
251
251
|
catch: () =>
|
|
252
|
-
new
|
|
252
|
+
new AzdoParseError({
|
|
253
253
|
message: "Failed to parse JSON from pipeline runs output",
|
|
254
254
|
rawOutput: rawResult.slice(0, 500),
|
|
255
255
|
hint: "The az CLI returned non-JSON output. Check that the command ran successfully.",
|
|
@@ -273,7 +273,7 @@ export const listPipelineRuns = Effect.fn("Build.listPipelineRuns")(function* (o
|
|
|
273
273
|
)(jsonData).pipe(
|
|
274
274
|
Effect.mapError(
|
|
275
275
|
(e) =>
|
|
276
|
-
new
|
|
276
|
+
new AzdoParseError({
|
|
277
277
|
message: `Failed to parse pipeline runs: ${String(e)}`,
|
|
278
278
|
rawOutput: rawResult.slice(0, 500),
|
|
279
279
|
hint: "The Azure DevOps API returned an unexpected response format for pipeline runs",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const ALLOWED_SUBCOMMANDS = ["list", "run", "show", "show-tags"] as const;
|
|
2
|
+
|
|
3
|
+
export const BLOCKED_SUBCOMMANDS = ["create", "delete", "update", "cancel", "queue"] as const;
|
|
4
|
+
|
|
5
|
+
export const DIRECT_AZ_COMMANDS = ["pipelines", "repos"] as const;
|
|
6
|
+
|
|
7
|
+
export const STANDALONE_AZ_COMMANDS = ["acr", "account"] as const;
|
|
8
|
+
|
|
9
|
+
export const ALLOWED_INVOKE_AREAS = ["build"] as const;
|
|
10
|
+
|
|
11
|
+
export const ALLOWED_INVOKE_RESOURCES: Record<string, readonly string[]> = {
|
|
12
|
+
build: ["timeline", "logs", "builds"],
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export const BLOCKED_INVOKE_AREAS = [
|
|
16
|
+
"git",
|
|
17
|
+
"policy",
|
|
18
|
+
"security",
|
|
19
|
+
"wiki",
|
|
20
|
+
"work",
|
|
21
|
+
"graph",
|
|
22
|
+
"audit",
|
|
23
|
+
"permissions",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
export const BLOCKED_INVOKE_RESOURCES: Record<string, readonly string[]> = {
|
|
27
|
+
build: ["definitions", "folders", "tags", "retention"],
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
export type AllowedSubcommand = (typeof ALLOWED_SUBCOMMANDS)[number];
|
|
31
|
+
export type BlockedSubcommand = (typeof BLOCKED_SUBCOMMANDS)[number];
|
|
32
|
+
export type AllowedInvokeArea = (typeof ALLOWED_INVOKE_AREAS)[number];
|
|
33
|
+
export type BlockedInvokeArea = (typeof BLOCKED_INVOKE_AREAS)[number];
|