@chibitek/chibi 0.1.155 → 0.1.158
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/dist/assets/{abnfDiagram-O67JEVCF-B8LdsCr1.js → abnfDiagram-O67JEVCF-B8-8zzMS.js} +1 -1
- package/dist/assets/{arc-DU5fSmKU.js → arc-CSHQ9Dx3.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-DdQSIFKv.js → architectureDiagram-NJMV4G6O-BCHM9rXW.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CaPcGYSS.js → blockDiagram-BEXU5L5S-B-5uxPFN.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-unt0ePgO.js → c4Diagram-YGBWAQC7-CuKNM_MR.js} +1 -1
- package/dist/assets/channel-bPN03jIt.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-Babg5Fzu.js → chunk-2Q5K7J3B-BNF02GM4.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-D1-dWVHo.js → chunk-3FUC2YCW-uEWgBd4M.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-DU7nSMbT.js → chunk-5VM5RSS4-eSZuXctY.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-QIjCJIhV.js → chunk-GTNCS2PH-CslS8nRS.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-pSByUE5R.js → chunk-GWA4HPMP-w71-MeKj.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-Dj2AaR1b.js → chunk-JWPE2WC7-Bjju7WGh.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-pZbN0-FP.js → chunk-NETBCI7D-HxrroD4M.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-Bqarwe4D.js → chunk-WEXAMYUT-D7Rz470x.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-WV8lm3aa.js → chunk-XXDRQBXY-BWKnAeyM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BANBtky1.js → classDiagram-v2-NBCMYWYE-CeBDRHmP.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-CtrktgBC.js → cose-bilkent-JH36ORCC-BBbRKlvr.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-DnhVVfba.js → cynefin-OW5HDTMX-W02cnJ0B.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-B42SRR6x.js → cynefinDiagram-VND7K2PF-Cs47eaxd.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DM--vFMK.js → dagre-6A5THRUB-8mG18hzT.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-ZBupBrPU.js → diagram-22UHCM2B-DF4Qrw67.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-BPQzNIOd.js → diagram-3UASUU5V-3BAVoed1.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-5kO8vf8Z.js → diagram-ATOU4E4O-CKO3pECE.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DtxogB86.js → diagram-CDSNMT55-CZhrGvur.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-Co9-nYw_.js → diagram-MLGK6HIB-C07l9pdQ.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-DWRuMh5y.js → diagram-MPIPVDR6-D3or4lkZ.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DU_nmoG9.js → ebnfDiagram-ZINNZB2B-DV2epXp_.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-Bcje7S94.js → elk-276RUBZZ-DlvEn4-1.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-9aCo_2CI.js → erDiagram-OPXOYQCR-CB6X7Wt5.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-D4FYFyc5.js → flowDiagram-KWPJA3E3-C3bqz4lC.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-WQHX5rbx.js → ganttDiagram-FUAMR5RP-yo_Evhab.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-DRyzBdiV.js → gitGraphDiagram-X574FWY7-Cq2oUlap.js} +1 -1
- package/dist/assets/{index-CG1OvRgO.js → index-CG4AFPb4.js} +1 -1
- package/dist/assets/{index-D_8SD_0K.js → index-CMcr5k3Z.js} +205 -200
- package/dist/assets/index-CZjRcUdn.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-D7SOmp4n.js → infoDiagram-VRGFBTTK-Y3ZrY7qL.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-CdxOnDVX.js → ishikawaDiagram-OU5B5YK6-BpOtSfyi.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CH7477Oa.js → journeyDiagram-ZHPQQLJL-UQM727le.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BdpVvkHd.js → kanban-definition-PNTS6WVX-C3s5oHpY.js} +1 -1
- package/dist/assets/{linear-DTGq6qn5.js → linear-hX6nFOQr.js} +1 -1
- package/dist/assets/{mermaid.core-Sn2sv0Ti.js → mermaid.core-DZObGLN7.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-koCmkYuq.js → mindmap-definition-NLK3R4M7-Br-i2E3P.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-C3sqhm8e.js → pegDiagram-GJSIUBJH-D5SERB-N.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-BELBK122.js → pieDiagram-5QR66LMP-BPNf4J3D.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Cksi5LSm.js → quadrantDiagram-O4NWA36T-Bm_Wh7Jr.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-f1av_EZW.js → railroadDiagram-XR7U4H2S-D6bo0abD.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CW4O5nER.js → requirementDiagram-PLB6GJNP--1n5rkV7.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-Dz8ADHNY.js → sankeyDiagram-IPEJSGJF-Ct4tP2kK.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-Cc3HE0iZ.js → sequenceDiagram-PO4LG4MO-CCG7O5fR.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-gdaZNHnK.js → sizeCapture-INFHLROL-CJJJra9c.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK--u8LCnab.js → stateDiagram-v2-GCMORJYK-BMdRvVEX.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-4MAw4kdO.js → swimlanes-2SLR337P-B1wnogip.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-DWyjdGGR.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-CL89YkCT.js → timeline-definition-EJHVYXUP-l-3yhXUP.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-C92l7YQe.js → usecaseDiagram-POWQR4AR-DtwtCOoJ.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-17siYhSj.js → vennDiagram-UO4OBE2U-BZAEce1B.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-CNwa9VmN.js → wardleyDiagram-VNRHLVJA-Na_BCMMO.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Dnu6Xzih.js → xychartDiagram-PMCCYNJV-BW9dlUfE.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +68 -14
- package/dist-server/index.js +5633 -4562
- package/dist-server/openmausbot.js +140 -81
- package/dist-server/pair-cli.js +140 -81
- package/dist-server/server/app-installers.js +107 -7
- package/dist-server/server/avatar-subscription.js +51 -0
- package/dist-server/server/cli-help.js +74 -0
- package/dist-server/server/cli.js +37 -78
- package/dist-server/server/colima-docker.js +136 -0
- package/dist-server/server/container-computer.js +31 -63
- package/dist-server/server/container-mcp.js +5 -0
- package/dist-server/server/device-routing.js +60 -0
- package/dist-server/server/drivers/claude.js +13 -11
- package/dist-server/server/drivers/local-inject.js +3 -4
- package/dist-server/server/engine-failover.js +22 -12
- package/dist-server/server/engine-tiers.js +5 -14
- package/dist-server/server/index.js +124 -120
- package/dist-server/server/personal-goal-runtime.js +86 -0
- package/dist-server/server/personal-goal-turn.js +125 -0
- package/dist-server/server/personal-goals.js +195 -0
- package/dist-server/server/port-in-use.js +21 -0
- package/dist-server/server/routes/local-models.js +3 -2
- package/dist-server/server/session-fallback.js +76 -0
- package/dist-server/server/skill-fetch.js +14 -0
- package/dist-server/server/vault/routes.js +43 -0
- package/dist-server/server/vault/runtime.js +48 -0
- package/dist-server/server/vault/store.js +177 -0
- package/dist-server/shared/desktop-private-port.js +1 -0
- package/dist-server/shared/engine-choices.js +15 -0
- package/dist-server/shared/engine-tiers.js +74 -2
- package/dist-server/shared/local-models.js +1 -3
- package/dist-server/shared/personal-goal.js +43 -0
- package/dist-server/shared/subscription-bots.js +3 -0
- package/dist-server/shared/vault.js +59 -0
- package/dist-server/vps-container-mcp.js +4 -2
- package/package.json +1 -1
- package/dist/assets/channel-GBaPOaz-.js +0 -1
- package/dist/assets/index-C8YP7Zxj.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CamSa0NE.js +0 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Download and install the two desktop apps the setup wizard offers, after
|
|
2
2
|
// the person clicks: Ollama (free local AI) and Podman (a separate computer
|
|
3
|
-
// for bots).
|
|
3
|
+
// for bots). Ollama installs on macOS, Windows and Linux; Podman on macOS.
|
|
4
4
|
//
|
|
5
5
|
// Every download is the vendor's own GitHub release asset, found through
|
|
6
6
|
// GitHub's release API, and is checked three ways before anything runs:
|
|
@@ -12,16 +12,22 @@
|
|
|
12
12
|
// ollama.com/download redirects to the same GitHub release asset.
|
|
13
13
|
//
|
|
14
14
|
// Ollama is unpacked with ditto, moved to /Applications (or ~/Applications
|
|
15
|
-
// when /Applications is not writable), and opened.
|
|
15
|
+
// when /Applications is not writable), and opened. On Windows its signed
|
|
16
|
+
// OllamaSetup.exe (Authenticode signer O=Ollama Inc., the check Ollama's own
|
|
17
|
+
// install.ps1 makes) runs silently into %LOCALAPPDATA%\Programs\Ollama, no
|
|
18
|
+
// administrator needed. On Linux, where Ollama ships no signature, the
|
|
19
|
+
// SHA-256 alone gates the tarball, unpacked into ~/.local (no sudo, no
|
|
20
|
+
// service): ~/.local/bin/ollama, started as `ollama serve`. Podman's package needs an
|
|
16
21
|
// administrator's approval, so Atomic opens it in Apple's Installer app,
|
|
17
22
|
// which asks for it; nobody types a command.
|
|
18
23
|
import { createHash } from "node:crypto";
|
|
19
|
-
import { accessSync, constants, createWriteStream, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync } from "node:fs";
|
|
24
|
+
import { accessSync, constants, createReadStream, createWriteStream, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync } from "node:fs";
|
|
20
25
|
import { homedir, tmpdir } from "node:os";
|
|
21
26
|
import { join } from "node:path";
|
|
22
27
|
import { Readable } from "node:stream";
|
|
23
28
|
import { pipeline } from "node:stream/promises";
|
|
24
|
-
import {
|
|
29
|
+
import { createZstdDecompress } from "node:zlib";
|
|
30
|
+
import { execFileCommand, spawnCommand } from "./procs.js";
|
|
25
31
|
export const APP_INSTALLERS = Object.freeze({
|
|
26
32
|
ollama: { repo: "ollama/ollama", asset: () => "Ollama-darwin.zip", teamId: "3MU9H2V9Y9", kind: "zip-app", appName: "Ollama.app" },
|
|
27
33
|
podman: {
|
|
@@ -58,7 +64,7 @@ export function runTool(command, args, timeoutMs = 120_000) {
|
|
|
58
64
|
export async function latestReleaseAsset(spec, arch, fetchImpl = fetch, signal) {
|
|
59
65
|
const name = spec.asset(arch);
|
|
60
66
|
if (!name)
|
|
61
|
-
throw new AppInstallError("unsupported", `No ${spec.repo} installer is published for ${arch}
|
|
67
|
+
throw new AppInstallError("unsupported", `No ${spec.repo} installer is published for ${arch} computers.`);
|
|
62
68
|
let response;
|
|
63
69
|
try {
|
|
64
70
|
response = await fetchImpl(`https://api.github.com/repos/${spec.repo}/releases/latest`, {
|
|
@@ -166,10 +172,104 @@ function writableDir(dir) {
|
|
|
166
172
|
return false;
|
|
167
173
|
}
|
|
168
174
|
}
|
|
175
|
+
/** Ollama's release asset on Windows and Linux. */
|
|
176
|
+
export const OLLAMA_WINDOWS_ASSET = "OllamaSetup.exe";
|
|
177
|
+
export const ollamaLinuxAsset = (arch) => (arch === "x64" ? "ollama-linux-amd64.tar.zst" : arch === "arm64" ? "ollama-linux-arm64.tar.zst" : null);
|
|
178
|
+
/** Where Atomic's Ollama lives off macOS: the per-user Windows install, or
|
|
179
|
+
* the ~/.local unpack on Linux. */
|
|
180
|
+
export function ollamaBinaryHere(platform = process.platform, env = process.env) {
|
|
181
|
+
if (platform === "win32")
|
|
182
|
+
return join(env.LOCALAPPDATA || join(homedir(), "AppData", "Local"), "Programs", "Ollama", "ollama.exe");
|
|
183
|
+
if (platform === "linux")
|
|
184
|
+
return join(homedir(), ".local", "bin", "ollama");
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
/** The Windows installer is signed by Ollama Inc. (Authenticode, valid). */
|
|
188
|
+
export async function verifyWindowsSignature(file, run = runTool) {
|
|
189
|
+
const script = `$s = Get-AuthenticodeSignature -LiteralPath '${file.replace(/'/g, "''")}'; "$($s.Status)|$($s.SignerCertificate.Subject)"`;
|
|
190
|
+
let stdout;
|
|
191
|
+
try {
|
|
192
|
+
({ stdout } = await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script]));
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
throw new AppInstallError("verify", `Windows could not check the installer's signature: ${String(error)}`);
|
|
196
|
+
}
|
|
197
|
+
const [status, subject = ""] = stdout.trim().split("|");
|
|
198
|
+
if (status !== "Valid" || !/(^|, )O=Ollama Inc\.(,|$)/.test(subject)) {
|
|
199
|
+
throw new AppInstallError("verify", `The installer is not signed by Ollama Inc. (${status}: ${subject}).`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function installOllamaWindows(deps, run) {
|
|
203
|
+
const asset = await latestReleaseAsset({ ...APP_INSTALLERS.ollama, asset: () => OLLAMA_WINDOWS_ASSET }, deps.arch ?? process.arch, deps.fetch, deps.signal);
|
|
204
|
+
const work = mkdtempSync(join(tmpdir(), "atomic-ollama-"));
|
|
205
|
+
try {
|
|
206
|
+
const setup = join(work, asset.name);
|
|
207
|
+
await downloadAsset(asset, setup, deps);
|
|
208
|
+
deps.onProgress?.({ phase: "verifying" });
|
|
209
|
+
await verifyWindowsSignature(setup, run);
|
|
210
|
+
deps.onProgress?.({ phase: "installing" });
|
|
211
|
+
// Per-user install; the installer opens the Ollama app when it is done.
|
|
212
|
+
await run(setup, ["/VERYSILENT", "/NORESTART", "/SUPPRESSMSGBOXES"]).catch((error) => {
|
|
213
|
+
throw new AppInstallError("install", `The Ollama installer stopped: ${String(error)}`);
|
|
214
|
+
});
|
|
215
|
+
deps.onProgress?.({ phase: "starting" });
|
|
216
|
+
return { appPath: ollamaBinaryHere("win32"), version: asset.version };
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
rmSync(work, { recursive: true, force: true });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function installOllamaLinux(deps, run) {
|
|
223
|
+
const asset = await latestReleaseAsset({ ...APP_INSTALLERS.ollama, asset: ollamaLinuxAsset }, deps.arch ?? process.arch, deps.fetch, deps.signal);
|
|
224
|
+
const work = mkdtempSync(join(tmpdir(), "atomic-ollama-"));
|
|
225
|
+
try {
|
|
226
|
+
const packed = join(work, asset.name);
|
|
227
|
+
await downloadAsset(asset, packed, deps);
|
|
228
|
+
deps.onProgress?.({ phase: "installing" });
|
|
229
|
+
const tar = join(work, "ollama.tar");
|
|
230
|
+
await pipeline(createReadStream(packed), createZstdDecompress(), createWriteStream(tar), { signal: deps.signal });
|
|
231
|
+
rmSync(packed, { force: true });
|
|
232
|
+
const prefix = join(homedir(), ".local");
|
|
233
|
+
mkdirSync(prefix, { recursive: true });
|
|
234
|
+
await run("tar", ["-xf", tar, "-C", prefix]).catch((error) => {
|
|
235
|
+
throw new AppInstallError("install", `Could not unpack Ollama into ${prefix}: ${String(error)}`);
|
|
236
|
+
});
|
|
237
|
+
deps.onProgress?.({ phase: "starting" });
|
|
238
|
+
startOllamaApp("linux");
|
|
239
|
+
return { appPath: ollamaBinaryHere("linux"), version: asset.version };
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
if (error instanceof AppInstallError)
|
|
243
|
+
throw error;
|
|
244
|
+
throw new AppInstallError(deps.signal?.aborted ? "cancelled" : "install", error instanceof Error ? error.message : String(error));
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
rmSync(work, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/** Start the installed Ollama the way each OS runs it: the app on macOS and
|
|
251
|
+
* Windows (it starts its own server), `ollama serve` on Linux. Returns at
|
|
252
|
+
* once; callers wait for it to answer. */
|
|
253
|
+
export function startOllamaApp(platform = process.platform, run = runTool) {
|
|
254
|
+
if (platform === "darwin") {
|
|
255
|
+
void run("/usr/bin/open", ["-a", "Ollama"]).catch(() => undefined);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const binary = ollamaBinaryHere(platform);
|
|
259
|
+
const command = platform === "win32" && binary ? join(binary, "..", "ollama app.exe") : existsSync(binary ?? "") ? binary : "ollama";
|
|
260
|
+
const child = spawnCommand(command, platform === "win32" ? [] : ["serve"], { detached: true, stdio: "ignore", windowsHide: true });
|
|
261
|
+
child.on("error", () => undefined);
|
|
262
|
+
child.unref();
|
|
263
|
+
}
|
|
169
264
|
/** Download, verify, install and open Ollama. Resolves with where it went. */
|
|
170
265
|
export async function installOllamaApp(deps = {}) {
|
|
171
|
-
|
|
172
|
-
|
|
266
|
+
const platform = deps.platform ?? process.platform;
|
|
267
|
+
if (platform === "win32" || platform === "linux") {
|
|
268
|
+
deps.onProgress?.({ phase: "finding" });
|
|
269
|
+
return (platform === "win32" ? installOllamaWindows : installOllamaLinux)(deps, deps.run ?? ((command, args) => runTool(command, args, 15 * 60_000)));
|
|
270
|
+
}
|
|
271
|
+
if (platform !== "darwin")
|
|
272
|
+
throw new AppInstallError("unsupported", `Atomic cannot install Ollama on ${platform}.`);
|
|
173
273
|
const spec = APP_INSTALLERS.ollama;
|
|
174
274
|
const run = deps.run ?? runTool;
|
|
175
275
|
deps.onProgress?.({ phase: "finding" });
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { decodeGeneratedImage } from "./generated-image.js";
|
|
2
|
+
import { avatarGenerationPrompt, AVATAR_IMAGE_TIMEOUT_MS } from "./avatar-image.js";
|
|
3
|
+
/** Capability and current billing are both required. An authenticated text
|
|
4
|
+
* engine is not evidence that its subscription can generate images. */
|
|
5
|
+
export async function avatarSubscription(instances) {
|
|
6
|
+
for (const instance of instances) {
|
|
7
|
+
if (!instance.enabled || !instance.generateImage)
|
|
8
|
+
continue;
|
|
9
|
+
try {
|
|
10
|
+
const status = await instance.snapshot();
|
|
11
|
+
if (status.state === "available" && status.authenticated === true &&
|
|
12
|
+
status.billing === "subscription" && status.account?.method !== "api-key")
|
|
13
|
+
return instance;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// A broken account must not conceal another eligible signed-in account.
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
/** Never turn a subscription failure into an unexpected API charge. */
|
|
22
|
+
export async function generateSubscriptionAvatar(instance, bot, direction, timeoutMs = AVATAR_IMAGE_TIMEOUT_MS) {
|
|
23
|
+
if (!instance.generateImage || await avatarSubscription([instance]) !== instance) {
|
|
24
|
+
throw Object.assign(new Error("This image subscription is no longer available. Check its sign-in in Settings."), { status: 409 });
|
|
25
|
+
}
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
let timer;
|
|
28
|
+
try {
|
|
29
|
+
const encoded = await Promise.race([
|
|
30
|
+
instance.generateImage(avatarGenerationPrompt(bot, direction), { signal: controller.signal }),
|
|
31
|
+
new Promise((_, reject) => {
|
|
32
|
+
timer = setTimeout(() => {
|
|
33
|
+
controller.abort();
|
|
34
|
+
reject(new Error("timeout"));
|
|
35
|
+
}, timeoutMs);
|
|
36
|
+
}),
|
|
37
|
+
]);
|
|
38
|
+
const image = decodeGeneratedImage(encoded);
|
|
39
|
+
if (image.mime === "image/gif")
|
|
40
|
+
throw new Error("unsupported avatar format");
|
|
41
|
+
return { bytes: image.bytes, mime: image.mime };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw Object.assign(new Error(controller.signal.aborted
|
|
45
|
+
? "Subscription avatar generation timed out. Try again."
|
|
46
|
+
: "Your subscription could not generate this avatar. Check its sign-in and usage allowance, then try again."), { status: 502 });
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export const USAGE = `chibi — your team of AI bots, ready in a few steps
|
|
2
|
+
|
|
3
|
+
chibi set up once, then open your workspace
|
|
4
|
+
chibi setup [--data-dir DIR]
|
|
5
|
+
chibi start [the same options as serve]
|
|
6
|
+
chibi serve [--port 8799] [--data-dir DIR] [--label NAME]
|
|
7
|
+
[--public-url https://host] [--tailscale | --tunnel | --domain HOST] [--no-pair]
|
|
8
|
+
chibi pair [--label NAME] [--client] [--phone ios|android]
|
|
9
|
+
[--public-url https://host]
|
|
10
|
+
chibi sessions [revoke ID]
|
|
11
|
+
chibi status
|
|
12
|
+
chibi login [--email you@example.com]
|
|
13
|
+
chibi logout
|
|
14
|
+
chibi command --bot ID|--group ID --thread ID --text "/goal status" [--port 8799]
|
|
15
|
+
chibi goal --bot ID --thread ID --text "--tokens 100000 objective"
|
|
16
|
+
chibi access list | add EMAIL [--chat-only] | remove EMAIL
|
|
17
|
+
chibi service install [--domain HOST | --tunnel | --tailscale] [--port N] [--data-dir DIR] | uninstall
|
|
18
|
+
chibi browser install [--with-deps] | status
|
|
19
|
+
chibi fleet init --domain HOST [--operator USER] | create NAME --admin EMAIL [--member EMAIL] [--brand FILE]
|
|
20
|
+
[--anthropic-key-file FILE] [--cap USD] [--license-key KEY] [--memory 1G]
|
|
21
|
+
| list | users NAME add|remove EMAIL [--chat-only] | suspend NAME | resume NAME
|
|
22
|
+
| delete NAME --yes [--keep-data] | upgrade (all take --dry-run)
|
|
23
|
+
| agent [--socket PATH] [--group USER] (root; installed by init --operator)
|
|
24
|
+
|
|
25
|
+
setup choose AI access and optional phone access; keep existing bots and chats
|
|
26
|
+
start same as chibi: use your saved settings and open the workspace
|
|
27
|
+
serve starts the server without prompts and prints a pairing link + QR code
|
|
28
|
+
pair mints a pairing code against a running server (--client: chat only)
|
|
29
|
+
sessions lists paired devices; "sessions revoke ID" signs one out
|
|
30
|
+
status what the server says about itself
|
|
31
|
+
login signs this machine in to an Atomic account (an emailed code)
|
|
32
|
+
and reserves its public address for --tunnel
|
|
33
|
+
logout releases that address and signs out
|
|
34
|
+
access who may sign in with an emailed code at /pair: an address or
|
|
35
|
+
@domain; --chat-only gives chat and approvals without settings.
|
|
36
|
+
Takes effect at once, no restart.
|
|
37
|
+
service keep the server running across reboots: writes a systemd unit
|
|
38
|
+
(Linux) or a launchd agent (macOS) for the same serve options and
|
|
39
|
+
prints the commands that install it. Install the package
|
|
40
|
+
permanently first (npm install -g @chibitek/chibi).
|
|
41
|
+
browser install: the bots' browser engine (agent-browser, pinned) into the
|
|
42
|
+
data dir, and Chrome for Testing into the user's browser cache.
|
|
43
|
+
--with-deps also installs
|
|
44
|
+
the Linux libraries Chrome needs (run as root once). Then run
|
|
45
|
+
browser install as the user running serve, from that user's home.
|
|
46
|
+
status: what the current user and data directory have.
|
|
47
|
+
fleet many client workspaces on one Linux server, each its own account,
|
|
48
|
+
service, data folder, brand, sign-in list and keys at NAME.HOST
|
|
49
|
+
behind the system Caddy. Plans are printed unless run as root;
|
|
50
|
+
--dry-run always prints. Install the package permanently first.
|
|
51
|
+
init --operator USER also installs the fleet agent, a root service
|
|
52
|
+
on a Unix socket only USER may open, so the workspace running as
|
|
53
|
+
USER manages the others from Settings → Workspaces.
|
|
54
|
+
|
|
55
|
+
--tailscale serve over your tailnet: Tailscale terminates HTTPS and the
|
|
56
|
+
link uses this machine's MagicDNS name (needs Tailscale signed in
|
|
57
|
+
and HTTPS certificates enabled for the tailnet)
|
|
58
|
+
--tunnel serve at a public HTTPS address through a Cloudflare tunnel
|
|
59
|
+
from a hosted control plane (set OMB_CONTROL_PLANE_URL; Atomic
|
|
60
|
+
ships none). Run \`chibi login\` once on this machine first.
|
|
61
|
+
--domain serve at https://HOST on your own domain: a pinned Caddy is
|
|
62
|
+
downloaded once and run alongside the server, and gets the
|
|
63
|
+
certificate itself. Point the domain's DNS at this machine and
|
|
64
|
+
open ports 80 and 443.
|
|
65
|
+
|
|
66
|
+
--no-open do not open a browser window
|
|
67
|
+
--no-pair skip phone setup and do not print a pairing code
|
|
68
|
+
--local start locally this time, ignoring saved remote-access settings
|
|
69
|
+
--quiet skip the welcome banner (it never shows when output is piped;
|
|
70
|
+
NO_COLOR=1 shows it without color)
|
|
71
|
+
|
|
72
|
+
Install once with \`npm install -g @chibitek/chibi\`, then type \`chibi\`.
|
|
73
|
+
Or run without a global install: \`npx @chibitek/chibi\`. Node 24+ is required.
|
|
74
|
+
`;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { USAGE } from "./cli-help.js";
|
|
2
|
+
export { USAGE } from "./cli-help.js";
|
|
1
3
|
// `chibi` on the command line: run the server anywhere and pair devices
|
|
2
4
|
// to it. One implementation for three homes — `npx @chibitek/chibi` (the npm
|
|
3
5
|
// package), `node dist-server/cli.js` (the container image) and
|
|
@@ -38,6 +40,7 @@ import { resolveLoopbackTrust } from "./request-auth.js";
|
|
|
38
40
|
import { writeFileAtomic } from "./atomic.js";
|
|
39
41
|
import { ensureCaddy, normalizeDomainOption, startCaddy } from "./caddy.js";
|
|
40
42
|
import { runServiceCommand } from "./service-cli.js";
|
|
43
|
+
import { reportBusyPort } from "./port-in-use.js";
|
|
41
44
|
import { colorDepth, renderBanner, shouldShowBanner, terminalBackground } from "./cli-banner.js";
|
|
42
45
|
import { runFleetCommand } from "./fleet-cli.js";
|
|
43
46
|
import { startFleetAgent } from "./fleet-agent.js";
|
|
@@ -47,7 +50,7 @@ import { defaultSetupIo, SetupCancelled } from "./cli-prompts.js";
|
|
|
47
50
|
import { normalizePhoneOrigin, phonePairingInstructions, runPhoneSetup } from "./cli-phone-setup.js";
|
|
48
51
|
import { cleanupTunnelOrigin, createTunnelAccount, createTunnelOrigin, describeTunnelAccount, describeTunnelState, ensureCloudflared, FLEET_CREDENTIAL_ENV, fleetAccess, fleetCredential, guardianEntry, startTunnel, tunnelAccess, } from "./tunnel.js";
|
|
49
52
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
50
|
-
export const COMMANDS = ["setup", "start", "serve", "pair", "sessions", "status", "login", "logout", "access", "service", "browser", "fleet", "help", "--help", "-h"];
|
|
53
|
+
export const COMMANDS = ["command", "goal", "setup", "start", "serve", "pair", "sessions", "status", "login", "logout", "access", "service", "browser", "fleet", "help", "--help", "-h"];
|
|
51
54
|
export function parseArgs(argv, env = process.env) {
|
|
52
55
|
const implicitStart = !argv.length || (argv[0].startsWith("--") && argv[0] !== "--help");
|
|
53
56
|
const [command = "start", ...rest] = implicitStart ? ["start", ...argv] : argv;
|
|
@@ -76,7 +79,15 @@ export function parseArgs(argv, env = process.env) {
|
|
|
76
79
|
return v;
|
|
77
80
|
};
|
|
78
81
|
try {
|
|
79
|
-
if (arg === "--
|
|
82
|
+
if ((options.command === "command" || options.command === "goal") && arg === "--bot")
|
|
83
|
+
options.botId = value();
|
|
84
|
+
else if ((options.command === "command" || options.command === "goal") && arg === "--group")
|
|
85
|
+
options.groupId = value();
|
|
86
|
+
else if ((options.command === "command" || options.command === "goal") && arg === "--thread")
|
|
87
|
+
options.threadId = value();
|
|
88
|
+
else if ((options.command === "command" || options.command === "goal") && arg === "--text")
|
|
89
|
+
options.commandText = value();
|
|
90
|
+
else if (arg === "--port")
|
|
80
91
|
options.port = Number(value());
|
|
81
92
|
else if (arg === "--data-dir")
|
|
82
93
|
options.dataDir = resolve(value());
|
|
@@ -182,6 +193,8 @@ export function parseArgs(argv, env = process.env) {
|
|
|
182
193
|
return { error: "--public-url must start with http:// or https://" };
|
|
183
194
|
if (options.tailscale && options.tunnel)
|
|
184
195
|
return { error: "choose one of --tailscale (your tailnet) and --tunnel (a public address)" };
|
|
196
|
+
if ((options.command === "command" || options.command === "goal") && (Boolean(options.botId) === Boolean(options.groupId) || !options.threadId || !options.commandText))
|
|
197
|
+
return { error: "command and goal require exactly one of --bot ID or --group ID, plus --thread ID --text TEXT" };
|
|
185
198
|
if (options.command === "access" && !options.accessAction)
|
|
186
199
|
return { error: "access needs one of: list, add EMAIL [--chat-only], remove EMAIL" };
|
|
187
200
|
if (options.command === "service" && !options.serviceAction)
|
|
@@ -204,78 +217,6 @@ export function parseArgs(argv, env = process.env) {
|
|
|
204
217
|
}
|
|
205
218
|
return options;
|
|
206
219
|
}
|
|
207
|
-
export const USAGE = `chibi — your team of AI bots, ready in a few steps
|
|
208
|
-
|
|
209
|
-
chibi set up once, then open your workspace
|
|
210
|
-
chibi setup [--data-dir DIR]
|
|
211
|
-
chibi start [the same options as serve]
|
|
212
|
-
chibi serve [--port 8799] [--data-dir DIR] [--label NAME]
|
|
213
|
-
[--public-url https://host] [--tailscale | --tunnel | --domain HOST] [--no-pair]
|
|
214
|
-
chibi pair [--label NAME] [--client] [--phone ios|android]
|
|
215
|
-
[--public-url https://host]
|
|
216
|
-
chibi sessions [revoke ID]
|
|
217
|
-
chibi status
|
|
218
|
-
chibi login [--email you@example.com]
|
|
219
|
-
chibi logout
|
|
220
|
-
chibi access list | add EMAIL [--chat-only] | remove EMAIL
|
|
221
|
-
chibi service install [--domain HOST | --tunnel | --tailscale] [--port N] [--data-dir DIR] | uninstall
|
|
222
|
-
chibi browser install [--with-deps] | status
|
|
223
|
-
chibi fleet init --domain HOST [--operator USER] | create NAME --admin EMAIL [--member EMAIL] [--brand FILE]
|
|
224
|
-
[--anthropic-key-file FILE] [--cap USD] [--license-key KEY] [--memory 1G]
|
|
225
|
-
| list | users NAME add|remove EMAIL [--chat-only] | suspend NAME | resume NAME
|
|
226
|
-
| delete NAME --yes [--keep-data] | upgrade (all take --dry-run)
|
|
227
|
-
| agent [--socket PATH] [--group USER] (root; installed by init --operator)
|
|
228
|
-
|
|
229
|
-
setup choose AI access and optional phone access; keep existing bots and chats
|
|
230
|
-
start same as chibi: use your saved settings and open the workspace
|
|
231
|
-
serve starts the server without prompts and prints a pairing link + QR code
|
|
232
|
-
pair mints a pairing code against a running server (--client: chat only)
|
|
233
|
-
sessions lists paired devices; "sessions revoke ID" signs one out
|
|
234
|
-
status what the server says about itself
|
|
235
|
-
login signs this machine in to an Atomic account (an emailed code)
|
|
236
|
-
and reserves its public address for --tunnel
|
|
237
|
-
logout releases that address and signs out
|
|
238
|
-
access who may sign in with an emailed code at /pair: an address or
|
|
239
|
-
@domain; --chat-only gives chat and approvals without settings.
|
|
240
|
-
Takes effect at once, no restart.
|
|
241
|
-
service keep the server running across reboots: writes a systemd unit
|
|
242
|
-
(Linux) or a launchd agent (macOS) for the same serve options and
|
|
243
|
-
prints the commands that install it. Install the package
|
|
244
|
-
permanently first (npm install -g @chibitek/chibi).
|
|
245
|
-
browser install: the bots' browser engine (agent-browser, pinned) into the
|
|
246
|
-
data dir, and Chrome for Testing into the user's browser cache.
|
|
247
|
-
--with-deps also installs
|
|
248
|
-
the Linux libraries Chrome needs (run as root once). Then run
|
|
249
|
-
browser install as the user running serve, from that user's home.
|
|
250
|
-
status: what the current user and data directory have.
|
|
251
|
-
fleet many client workspaces on one Linux server, each its own account,
|
|
252
|
-
service, data folder, brand, sign-in list and keys at NAME.HOST
|
|
253
|
-
behind the system Caddy. Plans are printed unless run as root;
|
|
254
|
-
--dry-run always prints. Install the package permanently first.
|
|
255
|
-
init --operator USER also installs the fleet agent, a root service
|
|
256
|
-
on a Unix socket only USER may open, so the workspace running as
|
|
257
|
-
USER manages the others from Settings → Workspaces.
|
|
258
|
-
|
|
259
|
-
--tailscale serve over your tailnet: Tailscale terminates HTTPS and the
|
|
260
|
-
link uses this machine's MagicDNS name (needs Tailscale signed in
|
|
261
|
-
and HTTPS certificates enabled for the tailnet)
|
|
262
|
-
--tunnel serve at a public HTTPS address through a Cloudflare tunnel
|
|
263
|
-
from a hosted control plane (set OMB_CONTROL_PLANE_URL; Atomic
|
|
264
|
-
ships none). Run \`chibi login\` once on this machine first.
|
|
265
|
-
--domain serve at https://HOST on your own domain: a pinned Caddy is
|
|
266
|
-
downloaded once and run alongside the server, and gets the
|
|
267
|
-
certificate itself. Point the domain's DNS at this machine and
|
|
268
|
-
open ports 80 and 443.
|
|
269
|
-
|
|
270
|
-
--no-open do not open a browser window
|
|
271
|
-
--no-pair skip phone setup and do not print a pairing code
|
|
272
|
-
--local start locally this time, ignoring saved remote-access settings
|
|
273
|
-
--quiet skip the welcome banner (it never shows when output is piped;
|
|
274
|
-
NO_COLOR=1 shows it without color)
|
|
275
|
-
|
|
276
|
-
Install once with \`npm install -g @chibitek/chibi\`, then type \`chibi\`.
|
|
277
|
-
Or run without a global install: \`npx @chibitek/chibi\`. Node 24+ is required.
|
|
278
|
-
`;
|
|
279
220
|
export function defaultIo() {
|
|
280
221
|
return {
|
|
281
222
|
log: (line) => console.log(line),
|
|
@@ -890,6 +831,8 @@ export async function runServe(options, log = console.log) {
|
|
|
890
831
|
console.error(`something already answers on http://127.0.0.1:${options.port}; use \`chibi pair\` against it, or --port for a second server`);
|
|
891
832
|
return 1;
|
|
892
833
|
}
|
|
834
|
+
if (await reportBusyPort(options.port, options.dataDir === join(homedir(), ".atomic-app") ? "" : ` --data-dir ${options.dataDir}`))
|
|
835
|
+
return 1;
|
|
893
836
|
let publicUrl = options.publicUrl;
|
|
894
837
|
let tailscale = null;
|
|
895
838
|
if (options.tailscale) {
|
|
@@ -1191,10 +1134,8 @@ export async function runOnboardingCommand(options, io = defaultIo(), startServe
|
|
|
1191
1134
|
saveCliStartup(options.dataDir, startupPreferences(launch));
|
|
1192
1135
|
}
|
|
1193
1136
|
if (options.command === "setup") {
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
io.log(`Use the same --data-dir (${options.dataDir}) and --port (${options.port}) options when starting.`);
|
|
1197
|
-
}
|
|
1137
|
+
const flags = [options.dataDir !== join(homedir(), ".atomic-app") && ` --data-dir ${options.dataDir}`, options.port !== 8799 && ` --port ${options.port}`].filter(Boolean).join("");
|
|
1138
|
+
io.log(`\nAll set. Start with: chibi${flags ? ` start${flags}` : ""} (or npx @chibitek/chibi without a global install).`);
|
|
1198
1139
|
return 0;
|
|
1199
1140
|
}
|
|
1200
1141
|
if (saved)
|
|
@@ -1232,6 +1173,24 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1232
1173
|
console.log(`\n${welcomeBanner(options)}\n`);
|
|
1233
1174
|
}
|
|
1234
1175
|
switch (options.command) {
|
|
1176
|
+
case "command":
|
|
1177
|
+
case "goal": {
|
|
1178
|
+
const text = options.command === "goal" ? `/goal ${options.commandText}` : options.commandText;
|
|
1179
|
+
const roomGoal = options.groupId ? /^\/goal(?:\s+([\s\S]*))?$/.exec(text ?? "") : null;
|
|
1180
|
+
if (roomGoal && (!roomGoal[1]?.trim() || (roomGoal[1].startsWith("--") || /^(status|pause|resume|stop|help)(?:\s|$)/.test(roomGoal[1])))) {
|
|
1181
|
+
console.error("Room goals accept an objective. Personal goal controls and budgets require --bot.");
|
|
1182
|
+
return 2;
|
|
1183
|
+
}
|
|
1184
|
+
const result = await api(options.port, `/api/${options.groupId ? "groups" : "bots"}/${encodeURIComponent(options.groupId ?? options.botId)}/messages`, {
|
|
1185
|
+
method: "POST", body: JSON.stringify({ threadId: options.threadId, text: roomGoal ? roomGoal[1] : text, ...(roomGoal ? { mode: "goal" } : {}), sendId: randomBytes(16).toString("hex") }),
|
|
1186
|
+
});
|
|
1187
|
+
if (result.status >= 400) {
|
|
1188
|
+
console.error(result.body.error ?? `Server returned ${result.status}`);
|
|
1189
|
+
return 1;
|
|
1190
|
+
}
|
|
1191
|
+
console.log(options.json ? JSON.stringify(result.body) : result.body.goalSummary ?? JSON.stringify(result.body));
|
|
1192
|
+
return 0;
|
|
1193
|
+
}
|
|
1235
1194
|
case "setup":
|
|
1236
1195
|
case "start":
|
|
1237
1196
|
return runOnboardingCommand(options);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Colima is the only container engine Atomic starts or talks to for the
|
|
2
|
+
// Local VM on macOS. Docker Desktop is never launched, and a docker context
|
|
3
|
+
// it leaves behind (desktop-linux) must not decide where `docker` commands
|
|
4
|
+
// go, so every docker invocation Atomic makes on a Mac is pinned to the
|
|
5
|
+
// Colima profile's socket through DOCKER_HOST.
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
export const COLIMA_DEFAULT_PROFILE = "default";
|
|
10
|
+
const readText = (path) => {
|
|
11
|
+
try {
|
|
12
|
+
return readFileSync(path, "utf8");
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
function homeOf(env) {
|
|
19
|
+
return env.HOME?.trim() || homedir();
|
|
20
|
+
}
|
|
21
|
+
/** The Colima profile Atomic uses: the one named by a `colima-<profile>`
|
|
22
|
+
* docker context (DOCKER_CONTEXT, then ~/.docker/config.json), else the
|
|
23
|
+
* default profile. Any other context, Docker Desktop's included, is ignored:
|
|
24
|
+
* it never redirects Atomic away from Colima. Reads a file only; never runs
|
|
25
|
+
* docker, since docker itself is what this pins. */
|
|
26
|
+
export function colimaProfile(env = process.env, read = readText) {
|
|
27
|
+
let context = env.DOCKER_CONTEXT?.trim() ?? "";
|
|
28
|
+
if (!context) {
|
|
29
|
+
const configDir = env.DOCKER_CONFIG?.trim() || join(homeOf(env), ".docker");
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(read(join(configDir, "config.json")) ?? "{}");
|
|
32
|
+
context = typeof parsed.currentContext === "string" ? parsed.currentContext.trim() : "";
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
context = "";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (context.startsWith("colima-") && /^[A-Za-z0-9_.-]+$/.test(context.slice("colima-".length))) {
|
|
39
|
+
return context.slice("colima-".length);
|
|
40
|
+
}
|
|
41
|
+
return COLIMA_DEFAULT_PROFILE;
|
|
42
|
+
}
|
|
43
|
+
/** Colima's docker socket for a profile. COLIMA_HOME wins; otherwise
|
|
44
|
+
* ~/.colima, or ~/.config/colima when only that one holds the profile. */
|
|
45
|
+
export function colimaDockerSocket(profile = COLIMA_DEFAULT_PROFILE, env = process.env, exists = existsSync) {
|
|
46
|
+
const explicit = env.COLIMA_HOME?.trim();
|
|
47
|
+
if (explicit)
|
|
48
|
+
return join(explicit, profile, "docker.sock");
|
|
49
|
+
const home = homeOf(env);
|
|
50
|
+
const legacy = join(home, ".colima");
|
|
51
|
+
const xdg = join(env.XDG_CONFIG_HOME?.trim() || join(home, ".config"), "colima");
|
|
52
|
+
const base = !exists(join(legacy, profile)) && exists(join(xdg, profile)) ? xdg : legacy;
|
|
53
|
+
return join(base, profile, "docker.sock");
|
|
54
|
+
}
|
|
55
|
+
export function colimaDockerHost(profile = COLIMA_DEFAULT_PROFILE, env = process.env, exists) {
|
|
56
|
+
return `unix://${colimaDockerSocket(profile, env, exists)}`;
|
|
57
|
+
}
|
|
58
|
+
/** The environment for one `docker` invocation. On macOS it always targets
|
|
59
|
+
* Colima: DOCKER_HOST is the Colima socket and DOCKER_CONTEXT is dropped (the
|
|
60
|
+
* CLI refuses both at once). Elsewhere the environment is returned as is. */
|
|
61
|
+
export function dockerEnvForColima(env = process.env, platform = process.platform, options = {}) {
|
|
62
|
+
if (platform !== "darwin")
|
|
63
|
+
return env;
|
|
64
|
+
const profile = colimaProfile(env, options.read);
|
|
65
|
+
const pinned = { ...env, DOCKER_HOST: colimaDockerHost(profile, env, options.exists) };
|
|
66
|
+
delete pinned.DOCKER_CONTEXT;
|
|
67
|
+
return pinned;
|
|
68
|
+
}
|
|
69
|
+
const COLIMA_START_TIMEOUT_MS = 4 * 60_000;
|
|
70
|
+
const COLIMA_STOP_TIMEOUT_MS = 60_000;
|
|
71
|
+
/** Colima on the profile the docker CLI is pinned to. `colima start` on a
|
|
72
|
+
* running profile is a no-op, so an instance the person started with their
|
|
73
|
+
* own flags is left exactly as it is. */
|
|
74
|
+
export function colimaStartPlan(env = process.env) {
|
|
75
|
+
const profile = colimaProfile(env);
|
|
76
|
+
const profileArgs = profile === COLIMA_DEFAULT_PROFILE ? [] : ["--profile", profile];
|
|
77
|
+
return {
|
|
78
|
+
label: "Colima",
|
|
79
|
+
profile,
|
|
80
|
+
start: { command: "colima", args: ["start", ...profileArgs], timeoutMs: COLIMA_START_TIMEOUT_MS },
|
|
81
|
+
stop: { command: "colima", args: ["stop", ...profileArgs], timeoutMs: COLIMA_STOP_TIMEOUT_MS },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Bring docker back on Colima: start and wait for `docker info`; if it
|
|
85
|
+
* does not answer, start again (retry); then restart the profile (`colima
|
|
86
|
+
* stop`, `colima start`); then give up with a clear error. Without a plan
|
|
87
|
+
* (Colima not installed) it says so on a Mac. Docker Desktop is never
|
|
88
|
+
* opened on any path. */
|
|
89
|
+
export async function startColima(plan, before, deps) {
|
|
90
|
+
if (!plan) {
|
|
91
|
+
return {
|
|
92
|
+
status: before,
|
|
93
|
+
started: null,
|
|
94
|
+
error: deps.platform === "darwin"
|
|
95
|
+
? "Docker is not answering and Colima is not installed. Atomic runs the Local VM on Colima only and never opens Docker Desktop: install Colima (brew install colima), then try again."
|
|
96
|
+
: null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
|
|
100
|
+
const quietly = async (step) => {
|
|
101
|
+
// Colima can exit non-zero while its VM is still coming up, and the wait
|
|
102
|
+
// below is the real test; a launch that truly failed times out.
|
|
103
|
+
await deps.runner(step.command, step.args, step.timeoutMs).catch(() => undefined);
|
|
104
|
+
};
|
|
105
|
+
const waitUntilUp = async (ms) => {
|
|
106
|
+
const deadline = Date.now() + ms;
|
|
107
|
+
let status = await deps.status();
|
|
108
|
+
while (!status.daemonUp && Date.now() < deadline) {
|
|
109
|
+
await sleep(deps.pollMs ?? 2000);
|
|
110
|
+
status = await deps.status();
|
|
111
|
+
}
|
|
112
|
+
return status;
|
|
113
|
+
};
|
|
114
|
+
await quietly(plan.start);
|
|
115
|
+
let status = await waitUntilUp(deps.waitMs ?? 120_000);
|
|
116
|
+
if (!status.daemonUp) {
|
|
117
|
+
// A VM still booting often answers late.
|
|
118
|
+
await quietly(plan.start);
|
|
119
|
+
status = await waitUntilUp(deps.retryWaitMs ?? 30_000);
|
|
120
|
+
}
|
|
121
|
+
if (!status.daemonUp) {
|
|
122
|
+
// A profile that reads as running but whose socket is dead only comes
|
|
123
|
+
// back after a stop.
|
|
124
|
+
await quietly(plan.stop);
|
|
125
|
+
await quietly(plan.start);
|
|
126
|
+
status = await waitUntilUp(deps.waitMs ?? 120_000);
|
|
127
|
+
}
|
|
128
|
+
const profileFlag = plan.profile === COLIMA_DEFAULT_PROFILE ? "" : ` --profile ${plan.profile}`;
|
|
129
|
+
return {
|
|
130
|
+
status,
|
|
131
|
+
started: plan.label,
|
|
132
|
+
error: status.daemonUp
|
|
133
|
+
? null
|
|
134
|
+
: `Colima (profile ${plan.profile}) did not answer after a start, a retry and a restart. Run \`colima status${profileFlag}\` to see why; Atomic does not fall back to Docker Desktop.`,
|
|
135
|
+
};
|
|
136
|
+
}
|