@kal-elsam/kairo-runtime 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/package.json +1 -1
- package/scripts/install.sh +2 -2
- package/scripts/lib/install-script-url.mjs +30 -1
- package/src/cli.js +59 -3
- package/src/global/action-planner.js +178 -0
- package/src/global/agent-capabilities/create-capability-adapter.js +224 -0
- package/src/global/agent-capabilities/index.js +54 -0
- package/src/global/brand/cli.js +11 -7
- package/src/global/capability-registry.js +80 -0
- package/src/global/capability-states.js +30 -0
- package/src/global/cli-probe.js +47 -0
- package/src/global/ink/orchestrator-app.js +209 -0
- package/src/global/ink/orchestrator-state.js +78 -0
- package/src/global/ink/run-orchestrator-ink.js +29 -0
- package/src/global/orchestrator.js +139 -0
- package/src/global/paths.js +1 -0
- package/src/global/profile.js +146 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { buildAdapterContext } from "./adapter-context.js";
|
|
2
|
+
import { CAPABILITY_STATES } from "./capability-states.js";
|
|
3
|
+
import {
|
|
4
|
+
AGENT_CAPABILITY_IDS,
|
|
5
|
+
listCapabilityAdapters,
|
|
6
|
+
resolveCapabilityAdapter
|
|
7
|
+
} from "./agent-capabilities/index.js";
|
|
8
|
+
|
|
9
|
+
export async function inspectAllCapabilities({
|
|
10
|
+
homeDir,
|
|
11
|
+
workspaceRoot = process.cwd(),
|
|
12
|
+
packageName = "@kal-elsam/kairo-runtime",
|
|
13
|
+
probeOverrides = null
|
|
14
|
+
} = {}) {
|
|
15
|
+
const context = buildAdapterContext({ homeDir, workspaceRoot, packageName });
|
|
16
|
+
const adapters = listCapabilityAdapters();
|
|
17
|
+
|
|
18
|
+
return Promise.all(adapters.map(async (adapter) => {
|
|
19
|
+
const probeOptions = probeOverrides?.[adapter.id] ?? {};
|
|
20
|
+
const inspection = adapter.inspect(context, probeOptions);
|
|
21
|
+
const models = adapter.listModels?.(context, probeOptions) ?? null;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
...inspection,
|
|
25
|
+
models
|
|
26
|
+
};
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function inspectCapability(agentId, context, probeOptions = {}) {
|
|
31
|
+
const adapter = resolveCapabilityAdapter(agentId);
|
|
32
|
+
const inspection = adapter.inspect(context, probeOptions);
|
|
33
|
+
const models = adapter.listModels?.(context, probeOptions) ?? null;
|
|
34
|
+
return { ...inspection, models };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function summarizeCapabilityRegistry(capabilities) {
|
|
38
|
+
const byState = {};
|
|
39
|
+
for (const state of Object.values(CAPABILITY_STATES)) {
|
|
40
|
+
byState[state] = 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const capability of capabilities) {
|
|
44
|
+
if (byState[capability.state] != null) {
|
|
45
|
+
byState[capability.state] += 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
total: capabilities.length,
|
|
51
|
+
supported: AGENT_CAPABILITY_IDS.length,
|
|
52
|
+
detected: capabilities.filter((entry) => entry.detected).length,
|
|
53
|
+
available: capabilities.filter((entry) => entry.state === CAPABILITY_STATES.AVAILABLE).length,
|
|
54
|
+
byState
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildCapabilityDiagnostics(capabilities) {
|
|
59
|
+
const recommendations = capabilities
|
|
60
|
+
.map((entry) => entry.recommendation)
|
|
61
|
+
.filter(Boolean);
|
|
62
|
+
|
|
63
|
+
const errors = capabilities
|
|
64
|
+
.filter((entry) => entry.state === CAPABILITY_STATES.ERROR)
|
|
65
|
+
.map((entry) => ({
|
|
66
|
+
agent: entry.id,
|
|
67
|
+
message: entry.error ?? entry.recommendation ?? "Inspection failed."
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
recommendations,
|
|
72
|
+
errors,
|
|
73
|
+
hasActionableErrors: errors.length > 0
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function delegateToAgent(agentId, context, runOptions = {}) {
|
|
78
|
+
const adapter = resolveCapabilityAdapter(agentId);
|
|
79
|
+
return adapter.run(context, runOptions);
|
|
80
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const CAPABILITY_STATES = {
|
|
2
|
+
DETECTED: "detected",
|
|
3
|
+
AUTHENTICATED: "authenticated",
|
|
4
|
+
AVAILABLE: "available",
|
|
5
|
+
UNKNOWN: "unknown",
|
|
6
|
+
ERROR: "error"
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function isCapabilityState(value) {
|
|
10
|
+
return Object.values(CAPABILITY_STATES).includes(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatCapabilityState(state) {
|
|
14
|
+
switch (state) {
|
|
15
|
+
case CAPABILITY_STATES.DETECTED:
|
|
16
|
+
return "detected";
|
|
17
|
+
case CAPABILITY_STATES.AUTHENTICATED:
|
|
18
|
+
return "authenticated";
|
|
19
|
+
case CAPABILITY_STATES.AVAILABLE:
|
|
20
|
+
return "available";
|
|
21
|
+
case CAPABILITY_STATES.UNKNOWN:
|
|
22
|
+
return "unknown";
|
|
23
|
+
case CAPABILITY_STATES.ERROR:
|
|
24
|
+
return "error";
|
|
25
|
+
default: {
|
|
26
|
+
const _exhaustive = state;
|
|
27
|
+
return String(_exhaustive);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 5000;
|
|
4
|
+
|
|
5
|
+
export function isExecutableAvailable(command, { env = process.env } = {}) {
|
|
6
|
+
const result = spawnSync("which", [command], { encoding: "utf8", env });
|
|
7
|
+
return result.status === 0 && result.stdout.trim().length > 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function probeCommand(command, args = [], {
|
|
11
|
+
cwd = process.cwd(),
|
|
12
|
+
env = process.env,
|
|
13
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
14
|
+
} = {}) {
|
|
15
|
+
const result = spawnSync(command, args, {
|
|
16
|
+
cwd,
|
|
17
|
+
env,
|
|
18
|
+
encoding: "utf8",
|
|
19
|
+
timeout: timeoutMs
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
ok: result.status === 0,
|
|
24
|
+
status: result.status,
|
|
25
|
+
stdout: (result.stdout ?? "").trim(),
|
|
26
|
+
stderr: (result.stderr ?? "").trim(),
|
|
27
|
+
error: result.error?.message ?? null,
|
|
28
|
+
timedOut: result.error?.code === "ETIMEDOUT"
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parseVersionFromOutput(output) {
|
|
33
|
+
if (!output) return null;
|
|
34
|
+
|
|
35
|
+
const match = output.match(/(\d+\.\d+(?:\.\d+)?(?:[-+][\w.]+)?)/);
|
|
36
|
+
return match?.[1] ?? null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function resolveProbeState({ detected, cliAvailable, authReady, probeError, opaque = false }) {
|
|
40
|
+
if (probeError) return "error";
|
|
41
|
+
if (opaque) return "unknown";
|
|
42
|
+
if (!detected && !cliAvailable) return "unknown";
|
|
43
|
+
if (cliAvailable && authReady) return "available";
|
|
44
|
+
if (authReady) return "authenticated";
|
|
45
|
+
if (detected || cliAvailable) return "detected";
|
|
46
|
+
return "unknown";
|
|
47
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
3
|
+
import { BRAND } from "../brand/index.js";
|
|
4
|
+
import { PLAN_ACTIONS, buildActionPlan, buildReadOnlyDiagnostics } from "../action-planner.js";
|
|
5
|
+
import { buildProfileJson, resolveProfile } from "../profile.js";
|
|
6
|
+
import {
|
|
7
|
+
ORCHESTRATOR_MENU,
|
|
8
|
+
ORCHESTRATOR_VIEWS,
|
|
9
|
+
formatAgentStatusLines,
|
|
10
|
+
formatPlanLines,
|
|
11
|
+
formatProfileLines
|
|
12
|
+
} from "./orchestrator-state.js";
|
|
13
|
+
|
|
14
|
+
const COLORS = {
|
|
15
|
+
accent: "cyan",
|
|
16
|
+
success: "green",
|
|
17
|
+
warning: "yellow",
|
|
18
|
+
danger: "red",
|
|
19
|
+
muted: "gray"
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function OrchestratorApp({
|
|
23
|
+
homeDir,
|
|
24
|
+
workspaceRoot,
|
|
25
|
+
packageName,
|
|
26
|
+
packageRoot,
|
|
27
|
+
cliVersion,
|
|
28
|
+
onComplete
|
|
29
|
+
}) {
|
|
30
|
+
const { exit } = useApp();
|
|
31
|
+
const [view, setView] = useState(ORCHESTRATOR_VIEWS.HOME);
|
|
32
|
+
const [menuIndex, setMenuIndex] = useState(0);
|
|
33
|
+
const [loading, setLoading] = useState(true);
|
|
34
|
+
const [error, setError] = useState(null);
|
|
35
|
+
const [diagnostics, setDiagnostics] = useState(null);
|
|
36
|
+
const [profileJson, setProfileJson] = useState(null);
|
|
37
|
+
const [plan, setPlan] = useState(null);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
let cancelled = false;
|
|
41
|
+
|
|
42
|
+
async function load() {
|
|
43
|
+
try {
|
|
44
|
+
const [diag, profileResolved] = await Promise.all([
|
|
45
|
+
buildReadOnlyDiagnostics({ homeDir, workspaceRoot, packageName, packageRoot, cliVersion }),
|
|
46
|
+
resolveProfile({ homeDir, workspaceRoot })
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
if (cancelled) return;
|
|
50
|
+
setDiagnostics(diag);
|
|
51
|
+
setProfileJson(buildProfileJson(profileResolved));
|
|
52
|
+
setLoading(false);
|
|
53
|
+
} catch (loadError) {
|
|
54
|
+
if (cancelled) return;
|
|
55
|
+
setError(loadError instanceof Error ? loadError.message : String(loadError));
|
|
56
|
+
setLoading(false);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
load();
|
|
61
|
+
return () => {
|
|
62
|
+
cancelled = true;
|
|
63
|
+
};
|
|
64
|
+
}, [homeDir, workspaceRoot, packageName, packageRoot, cliVersion]);
|
|
65
|
+
|
|
66
|
+
const finish = (outcome) => {
|
|
67
|
+
onComplete(outcome);
|
|
68
|
+
exit();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
useInput((inputKey, key) => {
|
|
72
|
+
if (key.escape) {
|
|
73
|
+
if (view === ORCHESTRATOR_VIEWS.HOME) {
|
|
74
|
+
finish({ cancelled: true });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
setView(ORCHESTRATOR_VIEWS.HOME);
|
|
78
|
+
setPlan(null);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (loading || error) return;
|
|
83
|
+
|
|
84
|
+
if (view === ORCHESTRATOR_VIEWS.CONFIRM) {
|
|
85
|
+
if (inputKey.toLowerCase() === "y") {
|
|
86
|
+
finish({ cancelled: false, action: plan?.action ?? PLAN_ACTIONS.SETUP, confirmed: true, plan });
|
|
87
|
+
}
|
|
88
|
+
if (inputKey.toLowerCase() === "n") {
|
|
89
|
+
setView(ORCHESTRATOR_VIEWS.HOME);
|
|
90
|
+
setPlan(null);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (view !== ORCHESTRATOR_VIEWS.HOME) return;
|
|
96
|
+
|
|
97
|
+
if (key.upArrow) {
|
|
98
|
+
setMenuIndex((index) => Math.max(0, index - 1));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (key.downArrow) {
|
|
103
|
+
setMenuIndex((index) => Math.min(ORCHESTRATOR_MENU.length - 1, index + 1));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!key.return) return;
|
|
108
|
+
|
|
109
|
+
const item = ORCHESTRATOR_MENU[menuIndex];
|
|
110
|
+
if (item.action === "setup") {
|
|
111
|
+
buildActionPlan({
|
|
112
|
+
action: PLAN_ACTIONS.SETUP,
|
|
113
|
+
homeDir,
|
|
114
|
+
workspaceRoot,
|
|
115
|
+
packageName,
|
|
116
|
+
options: { packageRoot, cliVersion }
|
|
117
|
+
}).then((builtPlan) => {
|
|
118
|
+
setPlan(builtPlan);
|
|
119
|
+
setView(ORCHESTRATOR_VIEWS.CONFIRM);
|
|
120
|
+
}).catch((planError) => {
|
|
121
|
+
setError(planError instanceof Error ? planError.message : String(planError));
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
setView(item.view);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (loading) {
|
|
130
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
131
|
+
React.createElement(Text, { bold: true, color: COLORS.accent }, BRAND.displayName),
|
|
132
|
+
React.createElement(Text, { color: COLORS.muted }, "Loading agent capabilities…")
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (error) {
|
|
137
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
138
|
+
React.createElement(Text, { bold: true, color: COLORS.danger }, "Orchestrator error"),
|
|
139
|
+
React.createElement(Text, null, error),
|
|
140
|
+
React.createElement(Text, { dimColor: true }, "Esc to exit")
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
145
|
+
React.createElement(Text, { bold: true, color: COLORS.accent }, `${BRAND.displayName} orchestrator`),
|
|
146
|
+
React.createElement(Text, { color: COLORS.muted }, "Provider-neutral agent coordinator · no model imposed"),
|
|
147
|
+
React.createElement(Text, null, ""),
|
|
148
|
+
renderView({ view, diagnostics, profileJson, plan, menuIndex }),
|
|
149
|
+
React.createElement(Text, null, ""),
|
|
150
|
+
React.createElement(Text, { dimColor: true }, footerHint(view))
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function renderView({ view, diagnostics, profileJson, plan, menuIndex }) {
|
|
155
|
+
switch (view) {
|
|
156
|
+
case ORCHESTRATOR_VIEWS.HOME:
|
|
157
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
158
|
+
React.createElement(Text, { bold: true }, "Menu"),
|
|
159
|
+
ORCHESTRATOR_MENU.map((item, index) =>
|
|
160
|
+
React.createElement(Text, {
|
|
161
|
+
key: item.id,
|
|
162
|
+
color: index === menuIndex ? COLORS.accent : undefined,
|
|
163
|
+
bold: index === menuIndex
|
|
164
|
+
}, `${index === menuIndex ? "› " : " "}${item.label}`)
|
|
165
|
+
),
|
|
166
|
+
React.createElement(Text, null, ""),
|
|
167
|
+
React.createElement(Text, { bold: true }, "Snapshot"),
|
|
168
|
+
React.createElement(Text, null, `Agents detected: ${diagnostics.diagnostics.detected}/${diagnostics.capabilities.length}`),
|
|
169
|
+
React.createElement(Text, null, `Available: ${diagnostics.diagnostics.available}`)
|
|
170
|
+
);
|
|
171
|
+
case ORCHESTRATOR_VIEWS.AGENTS:
|
|
172
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
173
|
+
React.createElement(Text, { bold: true }, "Agent capabilities"),
|
|
174
|
+
formatAgentStatusLines(diagnostics.capabilities)
|
|
175
|
+
.map((line) => React.createElement(Text, { key: line }, line))
|
|
176
|
+
);
|
|
177
|
+
case ORCHESTRATOR_VIEWS.PROFILE:
|
|
178
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
179
|
+
React.createElement(Text, { bold: true }, "Profile"),
|
|
180
|
+
formatProfileLines(profileJson)
|
|
181
|
+
.map((line) => React.createElement(Text, { key: line }, line))
|
|
182
|
+
);
|
|
183
|
+
case ORCHESTRATOR_VIEWS.PLAN:
|
|
184
|
+
case ORCHESTRATOR_VIEWS.CONFIRM:
|
|
185
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
186
|
+
React.createElement(Text, { bold: true }, view === ORCHESTRATOR_VIEWS.CONFIRM ? "Confirm plan" : "Plan"),
|
|
187
|
+
plan && formatPlanLines(plan).map((line) => React.createElement(Text, { key: line }, line)),
|
|
188
|
+
view === ORCHESTRATOR_VIEWS.CONFIRM && React.createElement(Text, { color: COLORS.warning }, "Y confirm · N decline")
|
|
189
|
+
);
|
|
190
|
+
case ORCHESTRATOR_VIEWS.HELP:
|
|
191
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
192
|
+
React.createElement(Text, { bold: true }, "Help"),
|
|
193
|
+
React.createElement(Text, null, "Kairo coordinates installed agent CLIs — it is not a coding model."),
|
|
194
|
+
React.createElement(Text, null, "Use explicit commands for scripts: setup, install, status, doctor, sync."),
|
|
195
|
+
React.createElement(Text, null, "Profiles: ~/.harness/profile.json and .harness/kairo.json (project wins)."),
|
|
196
|
+
React.createElement(Text, null, "Credentials are never stored by Kairo.")
|
|
197
|
+
);
|
|
198
|
+
default: {
|
|
199
|
+
const _exhaustive = view;
|
|
200
|
+
return React.createElement(Text, null, `Unknown view: ${_exhaustive}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function footerHint(view) {
|
|
206
|
+
if (view === ORCHESTRATOR_VIEWS.HOME) return "↑↓ navigate · Enter select · Esc quit";
|
|
207
|
+
if (view === ORCHESTRATOR_VIEWS.CONFIRM) return "Y confirm · N decline · Esc back";
|
|
208
|
+
return "Esc back to menu";
|
|
209
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
2
|
+
import { canUseSetupInk } from "./terminal.js";
|
|
3
|
+
|
|
4
|
+
export function canUseOrchestratorShell({
|
|
5
|
+
interactive = Boolean(input.isTTY && output.isTTY),
|
|
6
|
+
term = process.env.TERM ?? "",
|
|
7
|
+
columns = output.columns ?? 80,
|
|
8
|
+
forceInk = process.env.HARNESS_INK !== "0"
|
|
9
|
+
} = {}) {
|
|
10
|
+
return canUseSetupInk({ interactive, term, columns, forceInk });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ORCHESTRATOR_VIEWS = {
|
|
14
|
+
HOME: "home",
|
|
15
|
+
AGENTS: "agents",
|
|
16
|
+
PROFILE: "profile",
|
|
17
|
+
PLAN: "plan",
|
|
18
|
+
CONFIRM: "confirm",
|
|
19
|
+
HELP: "help"
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const ORCHESTRATOR_MENU = [
|
|
23
|
+
{ id: "status", label: "Diagnostics", view: ORCHESTRATOR_VIEWS.HOME },
|
|
24
|
+
{ id: "agents", label: "Agents", view: ORCHESTRATOR_VIEWS.AGENTS },
|
|
25
|
+
{ id: "profile", label: "Profile", view: ORCHESTRATOR_VIEWS.PROFILE },
|
|
26
|
+
{ id: "plan-setup", label: "Plan setup", view: ORCHESTRATOR_VIEWS.PLAN, action: "setup" },
|
|
27
|
+
{ id: "help", label: "Help", view: ORCHESTRATOR_VIEWS.HELP }
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export function formatAgentStatusLines(capabilities) {
|
|
31
|
+
return capabilities.map((entry) => {
|
|
32
|
+
const auth = entry.authenticated == null ? "n/a" : (entry.authenticated ? "yes" : "no");
|
|
33
|
+
const version = entry.version ?? "unknown";
|
|
34
|
+
return `${entry.label.padEnd(14)} ${entry.state.padEnd(14)} v${version} auth=${auth}`;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function formatProfileLines(profileJson) {
|
|
39
|
+
const lines = [
|
|
40
|
+
`Coordinator: ${profileJson.coordinator ?? "none"}`,
|
|
41
|
+
`Default agents: ${formatAgentsLabel(profileJson.defaultAgents)}`,
|
|
42
|
+
`Apply mode: ${profileJson.applyMode}`
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
if (profileJson.sources.global) {
|
|
46
|
+
lines.push(`Global: ${profileJson.sources.global}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (profileJson.sources.project) {
|
|
50
|
+
lines.push(`Project: ${profileJson.sources.project}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
lines.push(`Precedence: ${profileJson.sources.precedence}`);
|
|
54
|
+
return lines;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function formatPlanLines(plan) {
|
|
58
|
+
const lines = [`Action: ${plan.action}`, ""];
|
|
59
|
+
for (const step of plan.steps) {
|
|
60
|
+
lines.push(` • ${step}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (plan.warnings.length > 0) {
|
|
64
|
+
lines.push("", "Warnings:");
|
|
65
|
+
for (const warning of plan.warnings) {
|
|
66
|
+
lines.push(` ! ${warning}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatAgentsLabel(agents) {
|
|
74
|
+
if (agents === "detected") return "detected";
|
|
75
|
+
if (agents === "all") return "all";
|
|
76
|
+
if (Array.isArray(agents)) return agents.join(", ");
|
|
77
|
+
return String(agents);
|
|
78
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "ink";
|
|
3
|
+
import { OrchestratorApp } from "./orchestrator-app.js";
|
|
4
|
+
|
|
5
|
+
export async function runOrchestratorInk({
|
|
6
|
+
homeDir,
|
|
7
|
+
workspaceRoot,
|
|
8
|
+
packageRoot,
|
|
9
|
+
packageName,
|
|
10
|
+
cliVersion,
|
|
11
|
+
renderImpl = render
|
|
12
|
+
}) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const { waitUntilExit } = renderImpl(
|
|
15
|
+
React.createElement(OrchestratorApp, {
|
|
16
|
+
homeDir,
|
|
17
|
+
workspaceRoot,
|
|
18
|
+
packageRoot,
|
|
19
|
+
packageName,
|
|
20
|
+
cliVersion,
|
|
21
|
+
onComplete: resolve
|
|
22
|
+
})
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
waitUntilExit().catch((error) => {
|
|
26
|
+
resolve({ cancelled: true, error });
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
2
|
+
import { resolveHomeDir } from "./paths.js";
|
|
3
|
+
import { runGlobalSetup } from "./global-cli.js";
|
|
4
|
+
import { PLAN_ACTIONS, buildReadOnlyDiagnostics, shouldExecutePlan } from "./action-planner.js";
|
|
5
|
+
import { canUseOrchestratorShell } from "./ink/orchestrator-state.js";
|
|
6
|
+
import { runOrchestratorInk as defaultRunOrchestratorInk } from "./ink/run-orchestrator-ink.js";
|
|
7
|
+
import { formatCliCommand } from "./brand/cli.js";
|
|
8
|
+
import { BRAND } from "./brand/index.js";
|
|
9
|
+
|
|
10
|
+
export { canUseOrchestratorShell };
|
|
11
|
+
|
|
12
|
+
export function shouldOpenOrchestratorShell({
|
|
13
|
+
interactive = Boolean(input.isTTY && output.isTTY),
|
|
14
|
+
json = false,
|
|
15
|
+
hasImplicitFlags = false
|
|
16
|
+
} = {}) {
|
|
17
|
+
if (!interactive || json) return false;
|
|
18
|
+
if (hasImplicitFlags) return false;
|
|
19
|
+
return canUseOrchestratorShell({ interactive });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function runOrchestratorShell({
|
|
23
|
+
packageRoot,
|
|
24
|
+
packageManifest,
|
|
25
|
+
workspaceRoot,
|
|
26
|
+
interactive = Boolean(input.isTTY && output.isTTY),
|
|
27
|
+
runOrchestratorInkImpl = defaultRunOrchestratorInk
|
|
28
|
+
}) {
|
|
29
|
+
if (!interactive) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Non-interactive shell requires an explicit command. Try ${formatCliCommand("help")} or ${formatCliCommand("setup --yes")}.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!canUseOrchestratorShell({ interactive })) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Interactive shell requires a capable TTY. Use ${formatCliCommand("setup --simple")} or explicit commands.`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const homeDir = resolveHomeDir();
|
|
42
|
+
const outcome = await runOrchestratorInkImpl({
|
|
43
|
+
homeDir,
|
|
44
|
+
workspaceRoot,
|
|
45
|
+
packageRoot,
|
|
46
|
+
packageName: packageManifest.name,
|
|
47
|
+
cliVersion: packageManifest.version
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (outcome.error) {
|
|
51
|
+
throw outcome.error;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (outcome.cancelled) {
|
|
55
|
+
return { cancelled: true, wrote: false };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (outcome.confirmed && outcome.action === PLAN_ACTIONS.SETUP) {
|
|
59
|
+
const setupOutcome = await runGlobalSetup(
|
|
60
|
+
{
|
|
61
|
+
cwd: workspaceRoot,
|
|
62
|
+
adapters: null,
|
|
63
|
+
components: null,
|
|
64
|
+
noDefaultComponents: false,
|
|
65
|
+
dryRun: false,
|
|
66
|
+
yes: false,
|
|
67
|
+
confirm: false,
|
|
68
|
+
preflight: true,
|
|
69
|
+
preflightExplicit: false,
|
|
70
|
+
yesExplicit: false,
|
|
71
|
+
confirmExplicit: false,
|
|
72
|
+
json: false,
|
|
73
|
+
interactive: true,
|
|
74
|
+
simple: false
|
|
75
|
+
},
|
|
76
|
+
packageManifest,
|
|
77
|
+
packageRoot
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
cancelled: Boolean(setupOutcome.cancelled),
|
|
82
|
+
wrote: !setupOutcome.cancelled && !setupOutcome.result?.dryRun,
|
|
83
|
+
action: PLAN_ACTIONS.SETUP,
|
|
84
|
+
setupOutcome
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { cancelled: false, wrote: false, action: outcome.action ?? null };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function runOrchestratorDiagnostics({
|
|
92
|
+
homeDir,
|
|
93
|
+
workspaceRoot,
|
|
94
|
+
packageName,
|
|
95
|
+
packageRoot,
|
|
96
|
+
cliVersion,
|
|
97
|
+
json = false
|
|
98
|
+
}) {
|
|
99
|
+
const diagnostics = await buildReadOnlyDiagnostics({
|
|
100
|
+
homeDir,
|
|
101
|
+
workspaceRoot,
|
|
102
|
+
packageName,
|
|
103
|
+
packageRoot,
|
|
104
|
+
cliVersion
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (json) {
|
|
108
|
+
return diagnostics;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(commandHeader("orchestrator — agent capability diagnostics"));
|
|
112
|
+
console.log(`Home: ${homeDir}`);
|
|
113
|
+
console.log(`Workspace: ${workspaceRoot}`);
|
|
114
|
+
console.log("");
|
|
115
|
+
|
|
116
|
+
for (const capability of diagnostics.capabilities) {
|
|
117
|
+
console.log(
|
|
118
|
+
` ${capability.label.padEnd(14)} ${capability.state.padEnd(14)} detected=${capability.detected ? "yes" : "no"}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log("");
|
|
123
|
+
console.log("Recommendations:");
|
|
124
|
+
for (const recommendation of diagnostics.recommendations) {
|
|
125
|
+
console.log(` - ${recommendation}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return diagnostics;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function commandHeader(title) {
|
|
132
|
+
return `${BRAND.displayName} ${title}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function assertPlanExecution(plan, { confirmed = false } = {}) {
|
|
136
|
+
if (!shouldExecutePlan(plan, { confirmed })) {
|
|
137
|
+
throw new Error("Plan declined. No writes or installations were performed.");
|
|
138
|
+
}
|
|
139
|
+
}
|
package/src/global/paths.js
CHANGED
|
@@ -15,6 +15,7 @@ export function harnessHomePaths(homeDir) {
|
|
|
15
15
|
root,
|
|
16
16
|
statePath: join(root, "state.json"),
|
|
17
17
|
policyPath: join(root, "policy.json"),
|
|
18
|
+
profilePath: join(root, "profile.json"),
|
|
18
19
|
historyPath: join(root, "history.jsonl"),
|
|
19
20
|
coreDir: join(root, "core"),
|
|
20
21
|
backupsDir: join(root, "backups")
|