@kal-elsam/kairo-runtime 0.13.1 → 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 +23 -0
- package/README.md +12 -10
- package/package.json +5 -1
- package/scripts/cockpit-smoke.mjs +14 -10
- package/src/cli.js +10 -3
- package/src/global/cli-help.js +4 -2
- 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 +52 -19
- package/src/global/ink/ux/overview-actions.js +80 -0
- package/src/global/self-update.js +216 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ Historical entries below may reference the legacy `@kal-elsam/harness` package n
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.14.0 — 2026-08-07 (Kairo Runtime)
|
|
9
|
+
|
|
10
|
+
Minor release. Simpler Cockpit, one-command CLI self-update, pnpm for
|
|
11
|
+
contributors, and a minimal IDE status panel. Publish tag:
|
|
12
|
+
`kairo-runtime-v0.14.0`.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Cockpit Home with three tabs (Home · Settings · History) and two clear
|
|
17
|
+
actions (prepare/repair + Configure). Secondary destinations stay in `/`.
|
|
18
|
+
- `kairo update`: update the Kairo CLI from npm (`--yes` applies). Workspace
|
|
19
|
+
template refresh stays at `kairo update --scope=workspace`.
|
|
20
|
+
- VS Code / Cursor extension scaffold in `packages/kairo-vscode`: status bar +
|
|
21
|
+
explorer tree from `kairo status --json`; sync/doctor/cockpit open a terminal
|
|
22
|
+
(no silent writes). Local VSIX via `npm run package`.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Local development and CI use **pnpm 10.x** (`packageManager` pinned). End-user
|
|
27
|
+
installs and `npm publish` (OIDC Trusted Publishing) stay on npm.
|
|
28
|
+
- Install scripts blocked by default (`pnpm.onlyBuiltDependencies: []`);
|
|
29
|
+
`.npmrc` sets `strict-dep-builds` and `minimum-release-age=1440`.
|
|
30
|
+
|
|
8
31
|
## 0.13.1 — 2026-08-07 (Kairo Runtime)
|
|
9
32
|
|
|
10
33
|
Patch release. Docs/UX/license polish for npm adoption. Publish tag:
|
package/README.md
CHANGED
|
@@ -24,25 +24,23 @@ KAIRO
|
|
|
24
24
|
Needs attention · 4/4 agents · drift pending
|
|
25
25
|
Kairo coordinates installed AI agents for this project.
|
|
26
26
|
|
|
27
|
-
→
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Obsidian not connected · open Settings to choose your vault
|
|
33
|
-
Update available · run kairo updates check
|
|
34
|
-
9 more in Details · Space
|
|
27
|
+
→ Repair 3 changes
|
|
28
|
+
You will see the exact plan before anything is written.
|
|
29
|
+
|
|
30
|
+
Configure
|
|
31
|
+
Agents, Obsidian vault, integrations.
|
|
35
32
|
```
|
|
36
33
|
|
|
37
|
-
`↑↓` move · `Enter` open · `Space` details · `R` refresh · `Esc` back · `?` help
|
|
34
|
+
`↑↓` move · `Enter` open · `Tab` nav · `Space` details · `R` refresh · `Esc` back · `?` help
|
|
38
35
|
|
|
39
|
-
##
|
|
36
|
+
## Day-to-day commands
|
|
40
37
|
|
|
41
38
|
| Command | What it does |
|
|
42
39
|
|---|---|
|
|
43
40
|
| `kairo` | Open the cockpit |
|
|
44
41
|
| `kairo status` | See how your setup is doing |
|
|
45
42
|
| `kairo sync` | Repair what drifted |
|
|
43
|
+
| `kairo update` | Update Kairo itself from npm |
|
|
46
44
|
| `kairo doctor` | Deeper health checks |
|
|
47
45
|
|
|
48
46
|
Run `kairo help --all` for everything else, or read the
|
|
@@ -70,6 +68,10 @@ npx @kal-elsam/kairo-runtime
|
|
|
70
68
|
# Install globally
|
|
71
69
|
npm i -g @kal-elsam/kairo-runtime && kairo
|
|
72
70
|
|
|
71
|
+
# Later: update the CLI itself
|
|
72
|
+
kairo update
|
|
73
|
+
kairo update --yes
|
|
74
|
+
|
|
73
75
|
# Preview the setup plan, write nothing
|
|
74
76
|
npx @kal-elsam/kairo-runtime --dry-run
|
|
75
77
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kal-elsam/kairo-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude, Pi, Engram, and Graphify.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/Kal-elSam/harness#readme",
|
|
@@ -62,6 +62,10 @@
|
|
|
62
62
|
"engines": {
|
|
63
63
|
"node": ">=20.12.0"
|
|
64
64
|
},
|
|
65
|
+
"packageManager": "pnpm@10.34.5",
|
|
66
|
+
"pnpm": {
|
|
67
|
+
"onlyBuiltDependencies": []
|
|
68
|
+
},
|
|
65
69
|
"dependencies": {
|
|
66
70
|
"@clack/prompts": "^1.7.0",
|
|
67
71
|
"@modelcontextprotocol/server": "2.0.0",
|
|
@@ -86,7 +86,7 @@ const asciiNav = buildNavModel({
|
|
|
86
86
|
assert.equal(asciiNav.items[1].marker, ">");
|
|
87
87
|
assert.equal(asciiNav.items[0].current, true);
|
|
88
88
|
assert.equal(asciiNav.items[1].selected, true);
|
|
89
|
-
assert.match(asciiNav.explanation, /
|
|
89
|
+
assert.match(asciiNav.explanation, /agents|Obsidian|integrations/i);
|
|
90
90
|
|
|
91
91
|
function applyKey(state, keyAction) {
|
|
92
92
|
const routed = routeCockpitKey(state, keyAction);
|
|
@@ -99,25 +99,29 @@ function smokeNavigation(layoutMode) {
|
|
|
99
99
|
layoutMode,
|
|
100
100
|
region: layoutMode === LAYOUT_MODES.MINIMAL ? COCKPIT_REGIONS.CONTENT : COCKPIT_REGIONS.NAV
|
|
101
101
|
});
|
|
102
|
-
const
|
|
103
|
-
while (state.navIndex <
|
|
102
|
+
const settingsIndex = COCKPIT_NAV.findIndex((item) => item.id === "settings");
|
|
103
|
+
while (state.navIndex < settingsIndex) {
|
|
104
104
|
state = applyKey(state, { type: "arrow", direction: "down" });
|
|
105
105
|
}
|
|
106
106
|
state = applyKey(state, { type: "enter" });
|
|
107
|
-
assert.equal(state.view, ORCHESTRATOR_VIEWS.
|
|
108
|
-
assert.equal(state.region, COCKPIT_REGIONS.
|
|
107
|
+
assert.equal(state.view, ORCHESTRATOR_VIEWS.PROFILE);
|
|
108
|
+
assert.equal(state.region, COCKPIT_REGIONS.CONTENT);
|
|
109
109
|
|
|
110
|
-
state = applyKey(state, { type: "
|
|
110
|
+
state = applyKey(state, { type: "tab" });
|
|
111
|
+
assert.equal(state.region, COCKPIT_REGIONS.NAV);
|
|
112
|
+
while (state.navIndex > 0) {
|
|
113
|
+
state = applyKey(state, { type: "arrow", direction: "up" });
|
|
114
|
+
}
|
|
111
115
|
state = applyKey(state, { type: "enter" });
|
|
112
116
|
assert.equal(state.view, ORCHESTRATOR_VIEWS.HOME);
|
|
113
|
-
assert.equal(state.region, COCKPIT_REGIONS.
|
|
117
|
+
assert.equal(state.region, COCKPIT_REGIONS.CONTENT);
|
|
114
118
|
|
|
115
119
|
const footer = buildFooterModel({
|
|
116
|
-
view: ORCHESTRATOR_VIEWS.
|
|
117
|
-
region: COCKPIT_REGIONS.
|
|
120
|
+
view: ORCHESTRATOR_VIEWS.HOME,
|
|
121
|
+
region: COCKPIT_REGIONS.CONTENT,
|
|
118
122
|
unicode: false
|
|
119
123
|
});
|
|
120
|
-
assert.
|
|
124
|
+
assert.match(footer.text, /Tab/);
|
|
121
125
|
|
|
122
126
|
state = applyKey(state, { type: "escape" });
|
|
123
127
|
assert.equal(state.view, ORCHESTRATOR_VIEWS.HOME);
|
package/src/cli.js
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
runGlobalReport
|
|
31
31
|
} from "./global/global-cli.js";
|
|
32
32
|
import { runEcosystemUpdatesCheck } from "./global/updates-cli.js";
|
|
33
|
+
import { runSelfUpdate } from "./global/self-update.js";
|
|
33
34
|
import { applyPolicyToOptions, loadPolicyFile } from "./global/policy.js";
|
|
34
35
|
import { resolveHomeDir } from "./global/paths.js";
|
|
35
36
|
import { runWorkspaceDetect, runWorkspaceDoctor, runWorkspaceInit, runWorkspaceUpdate } from "./workspace-cli.js";
|
|
@@ -173,9 +174,15 @@ export async function runCli(argv) {
|
|
|
173
174
|
});
|
|
174
175
|
return;
|
|
175
176
|
case "update":
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
if (options.scope === "workspace") {
|
|
178
|
+
await runWorkspaceUpdate(options, packageManifest, packageRoot);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
await runSelfUpdate({
|
|
182
|
+
packageName: packageManifest.name,
|
|
183
|
+
cliVersion: packageManifest.version,
|
|
184
|
+
yes: optionsWithPolicy.yes === true,
|
|
185
|
+
json: optionsWithPolicy.json === true
|
|
179
186
|
});
|
|
180
187
|
return;
|
|
181
188
|
case "doctor":
|
package/src/global/cli-help.js
CHANGED
|
@@ -29,6 +29,7 @@ Usage:
|
|
|
29
29
|
${cli} Open the cockpit (setup on first run)
|
|
30
30
|
${cli} status See how your setup is doing
|
|
31
31
|
${cli} sync Repair what drifted
|
|
32
|
+
${cli} update Update Kairo itself (npm)
|
|
32
33
|
${cli} doctor Deeper health checks
|
|
33
34
|
|
|
34
35
|
${cli} help --all Every command and flag
|
|
@@ -63,7 +64,8 @@ Bootstrap: see README.md and docs/install.md (curl install.sh or npx ${PACKAGE_N
|
|
|
63
64
|
${cli} updates check [--json] [--force]
|
|
64
65
|
${cli} install [--agents <list|all>] [--components <list>] [--dry-run]
|
|
65
66
|
${cli} install --no-default-components
|
|
66
|
-
${cli} update [--
|
|
67
|
+
${cli} update [--yes] [--json] Update the Kairo CLI from npm
|
|
68
|
+
${cli} update --scope=workspace [--dry-run] Refresh workspace template files
|
|
67
69
|
${cli} uninstall [--dry-run]
|
|
68
70
|
|
|
69
71
|
## Agents & runs
|
|
@@ -143,7 +145,7 @@ Commands:
|
|
|
143
145
|
updates Read-only ecosystem update check (kairo/hermes/gentle/skills). Never applies.
|
|
144
146
|
install Non-interactive configure (agent-global) or legacy workspace scaffold.
|
|
145
147
|
doctor Detailed health checks for managed state and configs.
|
|
146
|
-
update
|
|
148
|
+
update Update the Kairo CLI from npm (--yes applies). Workspace templates: --scope=workspace.
|
|
147
149
|
detect Inspect global agents and the current project. Read-only.
|
|
148
150
|
adapters Official adapter matrix: roots, config files, detected/managed.
|
|
149
151
|
explain Read-only audit of managed adapters, configs, markers, and backups.
|
|
@@ -122,9 +122,7 @@ export function reduceCockpitUi(state, action) {
|
|
|
122
122
|
if (state.region === COCKPIT_REGIONS.SYSTEM) return state;
|
|
123
123
|
|
|
124
124
|
const navigatesNav = state.region === COCKPIT_REGIONS.NAV
|
|
125
|
-
|| isNavFocusedView(state.view)
|
|
126
|
-
|| (state.view === ORCHESTRATOR_VIEWS.HOME
|
|
127
|
-
&& state.layoutMode === LAYOUT_MODES.MINIMAL);
|
|
125
|
+
|| isNavFocusedView(state.view);
|
|
128
126
|
|
|
129
127
|
if (navigatesNav) {
|
|
130
128
|
const delta = action.direction === "up" ? -1 : 1;
|
|
@@ -11,7 +11,9 @@ export function resolveEnterNavIntent({
|
|
|
11
11
|
} = {}) {
|
|
12
12
|
if (!navItem) return { kind: "noop" };
|
|
13
13
|
|
|
14
|
-
const isOverview = navItem.id === "
|
|
14
|
+
const isOverview = navItem.id === "home"
|
|
15
|
+
|| navItem.id === "overview"
|
|
16
|
+
|| navItem.view === ORCHESTRATOR_VIEWS.HOME;
|
|
15
17
|
if (isOverview) {
|
|
16
18
|
if (currentView === ORCHESTRATOR_VIEWS.HOME && ctaDestination) {
|
|
17
19
|
if (ctaDestination === "setup") return { kind: "activate-setup" };
|
|
@@ -3,7 +3,6 @@ import { ORCHESTRATOR_VIEWS } from "./orchestrator-state.js";
|
|
|
3
3
|
import { LAYOUT_MODES } from "./layout.js";
|
|
4
4
|
|
|
5
5
|
const NAV_FOCUSED_VIEWS = new Set([
|
|
6
|
-
ORCHESTRATOR_VIEWS.HOME,
|
|
7
6
|
ORCHESTRATOR_VIEWS.IDES,
|
|
8
7
|
ORCHESTRATOR_VIEWS.MODULES,
|
|
9
8
|
ORCHESTRATOR_VIEWS.CHANGES,
|
|
@@ -14,6 +13,7 @@ const NAV_FOCUSED_VIEWS = new Set([
|
|
|
14
13
|
]);
|
|
15
14
|
|
|
16
15
|
const CONTENT_INTERACTIVE_VIEWS = new Set([
|
|
16
|
+
ORCHESTRATOR_VIEWS.HOME,
|
|
17
17
|
ORCHESTRATOR_VIEWS.RUNS,
|
|
18
18
|
ORCHESTRATOR_VIEWS.ACTIVE_RUNS,
|
|
19
19
|
ORCHESTRATOR_VIEWS.RECENT_RUNS,
|
|
@@ -102,6 +102,12 @@ export function routeCockpitKey(state, keyAction) {
|
|
|
102
102
|
if (state.region === COCKPIT_REGIONS.NAV || isNavFocusedView(state.view)) {
|
|
103
103
|
return { type: "enter-nav" };
|
|
104
104
|
}
|
|
105
|
+
if (
|
|
106
|
+
state.region === COCKPIT_REGIONS.CONTENT
|
|
107
|
+
&& state.view === ORCHESTRATOR_VIEWS.HOME
|
|
108
|
+
) {
|
|
109
|
+
return { type: "enter-home-button" };
|
|
110
|
+
}
|
|
105
111
|
return null;
|
|
106
112
|
default:
|
|
107
113
|
return null;
|
|
@@ -17,23 +17,33 @@ export const COCKPIT_REGIONS = {
|
|
|
17
17
|
|
|
18
18
|
export const COCKPIT_NAV = [
|
|
19
19
|
{
|
|
20
|
-
id: "
|
|
21
|
-
label: "
|
|
20
|
+
id: "home",
|
|
21
|
+
label: "Home",
|
|
22
22
|
view: ORCHESTRATOR_VIEWS.HOME,
|
|
23
|
-
description: "
|
|
23
|
+
description: "What needs you, and what to do about it."
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
id: "settings",
|
|
27
|
+
label: "Settings",
|
|
28
|
+
view: ORCHESTRATOR_VIEWS.PROFILE,
|
|
29
|
+
description: "Choose agents, connect Obsidian, add integrations."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "history",
|
|
33
|
+
label: "History",
|
|
34
|
+
view: ORCHESTRATOR_VIEWS.ACTIVITY,
|
|
35
|
+
description: "What Kairo changed, and how to undo it."
|
|
36
|
+
}
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** Destinations reachable via the action palette, not the top nav. */
|
|
40
|
+
export const COCKPIT_SECONDARY = [
|
|
25
41
|
{
|
|
26
42
|
id: "governance",
|
|
27
43
|
label: "Governance",
|
|
28
44
|
view: ORCHESTRATOR_VIEWS.CHANGES,
|
|
29
45
|
description: "Repair drift and apply governed changes."
|
|
30
46
|
},
|
|
31
|
-
{
|
|
32
|
-
id: "activity",
|
|
33
|
-
label: "Activity",
|
|
34
|
-
view: ORCHESTRATOR_VIEWS.ACTIVITY,
|
|
35
|
-
description: "Operations, backups, and recovery."
|
|
36
|
-
},
|
|
37
47
|
{
|
|
38
48
|
id: "orchestration",
|
|
39
49
|
label: "Orchestration",
|
|
@@ -45,12 +55,6 @@ export const COCKPIT_NAV = [
|
|
|
45
55
|
label: "Usage",
|
|
46
56
|
view: ORCHESTRATOR_VIEWS.USAGE,
|
|
47
57
|
description: "Token and context pressure when auditable."
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
id: "settings",
|
|
51
|
-
label: "Settings",
|
|
52
|
-
view: ORCHESTRATOR_VIEWS.PROFILE,
|
|
53
|
-
description: "Profile, policy, and guided configuration."
|
|
54
58
|
}
|
|
55
59
|
];
|
|
56
60
|
|
|
@@ -64,8 +68,8 @@ export function regionsForLayout(layoutMode) {
|
|
|
64
68
|
|
|
65
69
|
export function navIndexForView(view, items = COCKPIT_NAV) {
|
|
66
70
|
if (isRunsBranchView(view)) {
|
|
67
|
-
const
|
|
68
|
-
return
|
|
71
|
+
const historyIndex = items.findIndex((item) => item.id === "history");
|
|
72
|
+
return historyIndex >= 0 ? historyIndex : 0;
|
|
69
73
|
}
|
|
70
74
|
const index = items.findIndex((item) => item.view === view);
|
|
71
75
|
return index >= 0 ? index : 0;
|
|
@@ -105,6 +109,7 @@ export function resolveNavStatusSummary(item, {
|
|
|
105
109
|
const backups = snapshot?.backups?.count ?? 0;
|
|
106
110
|
|
|
107
111
|
switch (item.id) {
|
|
112
|
+
case "home":
|
|
108
113
|
case "overview":
|
|
109
114
|
return snapshot?.health?.replaceAll("_", " ")
|
|
110
115
|
?? resolveProjectReadiness({
|
|
@@ -114,6 +119,7 @@ export function resolveNavStatusSummary(item, {
|
|
|
114
119
|
}).label;
|
|
115
120
|
case "governance":
|
|
116
121
|
return changes > 0 ? `${changes} pending` : "Clean";
|
|
122
|
+
case "history":
|
|
117
123
|
case "activity":
|
|
118
124
|
return backups > 0 ? `${backups} backups` : "No backups";
|
|
119
125
|
case "orchestration":
|
|
@@ -174,6 +180,7 @@ export function buildNavModel({
|
|
|
174
180
|
const mapped = items.map((item, index) => {
|
|
175
181
|
const isSelected = index === navIndex;
|
|
176
182
|
const isCurrent = item.view === currentView
|
|
183
|
+
|| (item.id === "history" && isRunsBranchView(currentView))
|
|
177
184
|
|| (item.id === "orchestration" && isRunsBranchView(currentView));
|
|
178
185
|
return {
|
|
179
186
|
...item,
|
|
@@ -185,11 +192,13 @@ export function buildNavModel({
|
|
|
185
192
|
};
|
|
186
193
|
});
|
|
187
194
|
|
|
195
|
+
const explanation = !selected || selected.id === "home" || selected.id === "overview"
|
|
196
|
+
? ""
|
|
197
|
+
: (selected.description ?? "");
|
|
198
|
+
|
|
188
199
|
return {
|
|
189
200
|
title: "NAVIGATION",
|
|
190
|
-
explanation
|
|
191
|
-
? `${selected.description} (${resolveNavStatusSummary(selected, { dashboard, diagnostics, snapshot })})`
|
|
192
|
-
: "",
|
|
201
|
+
explanation,
|
|
193
202
|
items: mapped
|
|
194
203
|
};
|
|
195
204
|
}
|
|
@@ -320,7 +329,7 @@ export function buildFooterModel({
|
|
|
320
329
|
// HOME footer must stay on one line at 80 cols (frame already near 24 rows).
|
|
321
330
|
if (view === ORCHESTRATOR_VIEWS.HOME) {
|
|
322
331
|
return {
|
|
323
|
-
text: ["↑↓", "Enter", "Space", "R", "?", "/", "Esc"].join(` ${glyphs.bullet} `),
|
|
332
|
+
text: ["↑↓", "Enter", "Tab", "Space", "R", "?", "/", "Esc"].join(` ${glyphs.bullet} `),
|
|
324
333
|
columns: footerColumns
|
|
325
334
|
};
|
|
326
335
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { COCKPIT_NAV } from "./cockpit-models.js";
|
|
1
|
+
import { COCKPIT_NAV, COCKPIT_SECONDARY } from "./cockpit-models.js";
|
|
2
2
|
import { ORCHESTRATOR_VIEWS } from "./orchestrator-state.js";
|
|
3
3
|
|
|
4
|
+
const PALETTE_NAV_ITEMS = [...COCKPIT_NAV, ...COCKPIT_SECONDARY];
|
|
5
|
+
|
|
4
6
|
export const PALETTE_KINDS = Object.freeze({
|
|
5
7
|
NAVIGATE: "navigate",
|
|
6
8
|
REFRESH: "refresh",
|
|
@@ -12,10 +14,13 @@ const DESTINATION_VIEWS = Object.freeze({
|
|
|
12
14
|
changes: ORCHESTRATOR_VIEWS.CHANGES,
|
|
13
15
|
governance: ORCHESTRATOR_VIEWS.CHANGES,
|
|
14
16
|
"control-center": ORCHESTRATOR_VIEWS.HOME,
|
|
17
|
+
home: ORCHESTRATOR_VIEWS.HOME,
|
|
15
18
|
ides: ORCHESTRATOR_VIEWS.IDES,
|
|
16
19
|
modules: ORCHESTRATOR_VIEWS.MODULES,
|
|
17
20
|
activity: ORCHESTRATOR_VIEWS.ACTIVITY,
|
|
21
|
+
history: ORCHESTRATOR_VIEWS.ACTIVITY,
|
|
18
22
|
profile: ORCHESTRATOR_VIEWS.PROFILE,
|
|
23
|
+
settings: ORCHESTRATOR_VIEWS.PROFILE,
|
|
19
24
|
runs: ORCHESTRATOR_VIEWS.RUNS,
|
|
20
25
|
orchestration: ORCHESTRATOR_VIEWS.RUNS,
|
|
21
26
|
usage: ORCHESTRATOR_VIEWS.USAGE,
|
|
@@ -30,12 +35,12 @@ export function canOpenPalette({ loading = false, busy = false, confirming = fal
|
|
|
30
35
|
return !loading && !busy && !confirming;
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
/** Optional CTA +
|
|
38
|
+
/** Optional CTA + primary/secondary destinations + Refresh + Help. No write shortcuts. */
|
|
34
39
|
export function buildPaletteActions({
|
|
35
40
|
ctaDestination = null,
|
|
36
41
|
ctaTitle = null,
|
|
37
42
|
ctaDetail = null,
|
|
38
|
-
navItems =
|
|
43
|
+
navItems = PALETTE_NAV_ITEMS
|
|
39
44
|
} = {}) {
|
|
40
45
|
const actions = [];
|
|
41
46
|
if (ctaDestination === "setup") {
|
|
@@ -100,7 +100,8 @@ export function renderCockpitView({
|
|
|
100
100
|
governanceDetailsOpen = false,
|
|
101
101
|
activityDetailsOpen = false,
|
|
102
102
|
contentFocused = false,
|
|
103
|
-
homeDir = null
|
|
103
|
+
homeDir = null,
|
|
104
|
+
hasGlobalState = false
|
|
104
105
|
}) {
|
|
105
106
|
if (palette) {
|
|
106
107
|
return React.createElement(PalettePanel, { model: palette, colorEnabled });
|
|
@@ -112,7 +113,13 @@ export function renderCockpitView({
|
|
|
112
113
|
detailsOpen: overviewDetailsOpen,
|
|
113
114
|
colorEnabled,
|
|
114
115
|
unicode,
|
|
115
|
-
layoutMode
|
|
116
|
+
layoutMode,
|
|
117
|
+
selectedIndex: listIndex,
|
|
118
|
+
contentFocused,
|
|
119
|
+
hasGlobalState,
|
|
120
|
+
snapshot,
|
|
121
|
+
diagnostics,
|
|
122
|
+
dashboard
|
|
116
123
|
});
|
|
117
124
|
case ORCHESTRATOR_VIEWS.USAGE:
|
|
118
125
|
return React.createElement(SemanticUsagePanel, {
|
|
@@ -46,6 +46,7 @@ import { LAYOUT_MODES } from "./layout.js";
|
|
|
46
46
|
import { CHANGES_PHASE } from "./cockpit-changes.js";
|
|
47
47
|
import { RECOVERY_PHASE, listRecoverySnapshots } from "./cockpit-recovery.js";
|
|
48
48
|
import { SETTINGS_PHASE } from "./cockpit-settings.js";
|
|
49
|
+
import { buildOverviewButtons, OVERVIEW_BUTTON_COUNT } from "./ux/overview-actions.js";
|
|
49
50
|
|
|
50
51
|
export function OrchestratorApp({
|
|
51
52
|
homeDir,
|
|
@@ -245,7 +246,9 @@ export function OrchestratorApp({
|
|
|
245
246
|
return;
|
|
246
247
|
}
|
|
247
248
|
|
|
248
|
-
const listLength = ui.view === ORCHESTRATOR_VIEWS.
|
|
249
|
+
const listLength = ui.view === ORCHESTRATOR_VIEWS.HOME
|
|
250
|
+
? OVERVIEW_BUTTON_COUNT
|
|
251
|
+
: ui.view === ORCHESTRATOR_VIEWS.RUNS
|
|
249
252
|
? RUNS_HUB_ITEMS.length
|
|
250
253
|
: ui.view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS
|
|
251
254
|
? (data.dashboard?.activeRuns ?? []).length
|
|
@@ -301,6 +304,21 @@ export function OrchestratorApp({
|
|
|
301
304
|
return;
|
|
302
305
|
}
|
|
303
306
|
}
|
|
307
|
+
if (routed.type === "enter-home-button") {
|
|
308
|
+
const buttons = buildOverviewButtons({
|
|
309
|
+
hasGlobalState,
|
|
310
|
+
snapshot: data.snapshot,
|
|
311
|
+
diagnostics: data.diagnostics,
|
|
312
|
+
dashboard: data.dashboard
|
|
313
|
+
});
|
|
314
|
+
const selected = buttons[Math.min(Math.max(0, ui.listIndex), buttons.length - 1)];
|
|
315
|
+
const intent = selected?.intent ?? null;
|
|
316
|
+
if (intent === "setup") {
|
|
317
|
+
finish({ cancelled: false, action: "setup" });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (intent && openDestination(intent)) return;
|
|
321
|
+
}
|
|
304
322
|
dispatch(routed);
|
|
305
323
|
return;
|
|
306
324
|
}
|
|
@@ -583,7 +601,8 @@ export function OrchestratorApp({
|
|
|
583
601
|
governanceDetailsOpen: ui.governanceDetailsOpen,
|
|
584
602
|
activityDetailsOpen: ui.activityDetailsOpen,
|
|
585
603
|
contentFocused: ui.region === COCKPIT_REGIONS.CONTENT,
|
|
586
|
-
homeDir
|
|
604
|
+
homeDir,
|
|
605
|
+
hasGlobalState
|
|
587
606
|
})
|
|
588
607
|
)
|
|
589
608
|
);
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Live semantic Overview for Cockpit HOME — product cover.
|
|
3
|
-
*
|
|
3
|
+
* Content owns the button focus mark when region=content.
|
|
4
4
|
* ASCII wordmark only here (wide/compact); minimal is textual.
|
|
5
5
|
*
|
|
6
|
-
* Rule: show purpose +
|
|
6
|
+
* Rule: show purpose + two buttons + a few plain-language needs.
|
|
7
7
|
* Machine/system noise stays out of the first screen (Details only).
|
|
8
8
|
*/
|
|
9
9
|
import React from "react";
|
|
10
10
|
import { Box, Text } from "ink";
|
|
11
11
|
import { DASHBOARD_PURPOSE } from "../../dashboard-guidance.js";
|
|
12
12
|
import { LAYOUT_MODES } from "../layout.js";
|
|
13
|
-
import { COCKPIT_COLORS } from "../theme.js";
|
|
13
|
+
import { COCKPIT_COLORS, resolveGlyphs } from "../theme.js";
|
|
14
14
|
import {
|
|
15
15
|
overviewBrandTitle,
|
|
16
16
|
shouldShowWordmark,
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
mapHealthTone,
|
|
25
25
|
partitionCompanionLines
|
|
26
26
|
} from "./overview-needs.js";
|
|
27
|
+
import { buildOverviewButtons, OVERVIEW_BUTTON_COUNT } from "./overview-actions.js";
|
|
27
28
|
|
|
28
29
|
export {
|
|
29
30
|
humanizeCompanionNeed,
|
|
@@ -33,6 +34,8 @@ export {
|
|
|
33
34
|
partitionCompanionLines
|
|
34
35
|
} from "./overview-needs.js";
|
|
35
36
|
|
|
37
|
+
export { buildOverviewButtons, OVERVIEW_BUTTON_COUNT } from "./overview-actions.js";
|
|
38
|
+
|
|
36
39
|
/** Safe Details lines — leftovers + raw signals; never invent paths/IDs. */
|
|
37
40
|
export function buildOverviewDetails(model = {}, companionRest = []) {
|
|
38
41
|
const lines = [];
|
|
@@ -59,13 +62,19 @@ export function buildOverviewDetails(model = {}, companionRest = []) {
|
|
|
59
62
|
|
|
60
63
|
/**
|
|
61
64
|
* Pure adapter: buildControlCenterModel → semantic overview props.
|
|
62
|
-
* First screen = purpose +
|
|
65
|
+
* First screen = purpose + two buttons + plain needs. Machine noise → Details.
|
|
63
66
|
*/
|
|
64
|
-
export function adaptControlCenterToOverview(model = {}) {
|
|
67
|
+
export function adaptControlCenterToOverview(model = {}, options = {}) {
|
|
65
68
|
const status = model.status ?? model.health ?? {};
|
|
66
69
|
const next = model.nextAction ?? model.cta ?? {};
|
|
67
70
|
const { needs, rest } = partitionCompanionLines(model.companion?.lines ?? []);
|
|
68
71
|
const primary = humanizePrimary(next);
|
|
72
|
+
const buttons = buildOverviewButtons({
|
|
73
|
+
hasGlobalState: options.hasGlobalState,
|
|
74
|
+
snapshot: options.snapshot,
|
|
75
|
+
diagnostics: options.diagnostics,
|
|
76
|
+
dashboard: options.dashboard
|
|
77
|
+
});
|
|
69
78
|
|
|
70
79
|
const activity = model.activity?.headline;
|
|
71
80
|
const metrics = [];
|
|
@@ -104,6 +113,7 @@ export function adaptControlCenterToOverview(model = {}) {
|
|
|
104
113
|
body: status.summaryLine ?? ""
|
|
105
114
|
},
|
|
106
115
|
primary,
|
|
116
|
+
buttons,
|
|
107
117
|
metrics,
|
|
108
118
|
details: buildOverviewDetails(model, rest)
|
|
109
119
|
};
|
|
@@ -136,14 +146,30 @@ export function SemanticOverviewPanel({
|
|
|
136
146
|
detailsOpen = false,
|
|
137
147
|
colorEnabled = true,
|
|
138
148
|
unicode = true,
|
|
139
|
-
layoutMode = LAYOUT_MODES.COMPACT
|
|
149
|
+
layoutMode = LAYOUT_MODES.COMPACT,
|
|
150
|
+
selectedIndex = 0,
|
|
151
|
+
contentFocused = false,
|
|
152
|
+
hasGlobalState = false,
|
|
153
|
+
snapshot = null,
|
|
154
|
+
diagnostics = null,
|
|
155
|
+
dashboard = null
|
|
140
156
|
}) {
|
|
141
|
-
const view = adaptControlCenterToOverview(model
|
|
157
|
+
const view = adaptControlCenterToOverview(model, {
|
|
158
|
+
hasGlobalState,
|
|
159
|
+
snapshot,
|
|
160
|
+
diagnostics,
|
|
161
|
+
dashboard
|
|
162
|
+
});
|
|
142
163
|
const showArt = shouldShowWordmark(layoutMode);
|
|
143
164
|
const brandTitle = overviewBrandTitle(layoutMode);
|
|
144
165
|
const isWide = layoutMode === LAYOUT_MODES.WIDE;
|
|
145
166
|
const mark = renderWordmark({ layoutMode, colorEnabled, unicode });
|
|
146
167
|
const status = renderCallout(view, colorEnabled);
|
|
168
|
+
const safeIndex = Math.min(
|
|
169
|
+
Math.max(0, selectedIndex),
|
|
170
|
+
Math.max(0, view.buttons.length - 1)
|
|
171
|
+
);
|
|
172
|
+
const glyphs = resolveGlyphs(unicode);
|
|
147
173
|
|
|
148
174
|
const hero = showArt
|
|
149
175
|
? (isWide
|
|
@@ -166,18 +192,25 @@ export function SemanticOverviewPanel({
|
|
|
166
192
|
color: colorEnabled ? COCKPIT_COLORS.muted : undefined
|
|
167
193
|
}, view.purpose),
|
|
168
194
|
React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
195
|
+
...view.buttons.map((button, index) => {
|
|
196
|
+
const selected = index === safeIndex;
|
|
197
|
+
const focused = contentFocused && selected;
|
|
198
|
+
return React.createElement(Box, {
|
|
199
|
+
key: button.id,
|
|
200
|
+
flexDirection: "column",
|
|
201
|
+
marginBottom: index === view.buttons.length - 1 ? 0 : 1
|
|
202
|
+
},
|
|
203
|
+
React.createElement(Text, {
|
|
204
|
+
bold: true,
|
|
205
|
+
color: focused && colorEnabled ? COCKPIT_COLORS.interactive : undefined
|
|
206
|
+
}, `${focused ? glyphs.focus : " "} ${button.label}`),
|
|
207
|
+
button.detail
|
|
208
|
+
? React.createElement(Text, {
|
|
209
|
+
color: colorEnabled ? COCKPIT_COLORS.muted : undefined
|
|
210
|
+
}, ` ${button.detail}`)
|
|
211
|
+
: null
|
|
212
|
+
);
|
|
213
|
+
})
|
|
181
214
|
),
|
|
182
215
|
React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
183
216
|
React.createElement(ActionList, {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two explicit Home buttons — prepare + configure.
|
|
3
|
+
* No writes here: intents only open existing consent-gated flows.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const OVERVIEW_BUTTON_COUNT = 2;
|
|
7
|
+
|
|
8
|
+
function changeCount(snapshot) {
|
|
9
|
+
if (!snapshot?.diff?.hasChanges) return 0;
|
|
10
|
+
return snapshot.diff.changeCount
|
|
11
|
+
?? snapshot.diff.changes?.length
|
|
12
|
+
?? 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function detectedAgentCount({ snapshot = null, diagnostics = null, dashboard = null } = {}) {
|
|
16
|
+
if (Number.isFinite(snapshot?.coverage?.detectedAgents)) {
|
|
17
|
+
return snapshot.coverage.detectedAgents;
|
|
18
|
+
}
|
|
19
|
+
if (Number.isFinite(diagnostics?.diagnostics?.detected)) {
|
|
20
|
+
return diagnostics.diagnostics.detected;
|
|
21
|
+
}
|
|
22
|
+
return (dashboard?.providers ?? []).filter((entry) => entry?.available).length;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildPrepareButton({
|
|
26
|
+
hasGlobalState = false,
|
|
27
|
+
snapshot = null,
|
|
28
|
+
diagnostics = null,
|
|
29
|
+
dashboard = null
|
|
30
|
+
} = {}) {
|
|
31
|
+
const detected = detectedAgentCount({ snapshot, diagnostics, dashboard });
|
|
32
|
+
const needsSetup = hasGlobalState === false || detected === 0;
|
|
33
|
+
if (needsSetup) {
|
|
34
|
+
return {
|
|
35
|
+
id: "prepare",
|
|
36
|
+
label: "Set up Kairo",
|
|
37
|
+
detail: "Detect your agents and install what is missing.",
|
|
38
|
+
intent: "setup"
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const pending = changeCount(snapshot);
|
|
43
|
+
if (pending > 0) {
|
|
44
|
+
return {
|
|
45
|
+
id: "prepare",
|
|
46
|
+
label: `Repair ${pending} change${pending === 1 ? "" : "s"}`,
|
|
47
|
+
detail: "You will see the exact plan before anything is written.",
|
|
48
|
+
intent: "governance"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
id: "prepare",
|
|
54
|
+
label: "Everything is ready",
|
|
55
|
+
detail: "Open History to see what changed, or Configure to adjust.",
|
|
56
|
+
intent: "history"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const CONFIGURE_BUTTON = Object.freeze({
|
|
61
|
+
id: "configure",
|
|
62
|
+
label: "Configure",
|
|
63
|
+
detail: "Agents, Obsidian vault, integrations.",
|
|
64
|
+
intent: "settings"
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Exactly two buttons for Home. First button changes face by state.
|
|
69
|
+
*/
|
|
70
|
+
export function buildOverviewButtons({
|
|
71
|
+
hasGlobalState = false,
|
|
72
|
+
snapshot = null,
|
|
73
|
+
diagnostics = null,
|
|
74
|
+
dashboard = null
|
|
75
|
+
} = {}) {
|
|
76
|
+
return [
|
|
77
|
+
buildPrepareButton({ hasGlobalState, snapshot, diagnostics, dashboard }),
|
|
78
|
+
{ ...CONFIGURE_BUTTON }
|
|
79
|
+
];
|
|
80
|
+
}
|
|
@@ -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
|
+
}
|