@kal-elsam/kairo-runtime 0.13.0 → 0.14.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/CHANGELOG.md +1202 -0
- package/LICENSE +21 -0
- package/README.md +64 -1192
- package/package.json +9 -3
- package/scripts/cockpit-smoke.mjs +14 -10
- package/src/cli.js +25 -142
- package/src/global/cli-help.js +182 -0
- package/src/global/ink/cockpit-controller.js +1 -3
- package/src/global/ink/cockpit-enter.js +3 -1
- package/src/global/ink/cockpit-focus.js +7 -1
- package/src/global/ink/cockpit-models.js +30 -21
- package/src/global/ink/cockpit-palette.js +8 -3
- package/src/global/ink/cockpit-views.js +9 -2
- package/src/global/ink/orchestrator-app.js +21 -2
- package/src/global/ink/ux/live-overview.js +120 -72
- package/src/global/ink/ux/overview-actions.js +80 -0
- package/src/global/ink/ux/overview-needs.js +160 -0
- package/src/global/self-update.js +216 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain-language Overview needs — no machine noise on the first screen.
|
|
3
|
+
*/
|
|
4
|
+
import { CONTROL_PLANE_HEALTH } from "../../control-plane-snapshot.js";
|
|
5
|
+
import { formatCliCommand } from "../../brand/cli.js";
|
|
6
|
+
|
|
7
|
+
export const OVERVIEW_NEED_LIMIT = 3;
|
|
8
|
+
|
|
9
|
+
/** Prefixes that never belong on the first screen (machine / internals). */
|
|
10
|
+
export const DETAILS_ONLY_PREFIXES = [
|
|
11
|
+
"System",
|
|
12
|
+
"Advisor",
|
|
13
|
+
"Soft links",
|
|
14
|
+
"Gentle",
|
|
15
|
+
"Graphify",
|
|
16
|
+
"Hermes"
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const DESTINATION_LABELS = {
|
|
20
|
+
setup: "Setup",
|
|
21
|
+
changes: "Governance",
|
|
22
|
+
ides: "Agents",
|
|
23
|
+
runs: "Orchestration",
|
|
24
|
+
"control-center": "Overview",
|
|
25
|
+
activity: "Activity",
|
|
26
|
+
usage: "Usage",
|
|
27
|
+
profile: "Settings"
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function mapHealthTone(kind) {
|
|
31
|
+
switch (kind) {
|
|
32
|
+
case CONTROL_PLANE_HEALTH.CHECK_FAILED:
|
|
33
|
+
return "danger";
|
|
34
|
+
case CONTROL_PLANE_HEALTH.ACTION_REQUIRED:
|
|
35
|
+
case CONTROL_PLANE_HEALTH.NOT_CONFIGURED:
|
|
36
|
+
case CONTROL_PLANE_HEALTH.HEALTHY_WITH_NOTES:
|
|
37
|
+
return "warn";
|
|
38
|
+
case CONTROL_PLANE_HEALTH.HEALTHY:
|
|
39
|
+
return "ready";
|
|
40
|
+
default:
|
|
41
|
+
return "warn";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Plain-language status title — never shouty ALL-CAPS jargon alone. */
|
|
46
|
+
export function humanizeHealthTitle(kind, fallback = "Unknown") {
|
|
47
|
+
switch (kind) {
|
|
48
|
+
case CONTROL_PLANE_HEALTH.NOT_CONFIGURED:
|
|
49
|
+
return "Needs setup";
|
|
50
|
+
case CONTROL_PLANE_HEALTH.ACTION_REQUIRED:
|
|
51
|
+
return "Needs attention";
|
|
52
|
+
case CONTROL_PLANE_HEALTH.CHECK_FAILED:
|
|
53
|
+
return "Something failed";
|
|
54
|
+
case CONTROL_PLANE_HEALTH.HEALTHY_WITH_NOTES:
|
|
55
|
+
return "Ready · with notes";
|
|
56
|
+
case CONTROL_PLANE_HEALTH.HEALTHY:
|
|
57
|
+
return "Ready";
|
|
58
|
+
default:
|
|
59
|
+
return typeof fallback === "string" && fallback ? fallback : "Unknown";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function humanizeDestination(destination) {
|
|
64
|
+
if (!destination) return null;
|
|
65
|
+
return DESTINATION_LABELS[destination] ?? null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Turn a raw companion line into a user-facing need, or null to skip.
|
|
70
|
+
* System/machine lines are never returned here.
|
|
71
|
+
*/
|
|
72
|
+
export function humanizeCompanionNeed(line) {
|
|
73
|
+
if (typeof line !== "string" || line.trim().length === 0) return null;
|
|
74
|
+
if (/^\s/.test(line)) return null;
|
|
75
|
+
|
|
76
|
+
for (const prefix of DETAILS_ONLY_PREFIXES) {
|
|
77
|
+
if (line.startsWith(prefix)) return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (line.startsWith("Obsidian · unconfigured")) {
|
|
81
|
+
return "Obsidian not connected · open Settings to choose your vault";
|
|
82
|
+
}
|
|
83
|
+
if (line.startsWith("Obsidian · missing")) {
|
|
84
|
+
return "Obsidian vault folder missing · check Settings";
|
|
85
|
+
}
|
|
86
|
+
if (line.startsWith("Obsidian · unavailable") || line.startsWith("Obsidian · error")) {
|
|
87
|
+
return "Obsidian unavailable · check Settings";
|
|
88
|
+
}
|
|
89
|
+
if (/^Updates · \d+\s+available/i.test(line)) {
|
|
90
|
+
return `Update available · run ${formatCliCommand("updates check")}`;
|
|
91
|
+
}
|
|
92
|
+
if (line.startsWith("Engram · conflict")) {
|
|
93
|
+
return "Memory conflict · open Settings → Engram";
|
|
94
|
+
}
|
|
95
|
+
if (line.startsWith("Engram · missing") || line.startsWith("Engram · error")) {
|
|
96
|
+
return "Memory not ready · open Settings → Engram";
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Partition companion lines: plain needs for Overview, raw rest for Details. */
|
|
102
|
+
export function partitionCompanionLines(lines = []) {
|
|
103
|
+
const needs = [];
|
|
104
|
+
const rest = [];
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
if (typeof line !== "string" || line.trim().length === 0) continue;
|
|
107
|
+
const need = humanizeCompanionNeed(line);
|
|
108
|
+
if (need && needs.length < OVERVIEW_NEED_LIMIT) {
|
|
109
|
+
needs.push(need);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
rest.push(line);
|
|
113
|
+
}
|
|
114
|
+
return { needs, rest };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function humanizePrimary(next = {}) {
|
|
118
|
+
const kind = next.kind;
|
|
119
|
+
const detail = typeof next.actionDetail === "string" && next.actionDetail.trim()
|
|
120
|
+
? next.actionDetail.trim()
|
|
121
|
+
: null;
|
|
122
|
+
switch (kind) {
|
|
123
|
+
case "setup":
|
|
124
|
+
return {
|
|
125
|
+
label: "Start setup",
|
|
126
|
+
detail: detail ?? `Run ${formatCliCommand("setup")} to configure agents for this project.`,
|
|
127
|
+
hint: "Enter → open Setup"
|
|
128
|
+
};
|
|
129
|
+
case "repair":
|
|
130
|
+
return {
|
|
131
|
+
label: "Fix drift",
|
|
132
|
+
detail: detail ?? `Run ${formatCliCommand("sync")} to repair managed content.`,
|
|
133
|
+
hint: "Enter → preview in Governance"
|
|
134
|
+
};
|
|
135
|
+
case "verify":
|
|
136
|
+
return {
|
|
137
|
+
label: "Check what failed",
|
|
138
|
+
detail: detail ?? `Run ${formatCliCommand("doctor")} for details.`,
|
|
139
|
+
hint: "Enter → reload"
|
|
140
|
+
};
|
|
141
|
+
case "review":
|
|
142
|
+
return {
|
|
143
|
+
label: "Review notes",
|
|
144
|
+
detail: detail ?? "Open Governance to inspect remaining notes.",
|
|
145
|
+
hint: "Enter → open"
|
|
146
|
+
};
|
|
147
|
+
case "idle":
|
|
148
|
+
return {
|
|
149
|
+
label: "All clear",
|
|
150
|
+
detail: detail ?? "No action needed right now.",
|
|
151
|
+
hint: null
|
|
152
|
+
};
|
|
153
|
+
default:
|
|
154
|
+
return {
|
|
155
|
+
label: next.actionTitle ?? "Review control plane",
|
|
156
|
+
detail,
|
|
157
|
+
hint: next.enterHint ?? "Enter →"
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { fetchPublishedVersion } from "./npm-registry.js";
|
|
3
|
+
import { formatCliCommand } from "./brand/cli.js";
|
|
4
|
+
import { commandHeader } from "./brand/index.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} version
|
|
8
|
+
* @returns {number[]}
|
|
9
|
+
*/
|
|
10
|
+
export function parseSemver(version) {
|
|
11
|
+
const match = String(version).trim().match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
12
|
+
if (!match) return null;
|
|
13
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} a
|
|
18
|
+
* @param {string} b
|
|
19
|
+
* @returns {-1|0|1|null}
|
|
20
|
+
*/
|
|
21
|
+
export function compareSemver(a, b) {
|
|
22
|
+
const left = parseSemver(a);
|
|
23
|
+
const right = parseSemver(b);
|
|
24
|
+
if (!left || !right) return null;
|
|
25
|
+
for (let i = 0; i < 3; i += 1) {
|
|
26
|
+
if (left[i] < right[i]) return -1;
|
|
27
|
+
if (left[i] > right[i]) return 1;
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
34
|
+
* @returns {"npm"|"pnpm"|"yarn"|"bun"}
|
|
35
|
+
*/
|
|
36
|
+
export function detectInstallPackageManager(env = process.env) {
|
|
37
|
+
const execPath = env.npm_execpath ?? "";
|
|
38
|
+
const userAgent = env.npm_config_user_agent ?? "";
|
|
39
|
+
if (execPath.includes("pnpm") || userAgent.startsWith("pnpm/")) return "pnpm";
|
|
40
|
+
if (execPath.includes("yarn") || userAgent.startsWith("yarn/")) return "yarn";
|
|
41
|
+
if (execPath.includes("bun") || userAgent.startsWith("bun/")) return "bun";
|
|
42
|
+
return "npm";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {string} packageName
|
|
47
|
+
* @param {string} version
|
|
48
|
+
* @param {"npm"|"pnpm"|"yarn"|"bun"} manager
|
|
49
|
+
* @returns {{ command: string, args: string[], display: string }}
|
|
50
|
+
*/
|
|
51
|
+
export function buildGlobalInstallSpec(packageName, version, manager = "npm") {
|
|
52
|
+
const spec = `${packageName}@${version}`;
|
|
53
|
+
switch (manager) {
|
|
54
|
+
case "pnpm":
|
|
55
|
+
return {
|
|
56
|
+
command: "pnpm",
|
|
57
|
+
args: ["add", "-g", spec],
|
|
58
|
+
display: `pnpm add -g ${spec}`
|
|
59
|
+
};
|
|
60
|
+
case "yarn":
|
|
61
|
+
return {
|
|
62
|
+
command: "yarn",
|
|
63
|
+
args: ["global", "add", spec],
|
|
64
|
+
display: `yarn global add ${spec}`
|
|
65
|
+
};
|
|
66
|
+
case "bun":
|
|
67
|
+
return {
|
|
68
|
+
command: "bun",
|
|
69
|
+
args: ["add", "-g", spec],
|
|
70
|
+
display: `bun add -g ${spec}`
|
|
71
|
+
};
|
|
72
|
+
default:
|
|
73
|
+
return {
|
|
74
|
+
command: "npm",
|
|
75
|
+
args: ["install", "-g", spec],
|
|
76
|
+
display: `npm install -g ${spec}`
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {object} options
|
|
83
|
+
* @param {string} options.packageName
|
|
84
|
+
* @param {string} options.cliVersion
|
|
85
|
+
* @param {boolean} [options.yes]
|
|
86
|
+
* @param {boolean} [options.json]
|
|
87
|
+
* @param {typeof fetchPublishedVersion} [options.fetchVersion]
|
|
88
|
+
* @param {typeof detectInstallPackageManager} [options.detectManager]
|
|
89
|
+
* @param {(cmd: string, args: string[]) => Promise<{ status: number, stdout: string, stderr: string }>} [options.runCommand]
|
|
90
|
+
*/
|
|
91
|
+
export async function runSelfUpdate({
|
|
92
|
+
packageName,
|
|
93
|
+
cliVersion,
|
|
94
|
+
yes = false,
|
|
95
|
+
json = false,
|
|
96
|
+
fetchVersion = fetchPublishedVersion,
|
|
97
|
+
detectManager = detectInstallPackageManager,
|
|
98
|
+
runCommand = defaultRunCommand
|
|
99
|
+
} = {}) {
|
|
100
|
+
if (!packageName) throw new Error("packageName is required.");
|
|
101
|
+
if (!cliVersion) throw new Error("cliVersion is required.");
|
|
102
|
+
|
|
103
|
+
const latestVersion = await fetchVersion(packageName);
|
|
104
|
+
const cmp = compareSemver(cliVersion, latestVersion);
|
|
105
|
+
const manager = detectManager();
|
|
106
|
+
const install = buildGlobalInstallSpec(packageName, latestVersion, manager);
|
|
107
|
+
|
|
108
|
+
/** @type {"current"|"behind"|"ahead"|"unknown"} */
|
|
109
|
+
let state = "unknown";
|
|
110
|
+
if (cmp === 0) state = "current";
|
|
111
|
+
else if (cmp === -1) state = "behind";
|
|
112
|
+
else if (cmp === 1) state = "ahead";
|
|
113
|
+
|
|
114
|
+
const report = {
|
|
115
|
+
ok: true,
|
|
116
|
+
state,
|
|
117
|
+
installedVersion: cliVersion,
|
|
118
|
+
latestVersion,
|
|
119
|
+
packageName,
|
|
120
|
+
manager,
|
|
121
|
+
installCommand: install.display,
|
|
122
|
+
applied: false,
|
|
123
|
+
wrote: false
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
if (state === "current") {
|
|
127
|
+
report.nextAction = "Already up to date.";
|
|
128
|
+
} else if (state === "ahead") {
|
|
129
|
+
report.nextAction = `Local ${cliVersion} is newer than npm ${latestVersion}.`;
|
|
130
|
+
} else if (state === "behind") {
|
|
131
|
+
report.nextAction = yes
|
|
132
|
+
? `Updating ${cliVersion} → ${latestVersion}…`
|
|
133
|
+
: `Update available: ${cliVersion} → ${latestVersion}. Run "${formatCliCommand("update --yes")}" or: ${install.display}`;
|
|
134
|
+
} else {
|
|
135
|
+
report.nextAction = `Could not compare versions. Install manually: ${install.display}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (json) {
|
|
139
|
+
if (yes && state === "behind") {
|
|
140
|
+
const result = await runCommand(install.command, install.args);
|
|
141
|
+
report.applied = result.status === 0;
|
|
142
|
+
report.wrote = result.status === 0;
|
|
143
|
+
report.ok = result.status === 0;
|
|
144
|
+
report.installStatus = result.status;
|
|
145
|
+
report.stderr = result.stderr.trim() || undefined;
|
|
146
|
+
report.nextAction = result.status === 0
|
|
147
|
+
? `Updated to ${latestVersion}.`
|
|
148
|
+
: `Install failed (exit ${result.status}). Try: ${install.display}`;
|
|
149
|
+
}
|
|
150
|
+
console.log(JSON.stringify(report));
|
|
151
|
+
return report;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log(commandHeader("update — Kairo Runtime"));
|
|
155
|
+
console.log(`Installed: ${cliVersion}`);
|
|
156
|
+
console.log(`Latest: ${latestVersion}`);
|
|
157
|
+
console.log(`Status: ${state}`);
|
|
158
|
+
|
|
159
|
+
if (state === "current") {
|
|
160
|
+
console.log("\nAlready up to date.");
|
|
161
|
+
return report;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (state === "ahead") {
|
|
165
|
+
console.log(`\n${report.nextAction}`);
|
|
166
|
+
return report;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!yes) {
|
|
170
|
+
console.log(`\nUpdate available: ${cliVersion} → ${latestVersion}`);
|
|
171
|
+
console.log(`Run: ${formatCliCommand("update --yes")}`);
|
|
172
|
+
console.log(`Or: ${install.display}`);
|
|
173
|
+
return report;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(`\nInstalling ${install.display}…`);
|
|
177
|
+
const result = await runCommand(install.command, install.args);
|
|
178
|
+
report.applied = result.status === 0;
|
|
179
|
+
report.wrote = result.status === 0;
|
|
180
|
+
report.ok = result.status === 0;
|
|
181
|
+
report.installStatus = result.status;
|
|
182
|
+
if (result.stdout.trim()) console.log(result.stdout.trim());
|
|
183
|
+
if (result.stderr.trim()) console.error(result.stderr.trim());
|
|
184
|
+
if (result.status !== 0) {
|
|
185
|
+
throw new Error(`Self-update failed (exit ${result.status}). Try: ${install.display}`);
|
|
186
|
+
}
|
|
187
|
+
console.log(`\nUpdated to ${latestVersion}. Run "${formatCliCommand("--version")}" to confirm.`);
|
|
188
|
+
report.nextAction = `Updated to ${latestVersion}.`;
|
|
189
|
+
return report;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function defaultRunCommand(command, args) {
|
|
193
|
+
return new Promise((resolve) => {
|
|
194
|
+
const child = spawn(command, args, {
|
|
195
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
196
|
+
shell: false,
|
|
197
|
+
env: process.env
|
|
198
|
+
});
|
|
199
|
+
let stdout = "";
|
|
200
|
+
let stderr = "";
|
|
201
|
+
child.stdout?.setEncoding("utf8");
|
|
202
|
+
child.stderr?.setEncoding("utf8");
|
|
203
|
+
child.stdout?.on("data", (chunk) => {
|
|
204
|
+
stdout += chunk;
|
|
205
|
+
});
|
|
206
|
+
child.stderr?.on("data", (chunk) => {
|
|
207
|
+
stderr += chunk;
|
|
208
|
+
});
|
|
209
|
+
child.on("error", (error) => {
|
|
210
|
+
resolve({ status: 1, stdout, stderr: error.message });
|
|
211
|
+
});
|
|
212
|
+
child.on("close", (code) => {
|
|
213
|
+
resolve({ status: code ?? 1, stdout, stderr });
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|