@kylecheng3146/agent-ops 0.1.3 → 0.1.5
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 +88 -13
- package/dist/packages/cli/src/args.js +39 -6
- package/dist/packages/cli/src/bin.js +33 -11
- package/dist/packages/cli/src/cli.js +8 -8
- package/dist/packages/cli/src/commands/doctor.js +31 -9
- package/dist/packages/cli/src/commands/hook.js +18 -16
- package/dist/packages/cli/src/commands/init.js +29 -7
- package/dist/packages/cli/src/commands/review.js +5 -1
- package/dist/packages/cli/src/commands/uninstall.js +6 -5
- package/dist/packages/cli/src/commands/update.js +13 -5
- package/dist/packages/cli/src/context.js +9 -3
- package/dist/packages/cli/src/hook-process.js +109 -7
- package/dist/packages/cli/src/plan-output.js +9 -4
- package/dist/packages/cli/src/public-plan.js +62 -0
- package/dist/packages/cli/src/ui.js +242 -1
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/cli/src/wizard.js +68 -2
- package/dist/runtime/src/adapters/claude/config.js +0 -8
- package/dist/runtime/src/adapters/claude/events.js +26 -0
- package/dist/runtime/src/adapters/claude/output.js +6 -6
- package/dist/runtime/src/adapters/claude/surfaces.js +70 -0
- package/dist/runtime/src/adapters/codex/config.js +29 -11
- package/dist/runtime/src/adapters/codex/events.js +26 -0
- package/dist/runtime/src/adapters/codex/output.js +10 -0
- package/dist/runtime/src/adapters/codex/surfaces.js +12 -0
- package/dist/runtime/src/adapters/opencode/config.js +170 -0
- package/dist/runtime/src/adapters/opencode/events.js +49 -0
- package/dist/runtime/src/adapters/opencode/input.js +32 -0
- package/dist/runtime/src/adapters/opencode/output.js +23 -0
- package/dist/runtime/src/adapters/opencode/surfaces.js +23 -0
- package/dist/runtime/src/config/explain.js +7 -0
- package/dist/runtime/src/config/hash.js +24 -0
- package/dist/runtime/src/config/merge.js +6 -2
- package/dist/runtime/src/config/migrate.js +19 -4
- package/dist/runtime/src/contracts.js +10 -1
- package/dist/runtime/src/fs/manifest.js +32 -1
- package/dist/runtime/src/fs/transaction.js +14 -2
- package/dist/runtime/src/hooks/advisory.js +16 -0
- package/dist/runtime/src/hooks/stop-service.js +70 -0
- package/dist/runtime/src/hooks/stop-verify.js +4 -1
- package/dist/runtime/src/install/doctor.js +119 -9
- package/dist/runtime/src/install/harness.js +296 -35
- package/dist/runtime/src/install/hooks.js +22 -17
- package/dist/runtime/src/install/ownership.js +110 -34
- package/dist/runtime/src/install/plan.js +207 -28
- package/dist/runtime/src/install/probes.js +9 -43
- package/dist/runtime/src/install/profiles.js +8 -1
- package/dist/runtime/src/install/surface-inspection.js +296 -0
- package/dist/runtime/src/install/surfaces.js +11 -0
- package/dist/runtime/src/install/uninstall.js +10 -3
- package/dist/runtime/src/install/update.js +8 -2
- package/dist/runtime/src/schema/validate.js +37 -18
- package/dist/runtime/src/task/service.js +3 -3
- package/dist/runtime/src/task/store.js +12 -3
- package/dist/runtime/src/verify/command-executor.js +113 -0
- package/dist/runtime/src/verify/evidence.js +4 -24
- package/dist/runtime/src/verify/service.js +20 -92
- package/dist/runtime/src/verify/spawn.js +6 -1
- package/docs/en/guides/configuration.md +83 -0
- package/docs/en/guides/quickstart.md +9 -0
- package/docs/en/guides/security.md +5 -0
- package/docs/en/spec/README.md +9 -0
- package/docs/en/spec/harness-adapters.md +66 -2
- package/docs/en/spec/maintenance.md +11 -0
- package/docs/en/spec/review.md +11 -0
- package/docs/zh-TW/guides/configuration.md +77 -0
- package/docs/zh-TW/guides/quickstart.md +9 -0
- package/docs/zh-TW/guides/security.md +5 -0
- package/docs/zh-TW/spec/README.md +9 -0
- package/docs/zh-TW/spec/harness-adapters.md +57 -3
- package/docs/zh-TW/spec/maintenance.md +11 -1
- package/docs/zh-TW/spec/review.md +10 -0
- package/package.json +8 -4
- package/postinstall.cjs +102 -0
- package/schemas/config.schema.json +55 -1
- package/schemas/manifest.schema.json +7 -2
- package/templates/common/AGENTS.block.md +2 -1
- package/templates/common/CLAUDE.block.md +2 -1
- package/dist/runtime/src/review/claude-runner.js +0 -4
- package/dist/runtime/src/review/codex-runner.js +0 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { applyUninstallPlan, createUninstallPlan } from "../../../../runtime/src/install/uninstall.js";
|
|
2
2
|
import { okEnvelope } from "../output.js";
|
|
3
3
|
import { formatOperationPlan } from "../plan-output.js";
|
|
4
|
+
import { toPublicUninstallPlan } from "../public-plan.js";
|
|
4
5
|
function formatUninstallPlan(plan) {
|
|
5
6
|
return formatOperationPlan({
|
|
6
7
|
title: "Uninstall plan",
|
|
@@ -13,14 +14,14 @@ function formatUninstallPlan(plan) {
|
|
|
13
14
|
`Harness: ${plan.manifest.harness}`
|
|
14
15
|
])
|
|
15
16
|
],
|
|
16
|
-
operations: plan.operations
|
|
17
|
+
operations: toPublicUninstallPlan(plan).operations
|
|
17
18
|
});
|
|
18
19
|
}
|
|
19
20
|
function uninstallError(code, message, plan) {
|
|
20
21
|
return {
|
|
21
22
|
code,
|
|
22
23
|
status: "error",
|
|
23
|
-
data: { applied: false, plan, message },
|
|
24
|
+
data: { applied: false, plan: toPublicUninstallPlan(plan), message },
|
|
24
25
|
errors: [{ code, message }]
|
|
25
26
|
};
|
|
26
27
|
}
|
|
@@ -29,14 +30,14 @@ export async function runUninstallCommand(options) {
|
|
|
29
30
|
if (!plan.installed) {
|
|
30
31
|
return okEnvelope("UNINSTALL_NOT_INSTALLED", {
|
|
31
32
|
applied: false,
|
|
32
|
-
plan,
|
|
33
|
+
plan: toPublicUninstallPlan(plan),
|
|
33
34
|
message: "No managed installation exists."
|
|
34
35
|
});
|
|
35
36
|
}
|
|
36
37
|
if (options.args.dryRun) {
|
|
37
38
|
return okEnvelope("UNINSTALL_PLAN_READY", {
|
|
38
39
|
applied: false,
|
|
39
|
-
plan,
|
|
40
|
+
plan: toPublicUninstallPlan(plan),
|
|
40
41
|
message: "Uninstall plan calculated; no files were changed.",
|
|
41
42
|
text: formatUninstallPlan(plan)
|
|
42
43
|
});
|
|
@@ -51,7 +52,7 @@ export async function runUninstallCommand(options) {
|
|
|
51
52
|
await applyUninstallPlan(options.root, plan);
|
|
52
53
|
return okEnvelope("UNINSTALL_APPLIED", {
|
|
53
54
|
applied: true,
|
|
54
|
-
plan,
|
|
55
|
+
plan: toPublicUninstallPlan(plan),
|
|
55
56
|
message: "Managed installation content was removed."
|
|
56
57
|
});
|
|
57
58
|
}
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
import { applyUpdatePlan, createUpdatePlan } from "../../../../runtime/src/install/update.js";
|
|
2
2
|
import { okEnvelope } from "../output.js";
|
|
3
3
|
import { formatOperationPlan } from "../plan-output.js";
|
|
4
|
+
import { toPublicUpdatePlan } from "../public-plan.js";
|
|
4
5
|
export function formatUpdatePlan(plan) {
|
|
5
6
|
return formatOperationPlan({
|
|
6
7
|
title: "Update plan",
|
|
7
8
|
metadata: [
|
|
8
9
|
`Target version: ${plan.targetVersion}`,
|
|
10
|
+
`Harness: ${plan.installation.harness.join(", ")}`,
|
|
9
11
|
`Schema migrations: ${plan.migrationSteps.length === 0
|
|
10
12
|
? "none"
|
|
11
13
|
: plan.migrationSteps
|
|
12
14
|
.map(({ fromVersion, toVersion }) => `${fromVersion} -> ${toVersion}`)
|
|
13
15
|
.join(", ")}`
|
|
14
16
|
],
|
|
15
|
-
operations: plan.installation.operations
|
|
17
|
+
operations: toPublicUpdatePlan(plan).installation.operations
|
|
16
18
|
});
|
|
17
19
|
}
|
|
18
20
|
function updateError(code, message, plan) {
|
|
19
21
|
return {
|
|
20
22
|
code,
|
|
21
23
|
status: "error",
|
|
22
|
-
data: { applied: false, plan, message },
|
|
24
|
+
data: { applied: false, plan: toPublicUpdatePlan(plan), message },
|
|
23
25
|
errors: [{ code, message }]
|
|
24
26
|
};
|
|
25
27
|
}
|
|
@@ -27,6 +29,9 @@ export async function runUpdateCommand(options) {
|
|
|
27
29
|
const plan = await createUpdatePlan({
|
|
28
30
|
root: options.root,
|
|
29
31
|
adapters: options.adapters,
|
|
32
|
+
...(options.args.harness === undefined
|
|
33
|
+
? {}
|
|
34
|
+
: { harness: options.args.harness }),
|
|
30
35
|
...(options.registry === undefined
|
|
31
36
|
? {}
|
|
32
37
|
: { registry: options.registry }),
|
|
@@ -35,12 +40,15 @@ export async function runUpdateCommand(options) {
|
|
|
35
40
|
: { targetVersion: options.targetVersion }),
|
|
36
41
|
...(options.hookRuntimePath === undefined
|
|
37
42
|
? {}
|
|
38
|
-
: { hookRuntimePath: options.hookRuntimePath })
|
|
43
|
+
: { hookRuntimePath: options.hookRuntimePath }),
|
|
44
|
+
...((options.hookTargets ?? options.args.hookTargets) === undefined
|
|
45
|
+
? {}
|
|
46
|
+
: { hookTargets: options.hookTargets ?? options.args.hookTargets })
|
|
39
47
|
});
|
|
40
48
|
if (options.args.dryRun) {
|
|
41
49
|
return okEnvelope("UPDATE_PLAN_READY", {
|
|
42
50
|
applied: false,
|
|
43
|
-
plan,
|
|
51
|
+
plan: toPublicUpdatePlan(plan),
|
|
44
52
|
message: "Update plan calculated; no files were changed.",
|
|
45
53
|
text: formatUpdatePlan(plan)
|
|
46
54
|
});
|
|
@@ -55,7 +63,7 @@ export async function runUpdateCommand(options) {
|
|
|
55
63
|
await applyUpdatePlan(options.root, plan);
|
|
56
64
|
return okEnvelope("UPDATE_APPLIED", {
|
|
57
65
|
applied: true,
|
|
58
|
-
plan,
|
|
66
|
+
plan: toPublicUpdatePlan(plan),
|
|
59
67
|
message: `Managed installation updated to ${plan.targetVersion}.`
|
|
60
68
|
});
|
|
61
69
|
}
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { calculateConfigHash } from "../../../runtime/src/config/hash.js";
|
|
5
|
+
import { sha256 } from "../../../runtime/src/fs/hash.js";
|
|
4
6
|
import { loadConfigFile } from "../../../runtime/src/config/load.js";
|
|
5
7
|
import { mergeConfigLayers } from "../../../runtime/src/config/merge.js";
|
|
6
|
-
import { sha256 } from "../../../runtime/src/fs/hash.js";
|
|
7
8
|
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
8
9
|
import { localStatePaths } from "../../../runtime/src/security/permissions.js";
|
|
9
10
|
import { calculateTrustBinding, FileTrustStore } from "../../../runtime/src/security/trust.js";
|
|
10
11
|
export const DEFAULT_CONFIG = {
|
|
11
|
-
schemaVersion:
|
|
12
|
+
schemaVersion: 2,
|
|
12
13
|
profiles: [],
|
|
13
14
|
verification: { commands: [] },
|
|
15
|
+
features: {
|
|
16
|
+
stopVerification: {
|
|
17
|
+
enabled: false
|
|
18
|
+
}
|
|
19
|
+
},
|
|
14
20
|
pathMappings: [],
|
|
15
21
|
securityExceptions: []
|
|
16
22
|
};
|
|
@@ -91,7 +97,7 @@ export async function repositoryTrust(root, config, cliVersion) {
|
|
|
91
97
|
const binding = await calculateTrustBinding({
|
|
92
98
|
repositoryPath: root,
|
|
93
99
|
remoteUrl: repositoryRemoteUrl(root),
|
|
94
|
-
configHash:
|
|
100
|
+
configHash: calculateConfigHash(config),
|
|
95
101
|
runtimeHash: sha256(cliVersion)
|
|
96
102
|
});
|
|
97
103
|
return (await new FileTrustStore(state.trustStore, state.anchorDirectory).status(binding)).status;
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { calculateConfigHash } from "../../../runtime/src/config/hash.js";
|
|
4
|
+
import { claudeStopRecursionMarker } from "../../../runtime/src/adapters/claude/input.js";
|
|
5
|
+
import { HARNESS_IDS, harnessDescriptor } from "../../../runtime/src/install/harness.js";
|
|
6
|
+
import { STOP_VERIFICATION_ENV, StopVerificationService } from "../../../runtime/src/hooks/stop-service.js";
|
|
7
|
+
import { NodeVerificationProcessRunner } from "../../../runtime/src/verify/spawn.js";
|
|
1
8
|
import { runHookCommand, HOOK_EVENTS } from "./commands/hook.js";
|
|
2
9
|
import { loadEffectiveConfig, repositoryTrust } from "./context.js";
|
|
3
|
-
const HARNESSES = new Set(
|
|
10
|
+
const HARNESSES = new Set(HARNESS_IDS);
|
|
4
11
|
const MAX_HOOK_INPUT_BYTES = 1024 * 1024;
|
|
12
|
+
const execFile = promisify(execFileCallback);
|
|
5
13
|
async function readStdin(stream) {
|
|
6
14
|
const chunks = [];
|
|
7
15
|
let total = 0;
|
|
@@ -17,28 +25,122 @@ async function readStdin(stream) {
|
|
|
17
25
|
}
|
|
18
26
|
return Buffer.concat(chunks, total).toString("utf8");
|
|
19
27
|
}
|
|
28
|
+
function parseInput(source) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(source);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function defaultGitRunner(root) {
|
|
37
|
+
return {
|
|
38
|
+
run: async (args) => {
|
|
39
|
+
try {
|
|
40
|
+
const result = await execFile("git", [...args], {
|
|
41
|
+
cwd: root,
|
|
42
|
+
encoding: "buffer"
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
exitCode: 0,
|
|
46
|
+
stdout: result.stdout
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const failure = error;
|
|
51
|
+
return {
|
|
52
|
+
exitCode: failure.status ?? 1,
|
|
53
|
+
stdout: failure.stdout ?? new Uint8Array()
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function stopVerificationOptions(input) {
|
|
60
|
+
const nativeRecursion = input.harness === "claude" &&
|
|
61
|
+
claudeStopRecursionMarker(input.rawInput);
|
|
62
|
+
const recursionActive = nativeRecursion || process.env[STOP_VERIFICATION_ENV] === "1";
|
|
63
|
+
const configHash = calculateConfigHash(input.config);
|
|
64
|
+
return {
|
|
65
|
+
confirmedConfig: true,
|
|
66
|
+
trusted: input.trusted,
|
|
67
|
+
scopeMapped: true,
|
|
68
|
+
recursionMarker: recursionActive,
|
|
69
|
+
configHash,
|
|
70
|
+
verify: async () => await new StopVerificationService({
|
|
71
|
+
root: input.root,
|
|
72
|
+
config: input.config,
|
|
73
|
+
trusted: input.trusted,
|
|
74
|
+
gitRunner: input.gitRunner,
|
|
75
|
+
processRunner: input.processRunner,
|
|
76
|
+
recursionActive,
|
|
77
|
+
configHash
|
|
78
|
+
}).verify()
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function shouldBuildStopVerification(harness, event, config, rawInput) {
|
|
82
|
+
if (event !== "Stop" || !config.features.stopVerification.enabled) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
let normalized;
|
|
86
|
+
try {
|
|
87
|
+
normalized = harnessDescriptor(harness).runtime.normalizeInput(rawInput);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (normalized.event !== "stop") {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return harnessDescriptor(harness).control.registrations.some(({ capability, support }) => capability === "optional-stop-verify" && support !== "unsupported");
|
|
96
|
+
}
|
|
20
97
|
/**
|
|
21
98
|
* Runs one hook invocation. Always resolves to exit code 0: a hook that
|
|
22
99
|
* cannot answer must never block the harness it advises.
|
|
23
100
|
*/
|
|
24
|
-
export async function runHookProcess(argv, io, cliVersion) {
|
|
101
|
+
export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
25
102
|
const [harness, event] = argv;
|
|
26
103
|
if (harness === undefined ||
|
|
27
104
|
!HARNESSES.has(harness) ||
|
|
28
105
|
event === undefined ||
|
|
29
106
|
!HOOK_EVENTS.includes(event)) {
|
|
30
|
-
io.writeStderr(
|
|
107
|
+
io.writeStderr(`Usage: agent-ops hook <${HARNESS_IDS.join("|")}> <SessionStart|PreToolUse|Stop>\n`);
|
|
31
108
|
return 0;
|
|
32
109
|
}
|
|
33
110
|
try {
|
|
34
|
-
const root = process.cwd();
|
|
35
|
-
const config =
|
|
111
|
+
const root = dependencies.root ?? process.cwd();
|
|
112
|
+
const config = dependencies.loadConfig === undefined
|
|
113
|
+
? (await loadEffectiveConfig(root, "project")).config
|
|
114
|
+
: await dependencies.loadConfig(root);
|
|
115
|
+
const trustStatus = dependencies.trust === undefined
|
|
116
|
+
? await repositoryTrust(root, config, cliVersion)
|
|
117
|
+
: await dependencies.trust(root, config, cliVersion);
|
|
118
|
+
const rawInput = await readStdin(io.stdin);
|
|
119
|
+
const parsedInput = parseInput(rawInput);
|
|
120
|
+
const trusted = trustStatus === "TRUSTED";
|
|
121
|
+
const gitRunner = dependencies.gitRunner ?? defaultGitRunner(root);
|
|
122
|
+
const processRunner = dependencies.processRunner ?? new NodeVerificationProcessRunner();
|
|
123
|
+
const stopVerification = shouldBuildStopVerification(harness, event, config, parsedInput)
|
|
124
|
+
? stopVerificationOptions({
|
|
125
|
+
harness: harness,
|
|
126
|
+
rawInput: parsedInput,
|
|
127
|
+
root,
|
|
128
|
+
config,
|
|
129
|
+
trusted,
|
|
130
|
+
gitRunner,
|
|
131
|
+
processRunner
|
|
132
|
+
})
|
|
133
|
+
: undefined;
|
|
36
134
|
const output = await runHookCommand({
|
|
37
135
|
harness: harness,
|
|
38
136
|
event: event,
|
|
39
|
-
stdin:
|
|
137
|
+
stdin: rawInput,
|
|
40
138
|
config,
|
|
41
|
-
trusted
|
|
139
|
+
trusted,
|
|
140
|
+
...(dependencies.advisory === undefined
|
|
141
|
+
? {}
|
|
142
|
+
: { advisory: dependencies.advisory }),
|
|
143
|
+
...(stopVerification === undefined ? {} : { stopVerification })
|
|
42
144
|
});
|
|
43
145
|
if (output.stdout.length > 0) {
|
|
44
146
|
io.writeStdout(output.stdout);
|
|
@@ -18,10 +18,15 @@ export function formatOperationPlan(options) {
|
|
|
18
18
|
for (const operation of options.operations) {
|
|
19
19
|
lines.push(`- ${operation.kind} ${escapeTerminalText(operation.path)}`, ` expected: ${operation.expectedHash ?? "<absent>"}`);
|
|
20
20
|
if (operation.kind === "write") {
|
|
21
|
-
|
|
22
|
-
.
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
if ("contentHash" in operation) {
|
|
22
|
+
lines.push(` content: <opaque ${escapeTerminalText(operation.contentHash)}>`, ` summary: ${escapeTerminalText(operation.summary)}`);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
lines.push(" content:", ...operation.content
|
|
26
|
+
.replace(/\n$/u, "")
|
|
27
|
+
.split("\n")
|
|
28
|
+
.map((line) => ` ${escapeTerminalText(line)}`));
|
|
29
|
+
}
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
return `${lines.join("\n")}\n`;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { redactSecrets } from "../../../runtime/src/security/redact.js";
|
|
2
|
+
import { sha256 } from "../../../runtime/src/fs/hash.js";
|
|
3
|
+
const OPAQUE_SUMMARY = "Opaque managed settings content withheld.";
|
|
4
|
+
function publicPath(path) {
|
|
5
|
+
return redactSecrets(path);
|
|
6
|
+
}
|
|
7
|
+
export function disclosureOf(operation) {
|
|
8
|
+
return operation.disclosure ?? "full";
|
|
9
|
+
}
|
|
10
|
+
export function toPublicOperation(operation) {
|
|
11
|
+
const path = publicPath(operation.path);
|
|
12
|
+
if (operation.kind === "remove") {
|
|
13
|
+
return {
|
|
14
|
+
kind: "remove",
|
|
15
|
+
path,
|
|
16
|
+
expectedHash: operation.expectedHash
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (disclosureOf(operation) === "opaque") {
|
|
20
|
+
return {
|
|
21
|
+
kind: "write",
|
|
22
|
+
path,
|
|
23
|
+
expectedHash: operation.expectedHash,
|
|
24
|
+
contentHash: sha256(operation.content),
|
|
25
|
+
summary: redactSecrets(OPAQUE_SUMMARY)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
kind: "write",
|
|
30
|
+
path,
|
|
31
|
+
expectedHash: operation.expectedHash,
|
|
32
|
+
content: operation.content
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function toPublicOperations(operations) {
|
|
36
|
+
return operations.map(toPublicOperation);
|
|
37
|
+
}
|
|
38
|
+
export function toPublicInstallPlan(plan) {
|
|
39
|
+
return {
|
|
40
|
+
scope: plan.scope,
|
|
41
|
+
harness: plan.harness,
|
|
42
|
+
profiles: plan.profiles,
|
|
43
|
+
capabilities: plan.capabilities,
|
|
44
|
+
manifest: plan.manifest,
|
|
45
|
+
operations: toPublicOperations(plan.operations)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function toPublicUpdatePlan(plan) {
|
|
49
|
+
return {
|
|
50
|
+
targetVersion: plan.targetVersion,
|
|
51
|
+
migrationSteps: plan.migrationSteps,
|
|
52
|
+
installation: toPublicInstallPlan(plan.installation)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function toPublicUninstallPlan(plan) {
|
|
56
|
+
return {
|
|
57
|
+
installed: plan.installed,
|
|
58
|
+
manifest: plan.manifest,
|
|
59
|
+
manifestHash: plan.manifestHash,
|
|
60
|
+
operations: toPublicOperations(plan.operations)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// figlet "agent-ops" -f Standard, trimmed of trailing blank lines/columns.
|
|
2
|
-
const BANNER = [
|
|
2
|
+
export const BANNER = [
|
|
3
3
|
" _",
|
|
4
4
|
" __ _ __ _ ___ _ __ | |_ ___ _ __ ___ ",
|
|
5
5
|
" / _\` |/ _\` |/ _ \\ '_ \\| __|____ / _ \\| '_ \\/ __|",
|
|
@@ -75,6 +75,247 @@ async function typedFallback(question, io, defaultValue) {
|
|
|
75
75
|
readline.close();
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
+
async function typedChoice(question, choices, io, defaultIndex) {
|
|
79
|
+
const { createInterface } = await import("node:readline/promises");
|
|
80
|
+
const readline = createInterface({ input: io.input, output: io.output });
|
|
81
|
+
try {
|
|
82
|
+
const options = choices
|
|
83
|
+
.map((choice, index) => `${index + 1}: ${choice.label}`)
|
|
84
|
+
.join(", ");
|
|
85
|
+
const answer = (await readline.question(`${question} [${options}]: `)).trim();
|
|
86
|
+
if (answer === "") {
|
|
87
|
+
return choices[defaultIndex].value;
|
|
88
|
+
}
|
|
89
|
+
const index = Number.parseInt(answer, 10) - 1;
|
|
90
|
+
if (Number.isInteger(index) && choices[index] !== undefined) {
|
|
91
|
+
return choices[index].value;
|
|
92
|
+
}
|
|
93
|
+
const match = choices.find((choice) => choice.label.toLowerCase() === answer.toLowerCase());
|
|
94
|
+
return match?.value ?? choices[defaultIndex].value;
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
readline.close();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function renderChoices(outputLike, question, choices, selected, vertical = false, selectAll = false, selectAllLabel = "Select all", selectAllDescription, focusedIndex = 0) {
|
|
101
|
+
const renderChoice = (choice, active, focused) => {
|
|
102
|
+
const label = `${active ? DOT_ON : DOT_OFF} ${choice.label}`;
|
|
103
|
+
const cursor = focused ? "❯ " : " ";
|
|
104
|
+
const renderedLabel = active
|
|
105
|
+
? bold(outputLike, green(outputLike, label))
|
|
106
|
+
: dim(outputLike, label);
|
|
107
|
+
const lines = [
|
|
108
|
+
vertical
|
|
109
|
+
? `${dim(outputLike, RAIL)} ${cursor}${renderedLabel}`
|
|
110
|
+
: `${cursor}${renderedLabel}`
|
|
111
|
+
];
|
|
112
|
+
if (vertical && choice.description !== undefined) {
|
|
113
|
+
lines.push(`${dim(outputLike, RAIL)} ${dim(outputLike, choice.description)}`);
|
|
114
|
+
}
|
|
115
|
+
return lines;
|
|
116
|
+
};
|
|
117
|
+
const allActive = selectAll && selected.size === choices.length;
|
|
118
|
+
const renderedChoices = [
|
|
119
|
+
...(selectAll
|
|
120
|
+
? renderChoice({
|
|
121
|
+
label: selectAllLabel,
|
|
122
|
+
value: undefined,
|
|
123
|
+
description: selectAllDescription
|
|
124
|
+
}, allActive, selectAll && focusedIndex === 0)
|
|
125
|
+
: []),
|
|
126
|
+
...choices.flatMap((choice, index) => {
|
|
127
|
+
const active = selected.has(index);
|
|
128
|
+
const focused = (selectAll ? index + 1 : index) === focusedIndex;
|
|
129
|
+
return renderChoice(choice, active, focused);
|
|
130
|
+
})
|
|
131
|
+
];
|
|
132
|
+
const rendered = vertical
|
|
133
|
+
? renderedChoices
|
|
134
|
+
: [`${dim(outputLike, RAIL)} ${renderedChoices.join(" / ")}`];
|
|
135
|
+
return [
|
|
136
|
+
`${bold(outputLike, DIAMOND)} ${question}`,
|
|
137
|
+
...rendered
|
|
138
|
+
].join("\n");
|
|
139
|
+
}
|
|
140
|
+
function assertChoices(choices) {
|
|
141
|
+
if (choices.length === 0) {
|
|
142
|
+
throw new Error("At least one selector choice is required.");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Single-value selector for interactive wizard choices. */
|
|
146
|
+
export async function selectOption(question, choices, io, defaultIndex = 0) {
|
|
147
|
+
assertChoices(choices);
|
|
148
|
+
const initialIndex = Math.min(Math.max(defaultIndex, 0), choices.length - 1);
|
|
149
|
+
if (typeof io.input.setRawMode !== "function" ||
|
|
150
|
+
io.input.isTTY !== true) {
|
|
151
|
+
return await typedChoice(question, choices, io, initialIndex);
|
|
152
|
+
}
|
|
153
|
+
const outputLike = {
|
|
154
|
+
isTTY: true,
|
|
155
|
+
columns: io.output.columns,
|
|
156
|
+
write: (value) => io.output.write(value)
|
|
157
|
+
};
|
|
158
|
+
const { emitKeypressEvents } = await import("node:readline");
|
|
159
|
+
emitKeypressEvents(io.input);
|
|
160
|
+
io.input.setRawMode(true);
|
|
161
|
+
io.input.resume();
|
|
162
|
+
let index = initialIndex;
|
|
163
|
+
let rendered = renderChoices(outputLike, question, choices, new Set([index]), false, false, "Select all", undefined, index);
|
|
164
|
+
io.output.write(`${rendered}\n`);
|
|
165
|
+
return await new Promise((resolve) => {
|
|
166
|
+
const cleanup = () => {
|
|
167
|
+
io.input.setRawMode?.(false);
|
|
168
|
+
io.input.pause();
|
|
169
|
+
io.input.removeListener("keypress", onKeypress);
|
|
170
|
+
};
|
|
171
|
+
const redraw = () => {
|
|
172
|
+
eraseLines((value) => io.output.write(value), rendered.split("\n").length);
|
|
173
|
+
rendered = renderChoices(outputLike, question, choices, new Set([index]), false, false, "Select all", undefined, index);
|
|
174
|
+
io.output.write(`${rendered}\n`);
|
|
175
|
+
};
|
|
176
|
+
const move = (delta) => {
|
|
177
|
+
index = (index + delta + choices.length) % choices.length;
|
|
178
|
+
redraw();
|
|
179
|
+
};
|
|
180
|
+
const onKeypress = (_chunk, key) => {
|
|
181
|
+
if (key?.ctrl === true && key.name === "c") {
|
|
182
|
+
cleanup();
|
|
183
|
+
process.exit(130);
|
|
184
|
+
}
|
|
185
|
+
if (key?.name === "up" ||
|
|
186
|
+
key?.name === "left" ||
|
|
187
|
+
key?.name === "h") {
|
|
188
|
+
move(-1);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (key?.name === "down" ||
|
|
192
|
+
key?.name === "right" ||
|
|
193
|
+
key?.name === "tab" ||
|
|
194
|
+
key?.name === "l") {
|
|
195
|
+
move(1);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (key?.name === "home") {
|
|
199
|
+
index = 0;
|
|
200
|
+
redraw();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (key?.name === "end") {
|
|
204
|
+
index = choices.length - 1;
|
|
205
|
+
redraw();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (key?.name === "return" || key?.name === "space") {
|
|
209
|
+
cleanup();
|
|
210
|
+
resolve(choices[index].value);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
io.input.on("keypress", onKeypress);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/** Multi-value selector. Space toggles a choice; Enter confirms. */
|
|
217
|
+
export async function selectOptions(question, choices, io, defaultValues = [], options = {}) {
|
|
218
|
+
assertChoices(choices);
|
|
219
|
+
const defaultIndexes = new Set(choices.flatMap((choice, index) => defaultValues.includes(choice.value) ? [index] : []));
|
|
220
|
+
if (defaultIndexes.size === 0 && options.selectAll !== true) {
|
|
221
|
+
defaultIndexes.add(0);
|
|
222
|
+
}
|
|
223
|
+
if (typeof io.input.setRawMode !== "function" ||
|
|
224
|
+
io.input.isTTY !== true) {
|
|
225
|
+
const value = await typedChoice(question, choices, io, 0);
|
|
226
|
+
return [value];
|
|
227
|
+
}
|
|
228
|
+
const selectAll = options.selectAll === true;
|
|
229
|
+
const choiceCount = choices.length + (selectAll ? 1 : 0);
|
|
230
|
+
const outputLike = {
|
|
231
|
+
isTTY: true,
|
|
232
|
+
columns: io.output.columns,
|
|
233
|
+
write: (value) => io.output.write(value)
|
|
234
|
+
};
|
|
235
|
+
const { emitKeypressEvents } = await import("node:readline");
|
|
236
|
+
emitKeypressEvents(io.input);
|
|
237
|
+
io.input.setRawMode(true);
|
|
238
|
+
io.input.resume();
|
|
239
|
+
let index = 0;
|
|
240
|
+
const selected = new Set(defaultIndexes);
|
|
241
|
+
let selectionHintShown = false;
|
|
242
|
+
let rendered = renderChoices(outputLike, question, choices, selected, true, selectAll, options.selectAllLabel, options.selectAllDescription, index);
|
|
243
|
+
io.output.write(`${rendered}\n`);
|
|
244
|
+
return await new Promise((resolve) => {
|
|
245
|
+
const cleanup = () => {
|
|
246
|
+
io.input.setRawMode?.(false);
|
|
247
|
+
io.input.pause();
|
|
248
|
+
io.input.removeListener("keypress", onKeypress);
|
|
249
|
+
};
|
|
250
|
+
const redraw = () => {
|
|
251
|
+
eraseLines((value) => io.output.write(value), rendered.split("\n").length);
|
|
252
|
+
selectionHintShown = false;
|
|
253
|
+
rendered = renderChoices(outputLike, question, choices, selected, true, selectAll, options.selectAllLabel, options.selectAllDescription, index);
|
|
254
|
+
io.output.write(`${rendered}\n`);
|
|
255
|
+
};
|
|
256
|
+
const move = (delta) => {
|
|
257
|
+
index = (index + delta + choiceCount) % choiceCount;
|
|
258
|
+
redraw();
|
|
259
|
+
};
|
|
260
|
+
const onKeypress = (_chunk, key) => {
|
|
261
|
+
if (key?.ctrl === true && key.name === "c") {
|
|
262
|
+
cleanup();
|
|
263
|
+
process.exit(130);
|
|
264
|
+
}
|
|
265
|
+
if (key?.name === "up" || key?.name === "left") {
|
|
266
|
+
move(-1);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (key?.name === "down" ||
|
|
270
|
+
key?.name === "right" ||
|
|
271
|
+
key?.name === "tab") {
|
|
272
|
+
move(1);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (key?.name === "space") {
|
|
276
|
+
if (selectAll && index === 0) {
|
|
277
|
+
if (selected.size === choices.length) {
|
|
278
|
+
selected.clear();
|
|
279
|
+
selected.add(0);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
choices.forEach((_choice, choiceIndex) => {
|
|
283
|
+
selected.add(choiceIndex);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
const choiceIndex = selectAll ? index - 1 : index;
|
|
289
|
+
if (selected.has(choiceIndex)) {
|
|
290
|
+
if (selected.size > 1) {
|
|
291
|
+
selected.delete(choiceIndex);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
selected.add(choiceIndex);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
redraw();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (key?.name === "return") {
|
|
302
|
+
if (selected.size === 0) {
|
|
303
|
+
if (!selectionHintShown) {
|
|
304
|
+
rendered = `${rendered}\n${dim(outputLike, `${RAIL} Choose at least one option with Space.`)}`;
|
|
305
|
+
selectionHintShown = true;
|
|
306
|
+
io.output.write(`${rendered}\n`);
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
cleanup();
|
|
311
|
+
resolve(choices
|
|
312
|
+
.filter((_choice, choiceIndex) => selected.has(choiceIndex))
|
|
313
|
+
.map((choice) => choice.value));
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
io.input.on("keypress", onKeypress);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
78
319
|
/**
|
|
79
320
|
* Arrow-key Yes/No selector. Falls back to a typed y/N prompt when stdin
|
|
80
321
|
* cannot enter raw mode (piped input, or a stub stream in tests), so the
|