@h-rig/cli-surface-plugin 0.0.6-alpha.146
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 +1 -0
- package/dist/src/app/drone-ui.d.ts +34 -0
- package/dist/src/app/drone-ui.js +278 -0
- package/dist/src/commands/_async-ui.d.ts +10 -0
- package/dist/src/commands/_async-ui.js +121 -0
- package/dist/src/commands/_cli-format.d.ts +56 -0
- package/dist/src/commands/_cli-format.js +332 -0
- package/dist/src/commands/_connection-state.d.ts +54 -0
- package/dist/src/commands/_connection-state.js +187 -0
- package/dist/src/commands/_doctor-checks.d.ts +9 -0
- package/dist/src/commands/_doctor-checks.js +24 -0
- package/dist/src/commands/_help-catalog.d.ts +29 -0
- package/dist/src/commands/_help-catalog.js +157 -0
- package/dist/src/commands/_inprocess-services.d.ts +33 -0
- package/dist/src/commands/_inprocess-services.js +102 -0
- package/dist/src/commands/_json-output.d.ts +11 -0
- package/dist/src/commands/_json-output.js +54 -0
- package/dist/src/commands/_parsers.d.ts +15 -0
- package/dist/src/commands/_parsers.js +114 -0
- package/dist/src/commands/_paths.d.ts +11 -0
- package/dist/src/commands/_paths.js +50 -0
- package/dist/src/commands/_pi-frontend.d.ts +35 -0
- package/dist/src/commands/_pi-frontend.js +64 -0
- package/dist/src/commands/_pi-install.d.ts +42 -0
- package/dist/src/commands/_pi-install.js +167 -0
- package/dist/src/commands/_policy.d.ts +8 -0
- package/dist/src/commands/_policy.js +138 -0
- package/dist/src/commands/_probes.d.ts +1 -0
- package/dist/src/commands/_probes.js +13 -0
- package/dist/src/commands/_run-driver-helpers.d.ts +26 -0
- package/dist/src/commands/_run-driver-helpers.js +132 -0
- package/dist/src/commands/_run-subcommands.d.ts +3 -0
- package/dist/src/commands/_run-subcommands.js +31 -0
- package/dist/src/commands/_spinner.d.ts +25 -0
- package/dist/src/commands/_spinner.js +65 -0
- package/dist/src/commands/agent.d.ts +3 -0
- package/dist/src/commands/agent.js +322 -0
- package/dist/src/commands/config.d.ts +3 -0
- package/dist/src/commands/config.js +193 -0
- package/dist/src/commands/dist.d.ts +28 -0
- package/dist/src/commands/dist.js +435 -0
- package/dist/src/commands/doctor.d.ts +3 -0
- package/dist/src/commands/doctor.js +171 -0
- package/dist/src/commands/github.d.ts +3 -0
- package/dist/src/commands/github.js +342 -0
- package/dist/src/commands/inbox.d.ts +19 -0
- package/dist/src/commands/inbox.js +241 -0
- package/dist/src/commands/init.d.ts +64 -0
- package/dist/src/commands/init.js +1449 -0
- package/dist/src/commands/inspect.d.ts +20 -0
- package/dist/src/commands/inspect.js +337 -0
- package/dist/src/commands/pi.d.ts +3 -0
- package/dist/src/commands/pi.js +177 -0
- package/dist/src/commands/plugin.d.ts +20 -0
- package/dist/src/commands/plugin.js +238 -0
- package/dist/src/commands/profile-and-review.d.ts +4 -0
- package/dist/src/commands/profile-and-review.js +223 -0
- package/dist/src/commands/queue.d.ts +3 -0
- package/dist/src/commands/queue.js +197 -0
- package/dist/src/commands/remote.d.ts +3 -0
- package/dist/src/commands/remote.js +516 -0
- package/dist/src/commands/repo-git-harness.d.ts +5 -0
- package/dist/src/commands/repo-git-harness.js +282 -0
- package/dist/src/commands/run.d.ts +22 -0
- package/dist/src/commands/run.js +645 -0
- package/dist/src/commands/server.d.ts +3 -0
- package/dist/src/commands/server.js +155 -0
- package/dist/src/commands/setup.d.ts +16 -0
- package/dist/src/commands/setup.js +356 -0
- package/dist/src/commands/stats.d.ts +11 -0
- package/dist/src/commands/stats.js +219 -0
- package/dist/src/commands/task-run-driver.d.ts +93 -0
- package/dist/src/commands/task-run-driver.js +136 -0
- package/dist/src/commands/task.d.ts +46 -0
- package/dist/src/commands/task.js +555 -0
- package/dist/src/commands/test.d.ts +3 -0
- package/dist/src/commands/test.js +46 -0
- package/dist/src/commands/triage.d.ts +11 -0
- package/dist/src/commands/triage.js +224 -0
- package/dist/src/commands/workspace.d.ts +3 -0
- package/dist/src/commands/workspace.js +130 -0
- package/dist/src/kernel-dispatch.d.ts +15 -0
- package/dist/src/kernel-dispatch.js +16 -0
- package/dist/src/plugin.d.ts +3 -0
- package/dist/src/plugin.js +5440 -0
- package/dist/src/rig-config-package-deps.d.ts +10 -0
- package/dist/src/rig-config-package-deps.js +272 -0
- package/dist/src/runner.d.ts +47 -0
- package/dist/src/runner.js +267 -0
- package/dist/src/version.d.ts +8 -0
- package/dist/src/version.js +47 -0
- package/dist/src/withMutedConsole.d.ts +2 -0
- package/dist/src/withMutedConsole.js +42 -0
- package/package.json +34 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type PiInstallCheck = {
|
|
2
|
+
readonly ok: boolean;
|
|
3
|
+
readonly label: string;
|
|
4
|
+
readonly detail?: string;
|
|
5
|
+
readonly hint?: string;
|
|
6
|
+
};
|
|
7
|
+
export type CommandRunner = (command: string[], options?: {
|
|
8
|
+
cwd?: string;
|
|
9
|
+
}) => Promise<{
|
|
10
|
+
exitCode: number;
|
|
11
|
+
stdout: string;
|
|
12
|
+
stderr: string;
|
|
13
|
+
}>;
|
|
14
|
+
export type PiRigInstallStatus = {
|
|
15
|
+
readonly pi: PiInstallCheck;
|
|
16
|
+
readonly piRig: PiInstallCheck;
|
|
17
|
+
readonly extensionPath: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function resolvePiRigExtensionPath(homeDir: string): string;
|
|
20
|
+
export declare function resolvePiRigPackageSource(projectRoot: string, exists?: (path: string) => boolean): string;
|
|
21
|
+
export declare function checkPiRigInstall(input?: {
|
|
22
|
+
readonly homeDir?: string;
|
|
23
|
+
readonly commandRunner?: CommandRunner;
|
|
24
|
+
readonly exists?: (path: string) => boolean;
|
|
25
|
+
}): Promise<PiRigInstallStatus>;
|
|
26
|
+
export declare function ensurePiRigInstalled(input: {
|
|
27
|
+
readonly projectRoot: string;
|
|
28
|
+
readonly homeDir?: string;
|
|
29
|
+
readonly commandRunner?: CommandRunner;
|
|
30
|
+
}): Promise<PiRigInstallStatus & {
|
|
31
|
+
installedPath: string;
|
|
32
|
+
}>;
|
|
33
|
+
export declare function ensureRemotePiRigInstalled(input: {
|
|
34
|
+
readonly requestJson: (pathname: string, init?: RequestInit) => Promise<unknown>;
|
|
35
|
+
}): Promise<PiRigInstallStatus & {
|
|
36
|
+
readonly remote: true;
|
|
37
|
+
}>;
|
|
38
|
+
export declare function buildPiSetupChecks(input?: {
|
|
39
|
+
readonly homeDir?: string;
|
|
40
|
+
readonly commandRunner?: CommandRunner;
|
|
41
|
+
readonly exists?: (path: string) => boolean;
|
|
42
|
+
}): Promise<PiInstallCheck[]>;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/cli-surface-plugin/src/commands/_pi-install.ts
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { resolve } from "path";
|
|
6
|
+
var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
|
|
7
|
+
async function defaultCommandRunner(command, options = {}) {
|
|
8
|
+
const proc = Bun.spawn(command, { cwd: options.cwd, stdout: "pipe", stderr: "pipe" });
|
|
9
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
10
|
+
new Response(proc.stdout).text(),
|
|
11
|
+
new Response(proc.stderr).text(),
|
|
12
|
+
proc.exited
|
|
13
|
+
]);
|
|
14
|
+
return { exitCode, stdout, stderr };
|
|
15
|
+
}
|
|
16
|
+
function resolvePiRigExtensionPath(homeDir) {
|
|
17
|
+
return resolve(homeDir, ".pi", "agent", "extensions", "pi-rig");
|
|
18
|
+
}
|
|
19
|
+
function resolvePiRigPackageSource(projectRoot, exists = existsSync) {
|
|
20
|
+
const localPackage = resolve(projectRoot, "packages", "pi-rig");
|
|
21
|
+
if (exists(resolve(localPackage, "package.json")))
|
|
22
|
+
return localPackage;
|
|
23
|
+
return `npm:${PI_RIG_PACKAGE_NAME}`;
|
|
24
|
+
}
|
|
25
|
+
function resolvePiHomeDir(inputHomeDir) {
|
|
26
|
+
return inputHomeDir ?? process.env.RIG_PI_HOME_DIR?.trim() ?? homedir();
|
|
27
|
+
}
|
|
28
|
+
function piListContainsPiRig(output) {
|
|
29
|
+
return output.split(/\r?\n/).some((line) => {
|
|
30
|
+
const normalized = line.trim();
|
|
31
|
+
return normalized.includes(PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function safeRun(runner, command, options) {
|
|
35
|
+
try {
|
|
36
|
+
return await runner(command, options);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return { exitCode: 1, stdout: "", stderr: error instanceof Error ? error.message : String(error) };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function splitInstallCommand(value) {
|
|
42
|
+
return value.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((part) => part.replace(/^['"]|['"]$/g, "")) ?? [];
|
|
43
|
+
}
|
|
44
|
+
async function ensurePiBinaryAvailable(input) {
|
|
45
|
+
const current = await safeRun(input.runner, ["pi", "--version"]);
|
|
46
|
+
if (current.exitCode === 0) {
|
|
47
|
+
const updateCommand = process.env.RIG_PI_UPDATE_COMMAND?.trim();
|
|
48
|
+
if (updateCommand) {
|
|
49
|
+
const parts2 = splitInstallCommand(updateCommand);
|
|
50
|
+
if (parts2.length > 0)
|
|
51
|
+
await safeRun(input.runner, parts2, input.projectRoot ? { cwd: input.projectRoot } : undefined);
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, detail: (current.stdout || current.stderr).trim() || undefined };
|
|
54
|
+
}
|
|
55
|
+
const installCommand = process.env.RIG_PI_INSTALL_COMMAND?.trim();
|
|
56
|
+
if (!installCommand) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
error: `${(current.stderr || current.stdout).trim() || "pi --version failed"}. Set RIG_PI_INSTALL_COMMAND to a supported installer or install Pi manually.`
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const parts = splitInstallCommand(installCommand);
|
|
63
|
+
if (parts.length === 0) {
|
|
64
|
+
return { ok: false, error: (current.stderr || current.stdout).trim() || "pi --version failed" };
|
|
65
|
+
}
|
|
66
|
+
const install = await safeRun(input.runner, parts, input.projectRoot ? { cwd: input.projectRoot } : undefined);
|
|
67
|
+
if (install.exitCode !== 0) {
|
|
68
|
+
return { ok: false, installedOrUpdated: true, error: (install.stderr || install.stdout).trim() || `Pi install command failed (${install.exitCode})` };
|
|
69
|
+
}
|
|
70
|
+
const next = await safeRun(input.runner, ["pi", "--version"]);
|
|
71
|
+
return {
|
|
72
|
+
ok: next.exitCode === 0,
|
|
73
|
+
installedOrUpdated: true,
|
|
74
|
+
detail: (next.stdout || next.stderr).trim() || undefined,
|
|
75
|
+
...next.exitCode === 0 ? {} : { error: (next.stderr || next.stdout).trim() || "pi --version failed after install" }
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async function checkPiRigInstall(input = {}) {
|
|
79
|
+
const home = resolvePiHomeDir(input.homeDir);
|
|
80
|
+
const extensionPath = resolvePiRigExtensionPath(home);
|
|
81
|
+
if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
|
|
82
|
+
return {
|
|
83
|
+
extensionPath,
|
|
84
|
+
pi: { ok: true, label: "pi", detail: "fake-pi" },
|
|
85
|
+
piRig: { ok: true, label: "pi-rig global extension", detail: extensionPath }
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const runner = input.commandRunner ?? defaultCommandRunner;
|
|
89
|
+
const piResult = await safeRun(runner, ["pi", "--version"]);
|
|
90
|
+
const piListResult = piResult.exitCode === 0 ? await safeRun(runner, ["pi", "list"]) : { exitCode: 1, stdout: "", stderr: "" };
|
|
91
|
+
const hasPiRig = piListResult.exitCode === 0 && piListContainsPiRig(`${piListResult.stdout}
|
|
92
|
+
${piListResult.stderr}`);
|
|
93
|
+
const hasLegacyBridgeScaffold = !hasPiRig && (input.exists ?? existsSync)(extensionPath);
|
|
94
|
+
return {
|
|
95
|
+
extensionPath,
|
|
96
|
+
pi: {
|
|
97
|
+
ok: piResult.exitCode === 0,
|
|
98
|
+
label: "pi",
|
|
99
|
+
detail: (piResult.stdout || piResult.stderr).trim() || undefined,
|
|
100
|
+
hint: piResult.exitCode === 0 ? undefined : "Install Pi/OMP manually or set RIG_PI_INSTALL_COMMAND before verifying with bare `rig` / Cockpit \u2192 Doctor."
|
|
101
|
+
},
|
|
102
|
+
piRig: {
|
|
103
|
+
ok: hasPiRig,
|
|
104
|
+
label: "pi-rig global extension",
|
|
105
|
+
detail: hasPiRig ? piListResult.stdout.trim() || PI_RIG_PACKAGE_NAME : hasLegacyBridgeScaffold ? `legacy bridge scaffold at ${extensionPath}` : undefined,
|
|
106
|
+
hint: hasPiRig ? undefined : `Install the Rig OMP extension with \`pi install ${PI_RIG_PACKAGE_NAME}\`, then verify with bare \`rig\` / Cockpit \u2192 Doctor.`
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async function ensurePiRigInstalled(input) {
|
|
111
|
+
const home = resolvePiHomeDir(input.homeDir);
|
|
112
|
+
if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
|
|
113
|
+
const status = await checkPiRigInstall({ homeDir: home, commandRunner: input.commandRunner });
|
|
114
|
+
return { ...status, installedPath: status.extensionPath };
|
|
115
|
+
}
|
|
116
|
+
const runner = input.commandRunner ?? defaultCommandRunner;
|
|
117
|
+
const piAvailable = await ensurePiBinaryAvailable({ runner, projectRoot: input.projectRoot });
|
|
118
|
+
if (!piAvailable.ok) {
|
|
119
|
+
throw new Error(`Pi install/update failed: ${piAvailable.error ?? "pi unavailable"}`);
|
|
120
|
+
}
|
|
121
|
+
const packageSource = resolvePiRigPackageSource(input.projectRoot);
|
|
122
|
+
const install = await runner(["pi", "install", packageSource], { cwd: input.projectRoot });
|
|
123
|
+
if (install.exitCode !== 0) {
|
|
124
|
+
throw new Error(`pi-rig install failed: ${(install.stderr || install.stdout).trim() || `exit ${install.exitCode}`}`);
|
|
125
|
+
}
|
|
126
|
+
const next = await checkPiRigInstall({ homeDir: home, commandRunner: runner });
|
|
127
|
+
return { ...next, installedPath: packageSource };
|
|
128
|
+
}
|
|
129
|
+
async function ensureRemotePiRigInstalled(input) {
|
|
130
|
+
const payload = await input.requestJson("/api/pi-rig/install", {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: { "content-type": "application/json" },
|
|
133
|
+
body: JSON.stringify({ package: PI_RIG_PACKAGE_NAME, scope: "global" })
|
|
134
|
+
});
|
|
135
|
+
const record = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
136
|
+
const piOk = record.piOk === true || record.ok === true;
|
|
137
|
+
const piRigOk = record.piRigOk === true || record.installed === true || record.ok === true;
|
|
138
|
+
const extensionPath = typeof record.extensionPath === "string" ? record.extensionPath : "remote:~/.pi/agent/extensions/pi-rig";
|
|
139
|
+
return {
|
|
140
|
+
remote: true,
|
|
141
|
+
extensionPath,
|
|
142
|
+
pi: {
|
|
143
|
+
ok: piOk,
|
|
144
|
+
label: "pi",
|
|
145
|
+
detail: typeof record.piVersion === "string" ? record.piVersion : undefined,
|
|
146
|
+
hint: piOk ? undefined : "Install/update Pi on the selected remote control host, then verify with bare `rig` / Cockpit \u2192 Doctor."
|
|
147
|
+
},
|
|
148
|
+
piRig: {
|
|
149
|
+
ok: piRigOk,
|
|
150
|
+
label: "pi-rig global extension",
|
|
151
|
+
detail: extensionPath,
|
|
152
|
+
hint: piRigOk ? undefined : `Install/enable the Rig OMP extension (${PI_RIG_PACKAGE_NAME}) on the selected remote control host, then verify with bare \`rig\` / Cockpit \u2192 Doctor.`
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
async function buildPiSetupChecks(input = {}) {
|
|
157
|
+
const status = await checkPiRigInstall(input);
|
|
158
|
+
return [status.pi, status.piRig];
|
|
159
|
+
}
|
|
160
|
+
export {
|
|
161
|
+
resolvePiRigPackageSource,
|
|
162
|
+
resolvePiRigExtensionPath,
|
|
163
|
+
ensureRemotePiRigInstalled,
|
|
164
|
+
ensurePiRigInstalled,
|
|
165
|
+
checkPiRigInstall,
|
|
166
|
+
buildPiSetupChecks
|
|
167
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type RunnerContext } from "../runner";
|
|
2
|
+
export type PolicyMode = "off" | "observe" | "enforce";
|
|
3
|
+
export declare function resolveEffectivePolicyMode(context: RunnerContext): PolicyMode;
|
|
4
|
+
export declare function appendControlledBashAudit(context: RunnerContext, mode: PolicyMode, command: string, args: string[], matchedRuleIds: string[], action?: "warn" | "blocked"): void;
|
|
5
|
+
export declare function enforceNativeCommandPolicy(context: RunnerContext, args: string[], options: {
|
|
6
|
+
commandPrefix: string;
|
|
7
|
+
writeAuditLog: boolean;
|
|
8
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/cli-surface-plugin/src/commands/_policy.ts
|
|
3
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
|
|
4
|
+
import { resolve as resolve2 } from "path";
|
|
5
|
+
|
|
6
|
+
// packages/cli-surface-plugin/src/runner.ts
|
|
7
|
+
import { EventBus } from "@rig/runtime/control-plane/runtime/events";
|
|
8
|
+
import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
|
|
9
|
+
import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
|
|
10
|
+
import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
|
|
11
|
+
|
|
12
|
+
class CliError extends RuntimeCliError {
|
|
13
|
+
hint;
|
|
14
|
+
constructor(message, exitCode = 1, options = {}) {
|
|
15
|
+
super(message, exitCode);
|
|
16
|
+
if (options.hint?.trim()) {
|
|
17
|
+
this.hint = options.hint.trim();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function formatCommand(parts) {
|
|
22
|
+
return parts.map((part) => /[^a-zA-Z0-9_./:-]/.test(part) ? JSON.stringify(part) : part).join(" ");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// packages/cli-surface-plugin/src/commands/_paths.ts
|
|
26
|
+
import { resolve } from "path";
|
|
27
|
+
import { resolveMonorepoRoot } from "@rig/runtime/layout";
|
|
28
|
+
function resolveControlPlaneHostStateRoot(projectRoot) {
|
|
29
|
+
return resolve(projectRoot, ".rig");
|
|
30
|
+
}
|
|
31
|
+
function resolveControlPlaneHostLogsDir(projectRoot) {
|
|
32
|
+
return resolve(resolveControlPlaneHostStateRoot(projectRoot), "logs");
|
|
33
|
+
}
|
|
34
|
+
function resolveControlPlaneDefinitionRoot(projectRoot) {
|
|
35
|
+
return resolve(projectRoot, "rig");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// packages/cli-surface-plugin/src/commands/_policy.ts
|
|
39
|
+
function loadPolicyFile(projectRoot) {
|
|
40
|
+
const policyPath = resolve2(resolveControlPlaneDefinitionRoot(projectRoot), "policy", "policy.json");
|
|
41
|
+
if (!existsSync(policyPath)) {
|
|
42
|
+
return { mode: "observe", rules: [] };
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(readFileSync(policyPath, "utf8"));
|
|
46
|
+
const mode = parsed.mode === "off" || parsed.mode === "observe" || parsed.mode === "enforce" ? parsed.mode : "observe";
|
|
47
|
+
const rules = Array.isArray(parsed.rules) ? parsed.rules.filter((value) => Boolean(value && typeof value === "object" && !Array.isArray(value))).map((value, index) => {
|
|
48
|
+
const rule = {
|
|
49
|
+
id: typeof value.id === "string" && value.id.trim() ? value.id.trim() : `rule-${index + 1}`
|
|
50
|
+
};
|
|
51
|
+
const reason = typeof value.reason === "string" && value.reason.trim() ? value.reason.trim() : null;
|
|
52
|
+
const match = typeof value.match === "string" && value.match.trim() ? value.match.trim() : null;
|
|
53
|
+
const command = typeof value.command === "string" && value.command.trim() ? value.command.trim() : null;
|
|
54
|
+
if (reason)
|
|
55
|
+
return { ...rule, reason };
|
|
56
|
+
if (match && command)
|
|
57
|
+
return { ...rule, match, command };
|
|
58
|
+
if (match)
|
|
59
|
+
return { ...rule, match };
|
|
60
|
+
if (command)
|
|
61
|
+
return { ...rule, command };
|
|
62
|
+
return rule;
|
|
63
|
+
}) : [];
|
|
64
|
+
return { mode, rules };
|
|
65
|
+
} catch {
|
|
66
|
+
return { mode: "observe", rules: [] };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function matchingRules(command, rules) {
|
|
70
|
+
return rules.filter((rule) => {
|
|
71
|
+
const rawPattern = rule.command ?? rule.match;
|
|
72
|
+
if (!rawPattern)
|
|
73
|
+
return false;
|
|
74
|
+
try {
|
|
75
|
+
return new RegExp(rawPattern, "i").test(command);
|
|
76
|
+
} catch {
|
|
77
|
+
return command.includes(rawPattern);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function resolveEffectivePolicyMode(context) {
|
|
82
|
+
const envMode = process.env.RIG_BASH_MODE;
|
|
83
|
+
if (envMode === "off" || envMode === "observe" || envMode === "enforce") {
|
|
84
|
+
return envMode;
|
|
85
|
+
}
|
|
86
|
+
return context.policyMode ?? loadPolicyFile(context.projectRoot).mode;
|
|
87
|
+
}
|
|
88
|
+
function appendControlledBashAudit(context, mode, command, args, matchedRuleIds, action) {
|
|
89
|
+
if (mode === "off") {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const logsDir = resolveControlPlaneHostLogsDir(context.projectRoot);
|
|
93
|
+
const logFile = resolve2(logsDir, "controlled-bash.jsonl");
|
|
94
|
+
mkdirSync(logsDir, { recursive: true });
|
|
95
|
+
const payload = {
|
|
96
|
+
timestamp: new Date().toISOString(),
|
|
97
|
+
mode,
|
|
98
|
+
cwd: process.cwd(),
|
|
99
|
+
pid: process.pid,
|
|
100
|
+
ppid: process.ppid,
|
|
101
|
+
argv: args,
|
|
102
|
+
command,
|
|
103
|
+
matchedRuleIds
|
|
104
|
+
};
|
|
105
|
+
if (action) {
|
|
106
|
+
payload.action = action;
|
|
107
|
+
}
|
|
108
|
+
appendFileSync(logFile, `${JSON.stringify(payload)}
|
|
109
|
+
`, "utf-8");
|
|
110
|
+
}
|
|
111
|
+
async function enforceNativeCommandPolicy(context, args, options) {
|
|
112
|
+
const mode = resolveEffectivePolicyMode(context);
|
|
113
|
+
const command = formatCommand([options.commandPrefix, ...args]);
|
|
114
|
+
const rules = matchingRules(command, loadPolicyFile(context.projectRoot).rules);
|
|
115
|
+
const matchedRuleIds = rules.map((rule) => rule.id);
|
|
116
|
+
const action = rules.length === 0 ? "allow" : mode === "enforce" ? "block" : "warn";
|
|
117
|
+
await context.emitEvent("policy.decision", {
|
|
118
|
+
target: "command",
|
|
119
|
+
command,
|
|
120
|
+
mode,
|
|
121
|
+
allowed: action !== "block",
|
|
122
|
+
matchedRuleIds,
|
|
123
|
+
reasons: rules.map((rule) => rule.reason).filter(Boolean)
|
|
124
|
+
});
|
|
125
|
+
if (options.writeAuditLog) {
|
|
126
|
+
appendControlledBashAudit(context, mode, command, args, matchedRuleIds, action === "block" ? "blocked" : action === "warn" ? "warn" : undefined);
|
|
127
|
+
}
|
|
128
|
+
if (action === "block") {
|
|
129
|
+
throw new CliError(`Policy blocked command: ${command}`, 126, {
|
|
130
|
+
hint: "Review rig/policy/policy.json, or re-run with `--policy-mode observe` to log instead of block."
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export {
|
|
135
|
+
resolveEffectivePolicyMode,
|
|
136
|
+
enforceNativeCommandPolicy,
|
|
137
|
+
appendControlledBashAudit
|
|
138
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runQuietBinaryProbe(binaryPath: string, args: string[], cwd: string): Promise<boolean>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/cli-surface-plugin/src/commands/_probes.ts
|
|
3
|
+
async function runQuietBinaryProbe(binaryPath, args, cwd) {
|
|
4
|
+
try {
|
|
5
|
+
const run = await Bun.$`${binaryPath} ${args}`.cwd(cwd).quiet().nothrow();
|
|
6
|
+
return run.exitCode === 0;
|
|
7
|
+
} catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
runQuietBinaryProbe
|
|
13
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type SourceTaskContract = {
|
|
2
|
+
id: string;
|
|
3
|
+
title?: string | null;
|
|
4
|
+
description?: string | null;
|
|
5
|
+
body?: string | null;
|
|
6
|
+
acceptanceCriteria?: string | null;
|
|
7
|
+
acceptance_criteria?: string | null;
|
|
8
|
+
sourceIssueId?: string | null;
|
|
9
|
+
status?: string | null;
|
|
10
|
+
issueType?: string | null;
|
|
11
|
+
role?: string | null;
|
|
12
|
+
validation?: string[];
|
|
13
|
+
validators?: string[];
|
|
14
|
+
labels?: string[];
|
|
15
|
+
scope?: string[];
|
|
16
|
+
};
|
|
17
|
+
export declare function buildRunPrompt(input: {
|
|
18
|
+
projectRoot: string;
|
|
19
|
+
taskId?: string | undefined;
|
|
20
|
+
fallbackTitle?: string | undefined;
|
|
21
|
+
fallbackDescription?: string | undefined;
|
|
22
|
+
fallbackAcceptanceCriteria?: string | undefined;
|
|
23
|
+
sourceTask?: SourceTaskContract | null;
|
|
24
|
+
initialPrompt?: string;
|
|
25
|
+
runtimeAdapter: "pi";
|
|
26
|
+
}): string;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/cli-surface-plugin/src/commands/_run-driver-helpers.ts
|
|
3
|
+
import { existsSync, readFileSync } from "fs";
|
|
4
|
+
import { resolve as resolve2 } from "path";
|
|
5
|
+
|
|
6
|
+
// packages/cli-surface-plugin/src/commands/_paths.ts
|
|
7
|
+
import { resolve } from "path";
|
|
8
|
+
import { resolveMonorepoRoot } from "@rig/runtime/layout";
|
|
9
|
+
function resolveControlPlaneMonorepoRoot(projectRoot) {
|
|
10
|
+
return resolveMonorepoRoot(projectRoot);
|
|
11
|
+
}
|
|
12
|
+
function resolveControlPlaneTaskConfigPath(projectRoot) {
|
|
13
|
+
return resolve(resolveControlPlaneMonorepoRoot(projectRoot), ".rig", "task-config.json");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// packages/cli-surface-plugin/src/commands/_run-driver-helpers.ts
|
|
17
|
+
function readLatestBeadRecord(projectRoot, taskId) {
|
|
18
|
+
const issuesPath = resolve2(resolveControlPlaneMonorepoRoot(projectRoot), ".beads", "issues.jsonl");
|
|
19
|
+
if (!existsSync(issuesPath)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
let latest = null;
|
|
23
|
+
for (const line of readFileSync(issuesPath, "utf8").split(/\r?\n/)) {
|
|
24
|
+
const trimmed = line.trim();
|
|
25
|
+
if (!trimmed)
|
|
26
|
+
continue;
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(trimmed);
|
|
29
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
30
|
+
continue;
|
|
31
|
+
const record = parsed;
|
|
32
|
+
if (record.id === taskId)
|
|
33
|
+
latest = record;
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
return latest;
|
|
37
|
+
}
|
|
38
|
+
function firstPromptString(...values) {
|
|
39
|
+
for (const value of values) {
|
|
40
|
+
if (typeof value === "string" && value.trim())
|
|
41
|
+
return value.trim();
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function uniqueStrings(values) {
|
|
46
|
+
return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));
|
|
47
|
+
}
|
|
48
|
+
function renderSourceTaskContract(task, validation) {
|
|
49
|
+
if (!task)
|
|
50
|
+
return validation.length > 0 ? [`Validation: ${validation.join(", ")}`] : [];
|
|
51
|
+
const lines = [`id: ${task.id}`];
|
|
52
|
+
if (task.sourceIssueId)
|
|
53
|
+
lines.push(`source issue: ${task.sourceIssueId}`);
|
|
54
|
+
if (task.status)
|
|
55
|
+
lines.push(`status: ${task.status}`);
|
|
56
|
+
if (task.issueType)
|
|
57
|
+
lines.push(`issue type: ${task.issueType}`);
|
|
58
|
+
if (task.role)
|
|
59
|
+
lines.push(`role: ${task.role}`);
|
|
60
|
+
if (validation.length > 0)
|
|
61
|
+
lines.push(`validation: ${validation.join(", ")}`);
|
|
62
|
+
return lines;
|
|
63
|
+
}
|
|
64
|
+
function renderSourceScopeValidation(task, validation) {
|
|
65
|
+
if (task || validation.length === 0)
|
|
66
|
+
return "";
|
|
67
|
+
return `Validation:
|
|
68
|
+
- ${validation.join(`
|
|
69
|
+
- `)}`;
|
|
70
|
+
}
|
|
71
|
+
function readTaskScopeText(projectRoot, taskId) {
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(readFileSync(resolveControlPlaneTaskConfigPath(projectRoot), "utf8"));
|
|
74
|
+
const entry = parsed[taskId] ?? {};
|
|
75
|
+
const scope = Array.isArray(entry.scope) ? entry.scope.filter((item) => typeof item === "string") : [];
|
|
76
|
+
const validation = Array.isArray(entry.validation) ? entry.validation.filter((item) => typeof item === "string") : [];
|
|
77
|
+
const lines = [];
|
|
78
|
+
if (scope.length > 0)
|
|
79
|
+
lines.push(`Scope:
|
|
80
|
+
- ${scope.join(`
|
|
81
|
+
- `)}`);
|
|
82
|
+
if (validation.length > 0)
|
|
83
|
+
lines.push(`Validation:
|
|
84
|
+
- ${validation.join(`
|
|
85
|
+
- `)}`);
|
|
86
|
+
return lines.join(`
|
|
87
|
+
|
|
88
|
+
`);
|
|
89
|
+
} catch {
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function providerInstructionLines(_runtimeAdapter) {
|
|
94
|
+
return ["Keep file writes inside the scoped task workspace and use the bridged Pi tooling for shell and follow-up prompts."];
|
|
95
|
+
}
|
|
96
|
+
function buildRunPrompt(input) {
|
|
97
|
+
const initialPrompt = input.initialPrompt?.trim() || null;
|
|
98
|
+
if (!input.taskId) {
|
|
99
|
+
return initialPrompt ?? input.fallbackTitle?.trim() ?? "Continue the requested run and complete the work.";
|
|
100
|
+
}
|
|
101
|
+
const bead = readLatestBeadRecord(input.projectRoot, input.taskId);
|
|
102
|
+
const sourceTask = input.sourceTask ?? null;
|
|
103
|
+
const sourceDescription = firstPromptString(sourceTask?.description, sourceTask?.body);
|
|
104
|
+
const sourceAcceptance = firstPromptString(sourceTask?.acceptanceCriteria, sourceTask?.acceptance_criteria);
|
|
105
|
+
const sourceValidation = uniqueStrings([...sourceTask?.validation ?? [], ...sourceTask?.validators ?? []]);
|
|
106
|
+
const title = (bead && typeof bead.title === "string" ? bead.title : null) ?? firstPromptString(sourceTask?.title) ?? input.fallbackTitle ?? input.taskId;
|
|
107
|
+
const description = (bead && typeof bead.description === "string" ? bead.description : null) ?? sourceDescription ?? input.fallbackDescription ?? "";
|
|
108
|
+
const acceptance = (bead && typeof bead.acceptance_criteria === "string" ? bead.acceptance_criteria : null) ?? sourceAcceptance ?? input.fallbackAcceptanceCriteria ?? "";
|
|
109
|
+
const scopeText = readTaskScopeText(input.projectRoot, input.taskId);
|
|
110
|
+
const sourceContractLines = renderSourceTaskContract(sourceTask, sourceValidation);
|
|
111
|
+
const providerLines = providerInstructionLines(input.runtimeAdapter);
|
|
112
|
+
return [
|
|
113
|
+
`You are working on task ${input.taskId}: ${title}.`,
|
|
114
|
+
description ? `Description:
|
|
115
|
+
${description}` : null,
|
|
116
|
+
acceptance ? `Acceptance criteria:
|
|
117
|
+
${acceptance}` : null,
|
|
118
|
+
sourceContractLines.length > 0 ? `Source task contract:
|
|
119
|
+
${sourceContractLines.join(`
|
|
120
|
+
`)}` : null,
|
|
121
|
+
scopeText || renderSourceScopeValidation(sourceTask, sourceValidation) || null,
|
|
122
|
+
initialPrompt ? `Additional operator guidance:
|
|
123
|
+
${initialPrompt}` : null,
|
|
124
|
+
providerLines.length > 0 ? `Provider-specific runtime tooling:
|
|
125
|
+
${providerLines.join(" ")}` : null
|
|
126
|
+
].filter((value) => Boolean(value)).join(`
|
|
127
|
+
|
|
128
|
+
`);
|
|
129
|
+
}
|
|
130
|
+
export {
|
|
131
|
+
buildRunPrompt
|
|
132
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/cli-surface-plugin/src/commands/_run-subcommands.ts
|
|
3
|
+
var RUN_GROUP_SUBCOMMANDS = [
|
|
4
|
+
"list",
|
|
5
|
+
"status",
|
|
6
|
+
"start",
|
|
7
|
+
"start-serial",
|
|
8
|
+
"start-parallel",
|
|
9
|
+
"show",
|
|
10
|
+
"timeline",
|
|
11
|
+
"replay",
|
|
12
|
+
"attach",
|
|
13
|
+
"steer",
|
|
14
|
+
"stop",
|
|
15
|
+
"resume",
|
|
16
|
+
"restart",
|
|
17
|
+
"delete",
|
|
18
|
+
"cleanup"
|
|
19
|
+
];
|
|
20
|
+
var RUN_GROUP_SUBCOMMAND_SET = new Set(RUN_GROUP_SUBCOMMANDS);
|
|
21
|
+
function isRunGroupInvocation(first) {
|
|
22
|
+
if (first === undefined)
|
|
23
|
+
return true;
|
|
24
|
+
if (first === "--help" || first === "-h" || first === "help")
|
|
25
|
+
return true;
|
|
26
|
+
return RUN_GROUP_SUBCOMMAND_SET.has(first);
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
isRunGroupInvocation,
|
|
30
|
+
RUN_GROUP_SUBCOMMANDS
|
|
31
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const SPINNER_FRAMES: readonly ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
2
|
+
export type TtySpinner = {
|
|
3
|
+
setLabel(label: string): void;
|
|
4
|
+
/** Erase the spinner line so other output can print cleanly. */
|
|
5
|
+
pause(): void;
|
|
6
|
+
/** Resume rendering after pause(). */
|
|
7
|
+
resume(): void;
|
|
8
|
+
/** Stop for good; optionally leave a final line. */
|
|
9
|
+
stop(finalLine?: string): void;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* A line-rewriting spinner for async CLI waits. On non-TTY outputs it prints
|
|
13
|
+
* the label once (and label changes) so logs stay readable without ANSI.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createTtySpinner(input: {
|
|
16
|
+
label: string;
|
|
17
|
+
output?: Pick<NodeJS.WriteStream, "write"> & {
|
|
18
|
+
readonly isTTY?: boolean;
|
|
19
|
+
};
|
|
20
|
+
intervalMs?: number;
|
|
21
|
+
/** Frame glyph cycle; defaults to SPINNER_FRAMES. */
|
|
22
|
+
frames?: readonly string[];
|
|
23
|
+
/** Optional glyph decorator (e.g. color), applied on TTY renders only. */
|
|
24
|
+
styleFrame?: (frame: string) => string;
|
|
25
|
+
}): TtySpinner;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/cli-surface-plugin/src/commands/_spinner.ts
|
|
3
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
4
|
+
function createTtySpinner(input) {
|
|
5
|
+
const output = input.output ?? process.stdout;
|
|
6
|
+
const isTty = output.isTTY === true;
|
|
7
|
+
const frames = input.frames && input.frames.length > 0 ? input.frames : SPINNER_FRAMES;
|
|
8
|
+
let label = input.label;
|
|
9
|
+
let frame = 0;
|
|
10
|
+
let paused = false;
|
|
11
|
+
let stopped = false;
|
|
12
|
+
let lastPrintedLabel = "";
|
|
13
|
+
const render = () => {
|
|
14
|
+
if (stopped || paused)
|
|
15
|
+
return;
|
|
16
|
+
if (!isTty) {
|
|
17
|
+
if (label !== lastPrintedLabel) {
|
|
18
|
+
output.write(`${label}
|
|
19
|
+
`);
|
|
20
|
+
lastPrintedLabel = label;
|
|
21
|
+
}
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
frame = (frame + 1) % frames.length;
|
|
25
|
+
const glyph = frames[frame] ?? frames[0] ?? "";
|
|
26
|
+
output.write(`\r\x1B[2K${input.styleFrame ? input.styleFrame(glyph) : glyph} ${label}`);
|
|
27
|
+
};
|
|
28
|
+
const clearLine = () => {
|
|
29
|
+
if (isTty)
|
|
30
|
+
output.write("\r\x1B[2K");
|
|
31
|
+
};
|
|
32
|
+
render();
|
|
33
|
+
const timer = isTty ? setInterval(render, input.intervalMs ?? 16) : null;
|
|
34
|
+
return {
|
|
35
|
+
setLabel(next) {
|
|
36
|
+
label = next;
|
|
37
|
+
render();
|
|
38
|
+
},
|
|
39
|
+
pause() {
|
|
40
|
+
paused = true;
|
|
41
|
+
clearLine();
|
|
42
|
+
},
|
|
43
|
+
resume() {
|
|
44
|
+
if (stopped)
|
|
45
|
+
return;
|
|
46
|
+
paused = false;
|
|
47
|
+
render();
|
|
48
|
+
},
|
|
49
|
+
stop(finalLine) {
|
|
50
|
+
if (stopped)
|
|
51
|
+
return;
|
|
52
|
+
stopped = true;
|
|
53
|
+
if (timer)
|
|
54
|
+
clearInterval(timer);
|
|
55
|
+
clearLine();
|
|
56
|
+
if (finalLine)
|
|
57
|
+
output.write(`${finalLine}
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export {
|
|
63
|
+
createTtySpinner,
|
|
64
|
+
SPINNER_FRAMES
|
|
65
|
+
};
|