mini-coder 0.7.2 → 0.7.3
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/src/index.ts +9 -1
- package/src/tui-conversation.ts +12 -0
- package/src/tui.ts +6 -0
- package/src/types.ts +6 -0
- package/src/update.ts +171 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -4,10 +4,18 @@ import { getBranchLabel } from "./git.ts";
|
|
|
4
4
|
import { streamHeadless } from "./headless.ts";
|
|
5
5
|
import { initTUI } from "./tui.ts";
|
|
6
6
|
import type { TUIState } from "./types.ts";
|
|
7
|
+
import { updateMiniCoder } from "./update.ts";
|
|
7
8
|
|
|
8
9
|
export async function main(): Promise<void> {
|
|
10
|
+
const argv = process.argv.slice(2);
|
|
11
|
+
|
|
12
|
+
if (argv.includes("--update")) {
|
|
13
|
+
await updateMiniCoder();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
const cwd = basename(process.cwd());
|
|
10
|
-
const options = await handleArgv(
|
|
18
|
+
const options = await handleArgv(argv);
|
|
11
19
|
|
|
12
20
|
function leave(msg?: string) {
|
|
13
21
|
if (msg) console.log(msg);
|
package/src/tui-conversation.ts
CHANGED
|
@@ -14,6 +14,17 @@ export function emptyState(state: TUIState): Node {
|
|
|
14
14
|
state.options.theme === "ansi16"
|
|
15
15
|
? randColor
|
|
16
16
|
: textColorForBackground(theme.bblack, state.options.theme);
|
|
17
|
+
const updateNotice = state.availableUpdate
|
|
18
|
+
? [
|
|
19
|
+
HStack({ gap: 1 }, [
|
|
20
|
+
TextPill("update", shortcutFgColor, theme.bblack, 13),
|
|
21
|
+
Text(
|
|
22
|
+
`mini-coder ${state.availableUpdate.latestVersion} is available. Run mc --update.`,
|
|
23
|
+
{ fgColor: theme.bblack },
|
|
24
|
+
),
|
|
25
|
+
]),
|
|
26
|
+
]
|
|
27
|
+
: [];
|
|
17
28
|
return HStack({ flex: 1, alignItems: "center" }, [
|
|
18
29
|
VStack({ flex: 1, alignItems: "center", gap: 1 }, [
|
|
19
30
|
HStack({ gap: 1 }, [
|
|
@@ -45,6 +56,7 @@ export function emptyState(state: TUIState): Node {
|
|
|
45
56
|
TextPill("ctrl+c|d|q", shortcutFgColor, theme.bblack, 13),
|
|
46
57
|
Text("Quit.", { fgColor: theme.bblack }),
|
|
47
58
|
]),
|
|
59
|
+
...updateNotice,
|
|
48
60
|
]),
|
|
49
61
|
]),
|
|
50
62
|
]);
|
package/src/tui.ts
CHANGED
|
@@ -30,6 +30,11 @@ import { Conversation, emptyState } from "./tui-conversation";
|
|
|
30
30
|
import { Editor } from "./tui-editor";
|
|
31
31
|
import { mainMenu } from "./tui-overlay";
|
|
32
32
|
import type { AgentContex, ToolAndRunner, TUIMessage, TUIState } from "./types";
|
|
33
|
+
import { getAvailableUpdate } from "./update";
|
|
34
|
+
|
|
35
|
+
async function refreshAvailableUpdate(state: TUIState): Promise<void> {
|
|
36
|
+
state.availableUpdate = await getAvailableUpdate();
|
|
37
|
+
}
|
|
33
38
|
|
|
34
39
|
function clearOrAbort(state: TUIState) {
|
|
35
40
|
// Are we mid stream? Abort it.
|
|
@@ -46,6 +51,7 @@ function clearOrAbort(state: TUIState) {
|
|
|
46
51
|
export function initTUI(state: TUIState, leave: (s: string) => void) {
|
|
47
52
|
// TODO: Cleanup accumulated sessions for this cwd.
|
|
48
53
|
const { spinnerEvery, currentSpinner } = Spinner();
|
|
54
|
+
void refreshAvailableUpdate(state);
|
|
49
55
|
|
|
50
56
|
// Stable 60fps rendering.
|
|
51
57
|
// This ensure Xfps, and excessive calls get coalesced in cel-tui.
|
package/src/types.ts
CHANGED
|
@@ -98,6 +98,11 @@ export type TUIMessage = {
|
|
|
98
98
|
toolCalls?: TUIToolCall[];
|
|
99
99
|
};
|
|
100
100
|
|
|
101
|
+
export type AvailableUpdate = {
|
|
102
|
+
currentVersion: string;
|
|
103
|
+
latestVersion: string;
|
|
104
|
+
};
|
|
105
|
+
|
|
101
106
|
export type TUIState = {
|
|
102
107
|
options: CliOptions;
|
|
103
108
|
prompt: string;
|
|
@@ -110,6 +115,7 @@ export type TUIState = {
|
|
|
110
115
|
abortController?: AbortController;
|
|
111
116
|
cwd: string;
|
|
112
117
|
gitBranch?: string;
|
|
118
|
+
availableUpdate?: AvailableUpdate | undefined;
|
|
113
119
|
overlay?: boolean | undefined;
|
|
114
120
|
forceThemeRefresh?: boolean | undefined;
|
|
115
121
|
sessionId?: string | undefined;
|
package/src/update.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { type Static, Type } from "@earendil-works/pi-ai";
|
|
5
|
+
import { Value } from "typebox/value";
|
|
6
|
+
|
|
7
|
+
import { DATA_DIR } from "./shared";
|
|
8
|
+
import type { AvailableUpdate } from "./types";
|
|
9
|
+
|
|
10
|
+
const PACKAGE_NAME = "mini-coder";
|
|
11
|
+
const PACKAGE_MANIFEST_URL = new URL("../package.json", import.meta.url);
|
|
12
|
+
const UPDATE_CHECK_CACHE_PATH = join(DATA_DIR, "update-check.json");
|
|
13
|
+
const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
14
|
+
const UPDATE_CHECK_TIMEOUT_MS = 5_000;
|
|
15
|
+
|
|
16
|
+
const UpdateCheckCacheSchema = Type.Object({
|
|
17
|
+
checkedAt: Type.Number(),
|
|
18
|
+
currentVersion: Type.String(),
|
|
19
|
+
latestVersion: Type.Optional(Type.String()),
|
|
20
|
+
});
|
|
21
|
+
type UpdateCheckCache = Static<typeof UpdateCheckCacheSchema>;
|
|
22
|
+
|
|
23
|
+
function parseLatestVersion(output: string): string | undefined {
|
|
24
|
+
const version = output.trim().split(/\s+/)[0];
|
|
25
|
+
|
|
26
|
+
if (!version || !isValidVersion(version)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return version;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isValidVersion(version: string): boolean {
|
|
34
|
+
try {
|
|
35
|
+
Bun.semver.order(version, version);
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isNewerVersion(
|
|
43
|
+
currentVersion: string,
|
|
44
|
+
latestVersion: string,
|
|
45
|
+
): boolean {
|
|
46
|
+
try {
|
|
47
|
+
return Bun.semver.order(latestVersion, currentVersion) === 1;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isFreshUpdateCheck(cache: UpdateCheckCache, now: number): boolean {
|
|
54
|
+
return now - cache.checkedAt < UPDATE_CHECK_INTERVAL_MS;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getAvailableUpdateFromLatest(
|
|
58
|
+
currentVersion: string,
|
|
59
|
+
latestVersion: string | undefined,
|
|
60
|
+
): AvailableUpdate | undefined {
|
|
61
|
+
if (!latestVersion || !isNewerVersion(currentVersion, latestVersion)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { currentVersion, latestVersion };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function getCurrentVersion(): Promise<string | undefined> {
|
|
69
|
+
const manifest = (await Bun.file(PACKAGE_MANIFEST_URL).json()) as {
|
|
70
|
+
version?: unknown;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (typeof manifest.version !== "string") {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return manifest.version;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function getLatestVersion(): Promise<string | undefined> {
|
|
81
|
+
try {
|
|
82
|
+
const proc = Bun.spawn(["bun", "pm", "view", PACKAGE_NAME, "version"], {
|
|
83
|
+
stdout: "pipe",
|
|
84
|
+
stderr: "ignore",
|
|
85
|
+
timeout: UPDATE_CHECK_TIMEOUT_MS,
|
|
86
|
+
});
|
|
87
|
+
const [stdout, exitCode] = await Promise.all([
|
|
88
|
+
proc.stdout.text(),
|
|
89
|
+
proc.exited,
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
if (exitCode !== 0) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return parseLatestVersion(stdout);
|
|
97
|
+
} catch {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function readUpdateCheckCache(): Promise<UpdateCheckCache | undefined> {
|
|
103
|
+
try {
|
|
104
|
+
const file = Bun.file(UPDATE_CHECK_CACHE_PATH);
|
|
105
|
+
|
|
106
|
+
if (!(await file.exists())) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const value = (await file.json()) as unknown;
|
|
111
|
+
|
|
112
|
+
if (!Value.Check(UpdateCheckCacheSchema, value)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return value;
|
|
117
|
+
} catch {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function writeUpdateCheckCache(cache: UpdateCheckCache): Promise<void> {
|
|
123
|
+
try {
|
|
124
|
+
await mkdir(DATA_DIR, { recursive: true });
|
|
125
|
+
await Bun.write(UPDATE_CHECK_CACHE_PATH, JSON.stringify(cache, null, 2));
|
|
126
|
+
} catch {
|
|
127
|
+
// Update checks are best-effort and must never interrupt startup.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function getAvailableUpdate(): Promise<
|
|
132
|
+
AvailableUpdate | undefined
|
|
133
|
+
> {
|
|
134
|
+
const currentVersion = await getCurrentVersion().catch(() => undefined);
|
|
135
|
+
|
|
136
|
+
if (!currentVersion) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
const cache = await readUpdateCheckCache();
|
|
142
|
+
|
|
143
|
+
if (cache && isFreshUpdateCheck(cache, now)) {
|
|
144
|
+
return getAvailableUpdateFromLatest(currentVersion, cache.latestVersion);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const latestVersion = await getLatestVersion();
|
|
148
|
+
await writeUpdateCheckCache({
|
|
149
|
+
checkedAt: now,
|
|
150
|
+
currentVersion,
|
|
151
|
+
latestVersion,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return getAvailableUpdateFromLatest(currentVersion, latestVersion);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function updateMiniCoder(): Promise<void> {
|
|
158
|
+
console.log("Updating mini-coder...");
|
|
159
|
+
|
|
160
|
+
const proc = Bun.spawn(["bun", "add", "-g", "mini-coder@latest"], {
|
|
161
|
+
stdout: "inherit",
|
|
162
|
+
stderr: "inherit",
|
|
163
|
+
});
|
|
164
|
+
const exitCode = await proc.exited;
|
|
165
|
+
|
|
166
|
+
if (exitCode !== 0) {
|
|
167
|
+
throw new Error(`Update failed with exit code ${exitCode}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log("mini-coder updated.");
|
|
171
|
+
}
|