@microck/canonfig 2.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/LICENSE +21 -0
- package/README.md +263 -0
- package/dist/agent/agent-resolution.errors.js +42 -0
- package/dist/agent/agent-resolution.layer.js +204 -0
- package/dist/agent/agent-resolution.service.js +2259 -0
- package/dist/agent/agent-resolution.types.js +1 -0
- package/dist/agent/controlled-executor.js +704 -0
- package/dist/agent/harness-adapters.js +85 -0
- package/dist/cli/cli.js +618 -0
- package/dist/cli/exit-codes.js +28 -0
- package/dist/cli/follower-commands.js +3 -0
- package/dist/cli/render.js +56 -0
- package/dist/cli/source-commands.js +5 -0
- package/dist/domain/brand.js +29 -0
- package/dist/domain/identity.js +31 -0
- package/dist/domain/npm-package-spec.js +186 -0
- package/dist/domain/profile.js +950 -0
- package/dist/domain/recipe-versions.js +297 -0
- package/dist/domain/resource.js +259 -0
- package/dist/domain/synchronization.js +346 -0
- package/dist/enrollment/enrollment.errors.js +43 -0
- package/dist/enrollment/enrollment.layer.js +724 -0
- package/dist/enrollment/enrollment.service.js +3 -0
- package/dist/enrollment/enrollment.types.js +59 -0
- package/dist/enrollment/follower-client.js +585 -0
- package/dist/enrollment/source-server.js +313 -0
- package/dist/machine/linux.layer.js +1183 -0
- package/dist/machine/machine-state.errors.js +52 -0
- package/dist/machine/machine-state.service.js +3 -0
- package/dist/machine/machine-state.types.js +1 -0
- package/dist/machine/macos.layer.js +470 -0
- package/dist/machine/windows.layer.js +879 -0
- package/dist/profile/discovery.js +740 -0
- package/dist/profile/profile-catalog.errors.js +50 -0
- package/dist/profile/profile-catalog.layer.js +20 -0
- package/dist/profile/profile-catalog.service.js +7 -0
- package/dist/profile/profile-codec.js +153 -0
- package/dist/profile/publication.js +298 -0
- package/dist/profile/tool-catalog.js +384 -0
- package/dist/runtime/doctor.js +306 -0
- package/dist/runtime/layers.js +706 -0
- package/dist/runtime/main.js +38 -0
- package/dist/schedule/linux-schedule.js +24 -0
- package/dist/schedule/macos-schedule.js +25 -0
- package/dist/schedule/schedule-manager.errors.js +17 -0
- package/dist/schedule/schedule-manager.layer.js +205 -0
- package/dist/schedule/schedule-manager.service.js +3 -0
- package/dist/schedule/schedule-manager.types.js +114 -0
- package/dist/schedule/windows-schedule.js +25 -0
- package/dist/state/state-repository.errors.js +55 -0
- package/dist/state/state-repository.layer.js +1507 -0
- package/dist/state/state-repository.service.js +3 -0
- package/dist/state/state-repository.types.js +1 -0
- package/dist/state/state-schema.js +298 -0
- package/dist/synchronization/config-codec.js +97 -0
- package/dist/synchronization/executor.js +700 -0
- package/dist/synchronization/follower-orchestration.js +939 -0
- package/dist/synchronization/follower-sync-config.js +81 -0
- package/dist/synchronization/npm-artifact.js +670 -0
- package/dist/synchronization/planner.js +378 -0
- package/dist/synchronization/recovery.js +397 -0
- package/dist/synchronization/resource-executors.js +1198 -0
- package/dist/synchronization/resource-plans.js +645 -0
- package/dist/synchronization/synchronization.errors.js +102 -0
- package/dist/synchronization/synchronization.layer.js +97 -0
- package/dist/synchronization/synchronization.service.js +11 -0
- package/dist/synchronization/synchronization.types.js +1 -0
- package/package.json +66 -0
package/dist/cli/cli.js
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
import { Effect, Option, Schema } from "effect";
|
|
2
|
+
import { CertificateFingerprint, FollowerId, GroupName, InvitationCode, ProfileId, ProfileRevisionId, ResourceId, Timestamp, } from "../domain/brand.js";
|
|
3
|
+
import { AgentPolicy } from "../domain/identity.js";
|
|
4
|
+
import { isNestedCommandLauncher } from "../agent/agent-resolution.service.js";
|
|
5
|
+
import { ExecutableAuthorizationSchema } from "../domain/synchronization.js";
|
|
6
|
+
import { scheduleWeekdays, } from "../schedule/schedule-manager.types.js";
|
|
7
|
+
import { AgentHarnessCapability, SupportedAgentHarness, } from "../synchronization/follower-sync-config.js";
|
|
8
|
+
import { CliExitCode, exitCodeForFailure, } from "./exit-codes.js";
|
|
9
|
+
import { FollowerCommands } from "./follower-commands.js";
|
|
10
|
+
import { renderCliResult, } from "./render.js";
|
|
11
|
+
import { SourceCommands, } from "./source-commands.js";
|
|
12
|
+
export const programName = "canonfig";
|
|
13
|
+
export const programDisplayName = "Canonfig";
|
|
14
|
+
export const programVersion = "2.0.0";
|
|
15
|
+
export const helpText = `${programDisplayName} ${programVersion}
|
|
16
|
+
|
|
17
|
+
Usage: ${programName} <command> [options]
|
|
18
|
+
|
|
19
|
+
Source:
|
|
20
|
+
source init
|
|
21
|
+
source scan --file <path> [--file <path>...]
|
|
22
|
+
source publish --proposal <path> --profile <id> --name <name> --reviewer <name>
|
|
23
|
+
source publish --profile-file <profile.jsonc> [--proposal <path>] --reviewer <name>
|
|
24
|
+
source serve [--host <127.0.0.1|::1>] [--port <port>]
|
|
25
|
+
source invite --endpoint <https-url> [--expires <duration>] [--group <name>...]
|
|
26
|
+
source revoke <follower-id>
|
|
27
|
+
|
|
28
|
+
Follower:
|
|
29
|
+
follower enroll <invite> --name <name> --profile <id>
|
|
30
|
+
sync [--plan | --apply] [--no-input]
|
|
31
|
+
recover [--no-input]
|
|
32
|
+
status [--follower <id>]
|
|
33
|
+
overlay list
|
|
34
|
+
overlay set <resource-id> --target <path> --key <config.path> [--key <config.path>...]
|
|
35
|
+
overlay remove <resource-id>
|
|
36
|
+
doctor [--no-input] [--timeout-ms <ms>]
|
|
37
|
+
|
|
38
|
+
Profiles and policy:
|
|
39
|
+
profile list
|
|
40
|
+
profile show <revision-id>
|
|
41
|
+
profile select <profile-id>
|
|
42
|
+
agent policy [deterministic-only|agent-propose|agent-apply]
|
|
43
|
+
agent harness [codex|claude|gemini] --executable <path> [--allow-path <path>...]
|
|
44
|
+
[--allow-leaf-executable <name>...]
|
|
45
|
+
[--allow-origin <https-origin>...]
|
|
46
|
+
[--allow-capability <capability>...] [--maximum-input-bytes <bytes>]
|
|
47
|
+
|
|
48
|
+
Scheduling:
|
|
49
|
+
schedule set <daily@HH:mm|weekly:Day@HH:mm> [--timezone <IANA>] [--executable <path>]
|
|
50
|
+
schedule status
|
|
51
|
+
schedule remove
|
|
52
|
+
|
|
53
|
+
Global options:
|
|
54
|
+
-h, --help Show help
|
|
55
|
+
-V, --version Show version
|
|
56
|
+
--json Emit stable machine-readable JSON
|
|
57
|
+
`;
|
|
58
|
+
const invalid = (message) => ({
|
|
59
|
+
_tag: "InvalidInput",
|
|
60
|
+
message: `${message}\nRun '${programName} --help' for usage.`,
|
|
61
|
+
exitCode: CliExitCode.usageOrConfiguration,
|
|
62
|
+
});
|
|
63
|
+
const command = (value, format) => ({
|
|
64
|
+
_tag: "Command",
|
|
65
|
+
command: value,
|
|
66
|
+
format,
|
|
67
|
+
exitCode: CliExitCode.success,
|
|
68
|
+
});
|
|
69
|
+
const parseOptions = (arguments_, valueOptions, switchOptions) => {
|
|
70
|
+
const positionals = [];
|
|
71
|
+
const values = new Map();
|
|
72
|
+
const switches = new Set();
|
|
73
|
+
for (let index = 0; index < arguments_.length; index += 1) {
|
|
74
|
+
const argument = arguments_[index];
|
|
75
|
+
if (!argument.startsWith("-")) {
|
|
76
|
+
positionals.push(argument);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (switchOptions.has(argument)) {
|
|
80
|
+
if (switches.has(argument)) {
|
|
81
|
+
throw new Error(`Option may be specified only once: ${argument}`);
|
|
82
|
+
}
|
|
83
|
+
switches.add(argument);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!valueOptions.has(argument))
|
|
87
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
88
|
+
const value = arguments_[index + 1];
|
|
89
|
+
if (value === undefined || value.startsWith("-")) {
|
|
90
|
+
throw new Error(`Option requires a value: ${argument}`);
|
|
91
|
+
}
|
|
92
|
+
index += 1;
|
|
93
|
+
const entries = values.get(argument) ?? [];
|
|
94
|
+
entries.push(value);
|
|
95
|
+
values.set(argument, entries);
|
|
96
|
+
}
|
|
97
|
+
return { positionals, values, switches };
|
|
98
|
+
};
|
|
99
|
+
const one = (options, name, required = false) => {
|
|
100
|
+
const entries = options.values.get(name) ?? [];
|
|
101
|
+
if (entries.length > 1)
|
|
102
|
+
throw new Error(`Option may be specified only once: ${name}`);
|
|
103
|
+
const value = entries[0];
|
|
104
|
+
if (required && value === undefined)
|
|
105
|
+
throw new Error(`Missing required option: ${name}`);
|
|
106
|
+
return value;
|
|
107
|
+
};
|
|
108
|
+
const decodeOption = (decoder, value, label) => {
|
|
109
|
+
const decoded = decoder(value);
|
|
110
|
+
if (Option.isNone(decoded))
|
|
111
|
+
throw new Error(`Invalid ${label}: ${value}`);
|
|
112
|
+
return decoded.value;
|
|
113
|
+
};
|
|
114
|
+
const parsePositiveInteger = (value, label, maximum = Number.MAX_SAFE_INTEGER) => {
|
|
115
|
+
if (!/^[1-9]\d*$/u.test(value))
|
|
116
|
+
throw new Error(`Invalid ${label}: ${value}`);
|
|
117
|
+
const number = Number(value);
|
|
118
|
+
if (!Number.isSafeInteger(number) || number > maximum) {
|
|
119
|
+
throw new Error(`Invalid ${label}: ${value}`);
|
|
120
|
+
}
|
|
121
|
+
return number;
|
|
122
|
+
};
|
|
123
|
+
const durationMilliseconds = (value) => {
|
|
124
|
+
const match = /^(?<amount>[1-9]\d*)(?<unit>ms|s|m|h)$/u.exec(value);
|
|
125
|
+
if (match?.groups === undefined)
|
|
126
|
+
throw new Error(`Invalid duration: ${value}`);
|
|
127
|
+
const amount = parsePositiveInteger(match.groups.amount ?? "", "duration");
|
|
128
|
+
const multiplier = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000 }[match.groups.unit ?? ""];
|
|
129
|
+
if (multiplier === undefined || amount * multiplier > 86_400_000) {
|
|
130
|
+
throw new Error("Invitation duration must not exceed 24h");
|
|
131
|
+
}
|
|
132
|
+
return amount * multiplier;
|
|
133
|
+
};
|
|
134
|
+
const invitationSchema = Schema.Struct({
|
|
135
|
+
code: InvitationCode,
|
|
136
|
+
nonce: Schema.NonEmptyString,
|
|
137
|
+
endpoint: Schema.NonEmptyString,
|
|
138
|
+
sourceFingerprint: CertificateFingerprint,
|
|
139
|
+
tlsFingerprint: CertificateFingerprint,
|
|
140
|
+
groups: Schema.Array(GroupName),
|
|
141
|
+
expiresAt: Timestamp,
|
|
142
|
+
});
|
|
143
|
+
const decodeInvitation = (encoded) => {
|
|
144
|
+
let parsed;
|
|
145
|
+
try {
|
|
146
|
+
parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
throw new Error("Invalid enrollment invitation");
|
|
150
|
+
}
|
|
151
|
+
const decoded = Schema.decodeUnknownOption(invitationSchema)(parsed);
|
|
152
|
+
if (Option.isNone(decoded))
|
|
153
|
+
throw new Error("Invalid enrollment invitation");
|
|
154
|
+
return decoded.value;
|
|
155
|
+
};
|
|
156
|
+
const parseSchedule = (calendar, timezone) => {
|
|
157
|
+
const daily = /^daily@(?<time>(?:[01]\d|2[0-3]):[0-5]\d)$/u.exec(calendar);
|
|
158
|
+
if (daily?.groups?.time !== undefined) {
|
|
159
|
+
const schedule = {
|
|
160
|
+
kind: "daily",
|
|
161
|
+
localTime: daily.groups.time,
|
|
162
|
+
};
|
|
163
|
+
return timezone === undefined ? schedule : { ...schedule, timezone };
|
|
164
|
+
}
|
|
165
|
+
const weekly = /^weekly:(?<days>(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)(?:,(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun))*)@(?<time>(?:[01]\d|2[0-3]):[0-5]\d)$/u.exec(calendar);
|
|
166
|
+
const days = weekly?.groups?.days?.split(",");
|
|
167
|
+
const localTime = weekly?.groups?.time;
|
|
168
|
+
const decodedWeekdays = days?.map((day) => decodeOption(Schema.decodeUnknownOption(Schema.Literals(scheduleWeekdays)), day, "schedule weekday"));
|
|
169
|
+
if (decodedWeekdays !== undefined && localTime !== undefined) {
|
|
170
|
+
const schedule = {
|
|
171
|
+
kind: "weekly",
|
|
172
|
+
weekdays: [...new Set(decodedWeekdays)].sort((left, right) => scheduleWeekdays.indexOf(left) - scheduleWeekdays.indexOf(right)),
|
|
173
|
+
localTime,
|
|
174
|
+
};
|
|
175
|
+
return timezone === undefined ? schedule : { ...schedule, timezone };
|
|
176
|
+
}
|
|
177
|
+
throw new Error(`Invalid schedule calendar: ${calendar}`);
|
|
178
|
+
};
|
|
179
|
+
const evaluateCommand = (arguments_, format) => {
|
|
180
|
+
const [area, action, ...rest] = arguments_;
|
|
181
|
+
try {
|
|
182
|
+
if (area === "source") {
|
|
183
|
+
if (action === "init" && rest.length === 0)
|
|
184
|
+
return command({ _tag: "SourceInit" }, format);
|
|
185
|
+
if (action === "scan") {
|
|
186
|
+
const options = parseOptions(rest, new Set(["--file"]), new Set());
|
|
187
|
+
if (options.positionals.length > 0)
|
|
188
|
+
return invalid("source scan accepts only --file inputs");
|
|
189
|
+
const files = (options.values.get("--file") ?? []).map((path) => ({ path }));
|
|
190
|
+
if (files.length === 0)
|
|
191
|
+
return invalid("source scan requires at least one --file");
|
|
192
|
+
return command({ _tag: "SourceScan", files }, format);
|
|
193
|
+
}
|
|
194
|
+
if (action === "publish") {
|
|
195
|
+
const options = parseOptions(rest, new Set(["--proposal", "--profile", "--name", "--profile-file", "--reviewer"]), new Set());
|
|
196
|
+
if (options.positionals.length > 0)
|
|
197
|
+
return invalid("source publish accepts only named options");
|
|
198
|
+
const proposalPath = one(options, "--proposal");
|
|
199
|
+
const profilePath = one(options, "--profile-file");
|
|
200
|
+
const profileValue = one(options, "--profile");
|
|
201
|
+
const name = one(options, "--name");
|
|
202
|
+
if (proposalPath === undefined && profilePath === undefined) {
|
|
203
|
+
return invalid("source publish requires --proposal or --profile-file");
|
|
204
|
+
}
|
|
205
|
+
if (profilePath === undefined && (profileValue === undefined || name === undefined)) {
|
|
206
|
+
return invalid("source publish requires --profile and --name without --profile-file");
|
|
207
|
+
}
|
|
208
|
+
return command({
|
|
209
|
+
_tag: "SourcePublish",
|
|
210
|
+
proposalPath,
|
|
211
|
+
profile: profileValue === undefined
|
|
212
|
+
? undefined
|
|
213
|
+
: decodeOption(Schema.decodeUnknownOption(ProfileId), profileValue, "profile id"),
|
|
214
|
+
name,
|
|
215
|
+
profilePath,
|
|
216
|
+
reviewer: one(options, "--reviewer", true),
|
|
217
|
+
}, format);
|
|
218
|
+
}
|
|
219
|
+
if (action === "serve") {
|
|
220
|
+
const options = parseOptions(rest, new Set(["--host", "--port"]), new Set());
|
|
221
|
+
if (options.positionals.length > 0)
|
|
222
|
+
return invalid("source serve accepts only named options");
|
|
223
|
+
const hostname = one(options, "--host") ?? "127.0.0.1";
|
|
224
|
+
if (hostname !== "127.0.0.1" && hostname !== "::1") {
|
|
225
|
+
return invalid(`Invalid source host: ${hostname}`);
|
|
226
|
+
}
|
|
227
|
+
const port = parsePositiveInteger(one(options, "--port") ?? "17342", "port", 65_535);
|
|
228
|
+
return command({ _tag: "SourceServe", hostname, port }, format);
|
|
229
|
+
}
|
|
230
|
+
if (action === "invite") {
|
|
231
|
+
const options = parseOptions(rest, new Set(["--endpoint", "--expires", "--group"]), new Set());
|
|
232
|
+
if (options.positionals.length > 0)
|
|
233
|
+
return invalid("source invite accepts only named options");
|
|
234
|
+
const endpoint = one(options, "--endpoint", true);
|
|
235
|
+
let url;
|
|
236
|
+
try {
|
|
237
|
+
url = new URL(endpoint);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return invalid(`Invalid source endpoint: ${endpoint}`);
|
|
241
|
+
}
|
|
242
|
+
if (url.protocol !== "https:")
|
|
243
|
+
return invalid("Source endpoint must use HTTPS");
|
|
244
|
+
if (url.hostname !== "127.0.0.1"
|
|
245
|
+
&& url.hostname !== "[::1]"
|
|
246
|
+
&& url.hostname !== "::1") {
|
|
247
|
+
return invalid("Source endpoint must use a loopback host");
|
|
248
|
+
}
|
|
249
|
+
const groups = (options.values.get("--group") ?? []).map((value) => decodeOption(Schema.decodeUnknownOption(GroupName), value, "group name"));
|
|
250
|
+
return command({
|
|
251
|
+
_tag: "SourceInvite",
|
|
252
|
+
endpoint: url.origin,
|
|
253
|
+
expiresInMilliseconds: durationMilliseconds(one(options, "--expires") ?? "15m"),
|
|
254
|
+
groups,
|
|
255
|
+
}, format);
|
|
256
|
+
}
|
|
257
|
+
if (action === "revoke") {
|
|
258
|
+
if (rest.length !== 1)
|
|
259
|
+
return invalid("Usage: canonfig source revoke <follower-id>");
|
|
260
|
+
return command({
|
|
261
|
+
_tag: "SourceRevoke",
|
|
262
|
+
follower: decodeOption(Schema.decodeUnknownOption(FollowerId), rest[0], "follower id"),
|
|
263
|
+
}, format);
|
|
264
|
+
}
|
|
265
|
+
return invalid(`Unknown source command: ${action ?? ""}`);
|
|
266
|
+
}
|
|
267
|
+
if (area === "follower" && action === "enroll") {
|
|
268
|
+
const options = parseOptions(rest, new Set(["--name", "--profile"]), new Set());
|
|
269
|
+
if (options.positionals.length !== 1) {
|
|
270
|
+
return invalid("Usage: canonfig follower enroll <invite> --name <name>");
|
|
271
|
+
}
|
|
272
|
+
const selectedProfile = one(options, "--profile");
|
|
273
|
+
const enrollment = {
|
|
274
|
+
_tag: "FollowerEnroll",
|
|
275
|
+
invitation: decodeInvitation(options.positionals[0]),
|
|
276
|
+
followerName: one(options, "--name", true),
|
|
277
|
+
};
|
|
278
|
+
return command(selectedProfile === undefined
|
|
279
|
+
? enrollment
|
|
280
|
+
: {
|
|
281
|
+
...enrollment,
|
|
282
|
+
selectedProfile: decodeOption(Schema.decodeUnknownOption(ProfileId), selectedProfile, "profile id"),
|
|
283
|
+
}, format);
|
|
284
|
+
}
|
|
285
|
+
if (area === "profile") {
|
|
286
|
+
if (action === "list" && rest.length === 0)
|
|
287
|
+
return command({ _tag: "ProfileList" }, format);
|
|
288
|
+
if (action === "show" && rest.length === 1) {
|
|
289
|
+
return command({
|
|
290
|
+
_tag: "ProfileShow",
|
|
291
|
+
revision: decodeOption(Schema.decodeUnknownOption(ProfileRevisionId), rest[0], "profile revision id"),
|
|
292
|
+
}, format);
|
|
293
|
+
}
|
|
294
|
+
if (action === "select" && rest.length === 1) {
|
|
295
|
+
return command({
|
|
296
|
+
_tag: "ProfileSelect",
|
|
297
|
+
profile: decodeOption(Schema.decodeUnknownOption(ProfileId), rest[0], "profile id"),
|
|
298
|
+
}, format);
|
|
299
|
+
}
|
|
300
|
+
return invalid(`Unknown profile command: ${action ?? ""}`);
|
|
301
|
+
}
|
|
302
|
+
if (area === "sync") {
|
|
303
|
+
const options = parseOptions(arguments_.slice(1), new Set(), new Set(["--plan", "--apply", "--no-input"]));
|
|
304
|
+
if (options.positionals.length > 0)
|
|
305
|
+
return invalid("sync accepts no positional arguments");
|
|
306
|
+
if (options.switches.has("--plan") && options.switches.has("--apply")) {
|
|
307
|
+
return invalid("--plan and --apply are mutually exclusive");
|
|
308
|
+
}
|
|
309
|
+
return command({
|
|
310
|
+
_tag: "Synchronize",
|
|
311
|
+
mode: options.switches.has("--apply") ? "apply" : "plan",
|
|
312
|
+
noInput: options.switches.has("--no-input"),
|
|
313
|
+
}, format);
|
|
314
|
+
}
|
|
315
|
+
if (area === "recover") {
|
|
316
|
+
const options = parseOptions(arguments_.slice(1), new Set(), new Set(["--no-input"]));
|
|
317
|
+
if (options.positionals.length > 0)
|
|
318
|
+
return invalid("recover accepts no positional arguments");
|
|
319
|
+
return command({ _tag: "Recover", noInput: options.switches.has("--no-input") }, format);
|
|
320
|
+
}
|
|
321
|
+
if (area === "status") {
|
|
322
|
+
const options = parseOptions(arguments_.slice(1), new Set(["--follower"]), new Set());
|
|
323
|
+
if (options.positionals.length > 0)
|
|
324
|
+
return invalid("status accepts no positional arguments");
|
|
325
|
+
const follower = one(options, "--follower");
|
|
326
|
+
if (follower === undefined)
|
|
327
|
+
return command({ _tag: "Status" }, format);
|
|
328
|
+
return command({
|
|
329
|
+
_tag: "Status",
|
|
330
|
+
follower: decodeOption(Schema.decodeUnknownOption(FollowerId), follower, "follower id"),
|
|
331
|
+
}, format);
|
|
332
|
+
}
|
|
333
|
+
if (area === "overlay") {
|
|
334
|
+
if (action === "list" && rest.length === 0) {
|
|
335
|
+
return command({ _tag: "OverlayList" }, format);
|
|
336
|
+
}
|
|
337
|
+
if (action === "remove" && rest.length === 1) {
|
|
338
|
+
return command({
|
|
339
|
+
_tag: "OverlayRemove",
|
|
340
|
+
resource: decodeOption(Schema.decodeUnknownOption(ResourceId), rest[0], "resource id"),
|
|
341
|
+
}, format);
|
|
342
|
+
}
|
|
343
|
+
if (action === "set") {
|
|
344
|
+
const options = parseOptions(rest, new Set(["--target", "--key"]), new Set());
|
|
345
|
+
if (options.positionals.length !== 1) {
|
|
346
|
+
return invalid("Usage: canonfig overlay set <resource-id> --target <path> --key <config.path>");
|
|
347
|
+
}
|
|
348
|
+
const keys = options.values.get("--key") ?? [];
|
|
349
|
+
if (keys.length === 0)
|
|
350
|
+
return invalid("overlay set requires at least one --key");
|
|
351
|
+
return command({
|
|
352
|
+
_tag: "OverlaySet",
|
|
353
|
+
resource: decodeOption(Schema.decodeUnknownOption(ResourceId), options.positionals[0], "resource id"),
|
|
354
|
+
target: one(options, "--target", true),
|
|
355
|
+
keys,
|
|
356
|
+
}, format);
|
|
357
|
+
}
|
|
358
|
+
return invalid(`Unknown overlay command: ${action ?? ""}`);
|
|
359
|
+
}
|
|
360
|
+
if (area === "doctor") {
|
|
361
|
+
const options = parseOptions(arguments_.slice(1), new Set(["--timeout-ms"]), new Set(["--no-input"]));
|
|
362
|
+
if (options.positionals.length > 0)
|
|
363
|
+
return invalid("doctor accepts no positional arguments");
|
|
364
|
+
return command({
|
|
365
|
+
_tag: "Doctor",
|
|
366
|
+
noInput: options.switches.has("--no-input"),
|
|
367
|
+
timeoutMilliseconds: parsePositiveInteger(one(options, "--timeout-ms") ?? "5000", "doctor timeout", 300_000),
|
|
368
|
+
}, format);
|
|
369
|
+
}
|
|
370
|
+
if (area === "agent" && action === "policy") {
|
|
371
|
+
if (rest.length === 0)
|
|
372
|
+
return command({ _tag: "AgentPolicyGet" }, format);
|
|
373
|
+
if (rest.length === 1) {
|
|
374
|
+
return command({
|
|
375
|
+
_tag: "AgentPolicySet",
|
|
376
|
+
policy: decodeOption(Schema.decodeUnknownOption(AgentPolicy), rest[0], "agent policy"),
|
|
377
|
+
}, format);
|
|
378
|
+
}
|
|
379
|
+
return invalid("Usage: canonfig agent policy [policy]");
|
|
380
|
+
}
|
|
381
|
+
if (area === "agent" && action === "harness") {
|
|
382
|
+
if (rest.length === 0)
|
|
383
|
+
return command({ _tag: "AgentHarnessGet" }, format);
|
|
384
|
+
const [kind, ...harnessArguments] = rest;
|
|
385
|
+
const options = parseOptions(harnessArguments, new Set([
|
|
386
|
+
"--executable",
|
|
387
|
+
"--allow-path",
|
|
388
|
+
"--allow-leaf-executable",
|
|
389
|
+
"--allow-origin",
|
|
390
|
+
"--allow-capability",
|
|
391
|
+
"--maximum-input-bytes",
|
|
392
|
+
]), new Set());
|
|
393
|
+
if (options.positionals.length > 0) {
|
|
394
|
+
return invalid("agent harness accepts one adapter kind and named options");
|
|
395
|
+
}
|
|
396
|
+
const origins = options.values.get("--allow-origin") ?? [];
|
|
397
|
+
for (const origin of origins) {
|
|
398
|
+
let url;
|
|
399
|
+
try {
|
|
400
|
+
url = new URL(origin);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return invalid(`Invalid agent harness origin: ${origin}`);
|
|
404
|
+
}
|
|
405
|
+
if (url.protocol !== "https:" || url.origin !== origin) {
|
|
406
|
+
return invalid(`Agent harness origin must be an exact HTTPS origin: ${origin}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const configuration = Schema.decodeUnknownOption(Schema.Struct({
|
|
410
|
+
kind: SupportedAgentHarness,
|
|
411
|
+
executable: Schema.NonEmptyString,
|
|
412
|
+
maximumInputBytes: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1024 * 1024)),
|
|
413
|
+
allowedPaths: Schema.Array(Schema.NonEmptyString),
|
|
414
|
+
allowedExecutables: Schema.Array(Schema.NonEmptyString),
|
|
415
|
+
executableAuthorizations: Schema.Array(ExecutableAuthorizationSchema),
|
|
416
|
+
allowedOrigins: Schema.Array(Schema.NonEmptyString),
|
|
417
|
+
allowedCapabilities: Schema.Array(AgentHarnessCapability),
|
|
418
|
+
}))({
|
|
419
|
+
kind,
|
|
420
|
+
executable: one(options, "--executable", true),
|
|
421
|
+
maximumInputBytes: parsePositiveInteger(one(options, "--maximum-input-bytes") ?? `${1024 * 1024}`, "agent harness maximum input bytes", 1024 * 1024),
|
|
422
|
+
allowedPaths: options.values.get("--allow-path") ?? [],
|
|
423
|
+
allowedExecutables: [
|
|
424
|
+
...new Set(options.values.get("--allow-leaf-executable") ?? []),
|
|
425
|
+
],
|
|
426
|
+
executableAuthorizations: (options.values.get("--allow-leaf-executable") ?? [])
|
|
427
|
+
.map((executable) => ({ executable, behavior: "leaf" })),
|
|
428
|
+
allowedOrigins: origins,
|
|
429
|
+
allowedCapabilities: options.values.get("--allow-capability") ?? [],
|
|
430
|
+
});
|
|
431
|
+
if (Option.isNone(configuration)) {
|
|
432
|
+
return invalid("Invalid agent harness configuration");
|
|
433
|
+
}
|
|
434
|
+
const unclassifiable = configuration.value.executableAuthorizations.find((authorization) => isNestedCommandLauncher(authorization.executable));
|
|
435
|
+
if (unclassifiable !== undefined) {
|
|
436
|
+
return invalid(`${unclassifiable.executable} launches nested commands that cannot be bounded by an execution model; remove it from the agent harness allowlist`);
|
|
437
|
+
}
|
|
438
|
+
return command({
|
|
439
|
+
_tag: "AgentHarnessSet",
|
|
440
|
+
configuration: configuration.value,
|
|
441
|
+
}, format);
|
|
442
|
+
}
|
|
443
|
+
if (area === "schedule") {
|
|
444
|
+
if (action === "status" && rest.length === 0)
|
|
445
|
+
return command({ _tag: "ScheduleStatus" }, format);
|
|
446
|
+
if (action === "remove" && rest.length === 0)
|
|
447
|
+
return command({ _tag: "ScheduleRemove" }, format);
|
|
448
|
+
if (action === "set") {
|
|
449
|
+
const options = parseOptions(rest, new Set(["--timezone", "--executable"]), new Set());
|
|
450
|
+
if (options.positionals.length !== 1) {
|
|
451
|
+
return invalid("Usage: canonfig schedule set <calendar>");
|
|
452
|
+
}
|
|
453
|
+
const executable = one(options, "--executable");
|
|
454
|
+
const parsed = {
|
|
455
|
+
_tag: "ScheduleSet",
|
|
456
|
+
schedule: parseSchedule(options.positionals[0], one(options, "--timezone")),
|
|
457
|
+
};
|
|
458
|
+
return command(executable === undefined ? parsed : { ...parsed, executable }, format);
|
|
459
|
+
}
|
|
460
|
+
return invalid(`Unknown schedule command: ${action ?? ""}`);
|
|
461
|
+
}
|
|
462
|
+
return invalid(`Unknown argument: ${area ?? ""}`);
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
return invalid(error instanceof Error ? error.message : "Invalid command input");
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
export const evaluateCli = (arguments_) => {
|
|
469
|
+
if (arguments_.length === 0 || arguments_.includes("--help") || arguments_.includes("-h")) {
|
|
470
|
+
return { _tag: "Help", text: helpText, exitCode: CliExitCode.success };
|
|
471
|
+
}
|
|
472
|
+
if (arguments_.includes("--version") || arguments_.includes("-V")) {
|
|
473
|
+
return { _tag: "Version", text: programVersion, exitCode: CliExitCode.success };
|
|
474
|
+
}
|
|
475
|
+
const format = arguments_.includes("--json") ? "json" : "human";
|
|
476
|
+
return evaluateCommand(arguments_.filter((argument) => argument !== "--json"), format);
|
|
477
|
+
};
|
|
478
|
+
const commandName = (value) => {
|
|
479
|
+
switch (value._tag) {
|
|
480
|
+
case "SourceInit": return "source.init";
|
|
481
|
+
case "SourceScan": return "source.scan";
|
|
482
|
+
case "SourcePublish": return "source.publish";
|
|
483
|
+
case "SourceServe": return "source.serve";
|
|
484
|
+
case "SourceInvite": return "source.invite";
|
|
485
|
+
case "SourceRevoke": return "source.revoke";
|
|
486
|
+
case "FollowerEnroll": return "follower.enroll";
|
|
487
|
+
case "Synchronize": return `sync.${value.mode}`;
|
|
488
|
+
case "Recover": return "recover";
|
|
489
|
+
case "Status": return "status";
|
|
490
|
+
case "OverlayList": return "overlay.list";
|
|
491
|
+
case "OverlaySet": return "overlay.set";
|
|
492
|
+
case "OverlayRemove": return "overlay.remove";
|
|
493
|
+
case "Doctor": return "doctor";
|
|
494
|
+
case "ProfileList": return "profile.list";
|
|
495
|
+
case "ProfileShow": return "profile.show";
|
|
496
|
+
case "ProfileSelect": return "profile.select";
|
|
497
|
+
case "AgentPolicyGet": return "agent.policy.get";
|
|
498
|
+
case "AgentPolicySet": return "agent.policy.set";
|
|
499
|
+
case "AgentHarnessGet": return "agent.harness.get";
|
|
500
|
+
case "AgentHarnessSet": return "agent.harness.set";
|
|
501
|
+
case "ScheduleSet": return "schedule.set";
|
|
502
|
+
case "ScheduleStatus": return "schedule.status";
|
|
503
|
+
case "ScheduleRemove": return "schedule.remove";
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
const executeCommand = Effect.fn("Cli.executeCommand")(function* (value) {
|
|
507
|
+
const source = yield* SourceCommands;
|
|
508
|
+
const follower = yield* FollowerCommands;
|
|
509
|
+
switch (value._tag) {
|
|
510
|
+
case "SourceInit": return yield* source.initialize();
|
|
511
|
+
case "SourceScan": return yield* source.scan({ files: value.files });
|
|
512
|
+
case "SourcePublish":
|
|
513
|
+
return yield* source.publish({
|
|
514
|
+
proposalPath: value.proposalPath,
|
|
515
|
+
profile: value.profile,
|
|
516
|
+
name: value.name,
|
|
517
|
+
profilePath: value.profilePath,
|
|
518
|
+
reviewer: value.reviewer,
|
|
519
|
+
});
|
|
520
|
+
case "SourceServe":
|
|
521
|
+
return yield* source.serve({ hostname: value.hostname, port: value.port });
|
|
522
|
+
case "SourceInvite":
|
|
523
|
+
return yield* source.invite({
|
|
524
|
+
endpoint: value.endpoint,
|
|
525
|
+
expiresInMilliseconds: value.expiresInMilliseconds,
|
|
526
|
+
groups: value.groups,
|
|
527
|
+
});
|
|
528
|
+
case "SourceRevoke": return yield* source.revoke(value.follower);
|
|
529
|
+
case "FollowerEnroll":
|
|
530
|
+
return yield* follower.enroll({
|
|
531
|
+
invitation: value.invitation,
|
|
532
|
+
followerName: value.followerName,
|
|
533
|
+
selectedProfile: value.selectedProfile,
|
|
534
|
+
});
|
|
535
|
+
case "Synchronize":
|
|
536
|
+
return yield* follower.synchronize({
|
|
537
|
+
mode: value.mode,
|
|
538
|
+
noInput: value.noInput,
|
|
539
|
+
});
|
|
540
|
+
case "Recover": return yield* follower.recover({ noInput: value.noInput });
|
|
541
|
+
case "Status": return yield* follower.status(value.follower);
|
|
542
|
+
case "OverlayList": return yield* follower.listLocalOverlays();
|
|
543
|
+
case "OverlaySet":
|
|
544
|
+
return yield* follower.setLocalOverlay({
|
|
545
|
+
resource: value.resource,
|
|
546
|
+
target: value.target,
|
|
547
|
+
keys: value.keys,
|
|
548
|
+
});
|
|
549
|
+
case "OverlayRemove": return yield* follower.removeLocalOverlay(value.resource);
|
|
550
|
+
case "Doctor":
|
|
551
|
+
return yield* follower.doctor({
|
|
552
|
+
noInput: value.noInput,
|
|
553
|
+
timeoutMilliseconds: value.timeoutMilliseconds,
|
|
554
|
+
});
|
|
555
|
+
case "ProfileList": return yield* source.listProfiles();
|
|
556
|
+
case "ProfileShow": return yield* source.inspectProfile(value.revision);
|
|
557
|
+
case "ProfileSelect": return yield* follower.selectProfile(value.profile);
|
|
558
|
+
case "AgentPolicyGet": return yield* follower.getAgentPolicy();
|
|
559
|
+
case "AgentPolicySet": return yield* follower.setAgentPolicy(value.policy);
|
|
560
|
+
case "AgentHarnessGet": return yield* follower.getAgentHarness();
|
|
561
|
+
case "AgentHarnessSet":
|
|
562
|
+
return yield* follower.setAgentHarness(value.configuration);
|
|
563
|
+
case "ScheduleSet":
|
|
564
|
+
return yield* follower.setSchedule(value.executable === undefined
|
|
565
|
+
? { schedule: value.schedule }
|
|
566
|
+
: { schedule: value.schedule, executable: value.executable });
|
|
567
|
+
case "ScheduleStatus": return yield* follower.scheduleStatus();
|
|
568
|
+
case "ScheduleRemove": return yield* follower.removeSchedule();
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
export const runCli = Effect.fn("runCli")(function* (arguments_, io) {
|
|
572
|
+
const outcome = evaluateCli(arguments_);
|
|
573
|
+
if (outcome._tag === "Help" || outcome._tag === "Version") {
|
|
574
|
+
yield* Effect.sync(() => {
|
|
575
|
+
io.writeStdout(`${outcome.text}\n`);
|
|
576
|
+
io.setExitCode(outcome.exitCode);
|
|
577
|
+
});
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (outcome._tag === "InvalidInput") {
|
|
581
|
+
yield* Effect.sync(() => {
|
|
582
|
+
io.writeStderr(`${outcome.message}\n`);
|
|
583
|
+
io.setExitCode(outcome.exitCode);
|
|
584
|
+
});
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const name = commandName(outcome.command);
|
|
588
|
+
const result = yield* executeCommand(outcome.command).pipe(Effect.match({
|
|
589
|
+
onFailure: (failure) => ({
|
|
590
|
+
command: name,
|
|
591
|
+
message: failure.message,
|
|
592
|
+
data: failure.details,
|
|
593
|
+
exitCode: exitCodeForFailure(failure.category),
|
|
594
|
+
}),
|
|
595
|
+
onSuccess: (data) => ({
|
|
596
|
+
command: name,
|
|
597
|
+
message: `${name} completed`,
|
|
598
|
+
data,
|
|
599
|
+
exitCode: CliExitCode.success,
|
|
600
|
+
}),
|
|
601
|
+
}));
|
|
602
|
+
yield* Effect.sync(() => {
|
|
603
|
+
const rendered = renderCliResult(result, outcome.format);
|
|
604
|
+
const quietScheduledSuccess = result.exitCode === CliExitCode.success
|
|
605
|
+
&& outcome.format === "human"
|
|
606
|
+
&& outcome.command._tag === "Synchronize"
|
|
607
|
+
&& outcome.command.mode === "apply"
|
|
608
|
+
&& outcome.command.noInput;
|
|
609
|
+
if (quietScheduledSuccess) {
|
|
610
|
+
// Native schedulers need no success chatter; failures remain visible.
|
|
611
|
+
}
|
|
612
|
+
else if (result.exitCode === CliExitCode.success)
|
|
613
|
+
io.writeStdout(rendered);
|
|
614
|
+
else
|
|
615
|
+
io.writeStderr(rendered);
|
|
616
|
+
io.setExitCode(result.exitCode);
|
|
617
|
+
});
|
|
618
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const CliExitCode = {
|
|
2
|
+
success: 0,
|
|
3
|
+
internal: 1,
|
|
4
|
+
usageOrConfiguration: 2,
|
|
5
|
+
humanActionRequired: 3,
|
|
6
|
+
conflictOrDrift: 4,
|
|
7
|
+
authenticationOrRevocation: 5,
|
|
8
|
+
transport: 6,
|
|
9
|
+
verificationOrApplyFailure: 7,
|
|
10
|
+
};
|
|
11
|
+
export const exitCodeForFailure = (category) => {
|
|
12
|
+
switch (category) {
|
|
13
|
+
case "usage-or-configuration":
|
|
14
|
+
return CliExitCode.usageOrConfiguration;
|
|
15
|
+
case "human-action-required":
|
|
16
|
+
return CliExitCode.humanActionRequired;
|
|
17
|
+
case "conflict-or-drift":
|
|
18
|
+
return CliExitCode.conflictOrDrift;
|
|
19
|
+
case "authentication-or-revocation":
|
|
20
|
+
return CliExitCode.authenticationOrRevocation;
|
|
21
|
+
case "transport":
|
|
22
|
+
return CliExitCode.transport;
|
|
23
|
+
case "verification-or-apply-failure":
|
|
24
|
+
return CliExitCode.verificationOrApplyFailure;
|
|
25
|
+
case "internal":
|
|
26
|
+
return CliExitCode.internal;
|
|
27
|
+
}
|
|
28
|
+
};
|