hive-am 1.0.0 → 1.2.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 +114 -1
- package/README.md +34 -4
- package/package.json +13 -2
- package/server/dist/api.js +42 -677
- package/server/dist/connections/addressing.js +87 -0
- package/server/dist/connections/channel-skill.js +62 -0
- package/server/dist/connections/fake.js +1 -0
- package/server/dist/connections/files.js +45 -1
- package/server/dist/connections/format.js +9 -1
- package/server/dist/connections/manager.js +60 -3
- package/server/dist/connections/outbound.js +2 -1
- package/server/dist/connections/prompt.js +12 -3
- package/server/dist/connections/rate.js +20 -0
- package/server/dist/connections/router.js +55 -22
- package/server/dist/connections/secrets.js +52 -0
- package/server/dist/connections/store.js +6 -43
- package/server/dist/connections/telegram.js +105 -14
- package/server/dist/cron.js +138 -0
- package/server/dist/db/json.js +16 -0
- package/server/dist/db/migrations.js +199 -0
- package/server/dist/db.js +98 -196
- package/server/dist/errors.js +11 -0
- package/server/dist/git/browse.js +99 -0
- package/server/dist/git/ops.js +136 -19
- package/server/dist/git/repo.js +2 -1
- package/server/dist/history/claude.js +33 -20
- package/server/dist/history/index.js +17 -9
- package/server/dist/history/kiro.js +17 -12
- package/server/dist/history/lines.js +12 -0
- package/server/dist/history/opencode.js +18 -1
- package/server/dist/http/body.js +46 -0
- package/server/dist/http/errors.js +21 -0
- package/server/dist/http/guards.js +44 -0
- package/server/dist/http/router.js +24 -0
- package/server/dist/http/routes/agents.js +103 -0
- package/server/dist/http/routes/channel.js +16 -0
- package/server/dist/http/routes/colonies.js +9 -0
- package/server/dist/http/routes/connections.js +36 -0
- package/server/dist/http/routes/fs.js +22 -0
- package/server/dist/http/routes/git.js +90 -0
- package/server/dist/http/routes/meta.js +11 -0
- package/server/dist/http/routes/notebook.js +34 -0
- package/server/dist/http/routes/orchestration.js +32 -0
- package/server/dist/http/routes/resources.js +68 -0
- package/server/dist/http/routes/schedules.js +40 -0
- package/server/dist/http/routes/skills.js +47 -0
- package/server/dist/http/routes/terminals.js +18 -0
- package/server/dist/http/routes/types.js +36 -0
- package/server/dist/http/validate.js +65 -0
- package/server/dist/index.js +74 -4
- package/server/dist/instructions.js +23 -11
- package/server/dist/objects/agent.js +49 -0
- package/server/dist/objects/database.js +451 -0
- package/server/dist/objects/db-history.js +79 -0
- package/server/dist/objects/docker-info.js +75 -0
- package/server/dist/objects/docker.js +285 -0
- package/server/dist/objects/driver-props.js +196 -0
- package/server/dist/objects/exec.js +16 -0
- package/server/dist/objects/http-history.js +44 -0
- package/server/dist/objects/http-script.js +644 -0
- package/server/dist/objects/http.js +735 -0
- package/server/dist/objects/index.js +445 -0
- package/server/dist/objects/model.js +250 -0
- package/server/dist/objects/runner.js +211 -0
- package/server/dist/objects/stats.js +106 -0
- package/server/dist/objects/store.js +115 -0
- package/server/dist/objects/supervisor.js +111 -0
- package/server/dist/objects/templates.js +67 -0
- package/server/dist/providers/claude.js +5 -13
- package/server/dist/providers/index.js +7 -1
- package/server/dist/providers/kiro.js +4 -5
- package/server/dist/providers/opencode-server.js +164 -0
- package/server/dist/providers/opencode.js +60 -37
- package/server/dist/providers/preamble.js +2 -0
- package/server/dist/providers/spawn.js +31 -3
- package/server/dist/runtime.js +10 -7
- package/server/dist/schedules.js +222 -0
- package/server/dist/seed.js +15 -11
- package/server/dist/services/agents.js +90 -0
- package/server/dist/services/colonies.js +38 -0
- package/server/dist/services/connections.js +76 -0
- package/server/dist/services/notebook.js +22 -0
- package/server/dist/services/providers.js +27 -0
- package/server/dist/services/schedules.js +18 -0
- package/server/dist/sessions-summary.js +51 -0
- package/server/dist/skills/defaults.js +120 -0
- package/server/dist/skills/notebook.js +3 -8
- package/server/dist/terminals.js +177 -0
- package/server/dist/wake.js +149 -0
- package/server/dist/watch.js +201 -0
- package/server/mcp/dispatch.mjs +169 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/app-build-manifest.json +145 -85
- package/web/.next/app-path-routes-manifest.json +5 -2
- package/web/.next/build-manifest.json +14 -14
- package/web/.next/prerender-manifest.json +85 -37
- package/web/.next/react-loadable-manifest.json +26 -1
- package/web/.next/required-server-files.json +13 -1
- package/web/.next/routes-manifest.json +32 -0
- package/web/.next/server/app/_not-found/page.js +2 -2
- package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/server/app/_not-found.rsc +12 -11
- package/web/.next/server/app/agents/[id]/page.js +4 -4
- package/web/.next/server/app/agents/[id]/page.js.nft.json +1 -1
- package/web/.next/server/app/agents/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/agents/page.js +2 -2
- package/web/.next/server/app/agents/page.js.nft.json +1 -1
- package/web/.next/server/app/agents/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/agents.html +1 -1
- package/web/.next/server/app/agents.rsc +14 -13
- package/web/.next/server/app/connections/page.js +2 -2
- package/web/.next/server/app/connections/page.js.nft.json +1 -1
- package/web/.next/server/app/connections/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/connections.html +1 -1
- package/web/.next/server/app/connections.rsc +14 -13
- package/web/.next/server/app/index.html +1 -1
- package/web/.next/server/app/index.rsc +14 -13
- package/web/.next/server/app/page.js +2 -2
- package/web/.next/server/app/page.js.nft.json +1 -1
- package/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/relations/page.js +2 -2
- package/web/.next/server/app/relations/page.js.nft.json +1 -1
- package/web/.next/server/app/relations/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/relations.html +1 -1
- package/web/.next/server/app/relations.rsc +14 -13
- package/web/.next/server/app/resources/[id]/page.js +2 -0
- package/web/.next/server/app/resources/[id]/page.js.nft.json +1 -0
- package/web/.next/server/app/resources/[id]/page_client-reference-manifest.js +1 -0
- package/web/.next/server/app/resources/page.js +2 -0
- package/web/.next/server/app/resources/page.js.nft.json +1 -0
- package/web/.next/server/app/resources/page_client-reference-manifest.js +1 -0
- package/web/.next/server/app/resources.html +1 -0
- package/web/.next/server/app/resources.meta +15 -0
- package/web/.next/server/app/resources.rsc +25 -0
- package/web/.next/server/app/schedules/page.js +2 -0
- package/web/.next/server/app/schedules/page.js.nft.json +1 -0
- package/web/.next/server/app/schedules/page_client-reference-manifest.js +1 -0
- package/web/.next/server/app/schedules.html +1 -0
- package/web/.next/server/app/schedules.meta +15 -0
- package/web/.next/server/app/schedules.rsc +25 -0
- package/web/.next/server/app/sessions/page.js +2 -2
- package/web/.next/server/app/sessions/page.js.nft.json +1 -1
- package/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/sessions.html +1 -1
- package/web/.next/server/app/sessions.rsc +14 -13
- package/web/.next/server/app/skills/page.js +2 -2
- package/web/.next/server/app/skills/page.js.nft.json +1 -1
- package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/skills.html +1 -1
- package/web/.next/server/app/skills.rsc +14 -13
- package/web/.next/server/app/types/page.js +2 -2
- package/web/.next/server/app/types/page.js.nft.json +1 -1
- package/web/.next/server/app/types/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/types.html +1 -1
- package/web/.next/server/app/types.rsc +14 -13
- package/web/.next/server/app-paths-manifest.json +5 -2
- package/web/.next/server/chunks/143.js +1 -1
- package/web/.next/server/chunks/274.js +1 -0
- package/web/.next/server/chunks/306.js +1 -0
- package/web/.next/server/chunks/423.js +1 -0
- package/web/.next/server/chunks/505.js +6 -0
- package/web/.next/server/chunks/555.js +1 -0
- package/web/.next/server/chunks/612.js +1 -0
- package/web/.next/server/chunks/638.js +1 -0
- package/web/.next/server/chunks/692.js +18 -0
- package/web/.next/server/chunks/716.js +2 -0
- package/web/.next/server/chunks/790.js +11 -0
- package/web/.next/server/chunks/802.js +1 -0
- package/web/.next/server/chunks/890.js +1 -1
- package/web/.next/server/chunks/919.js +1 -0
- package/web/.next/server/chunks/950.js +6 -0
- package/web/.next/server/middleware-build-manifest.js +1 -1
- package/web/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/web/.next/server/next-font-manifest.js +1 -1
- package/web/.next/server/next-font-manifest.json +1 -1
- package/web/.next/server/pages/404.html +1 -1
- package/web/.next/server/pages/500.html +1 -1
- package/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/static/chunks/0937d497-114a2a3de515565e.js +1 -0
- package/web/.next/static/chunks/176-aeaf907a6ea5627d.js +1 -0
- package/web/.next/static/chunks/291-efae53ea5f7ee92d.js +1 -0
- package/web/.next/static/chunks/2b9ff8ee-969e65cc354b2150.js +1 -0
- package/web/.next/static/chunks/30-9adcbe92b15a6815.js +1 -0
- package/web/.next/static/chunks/316-26da9cc233ba7557.js +1 -0
- package/web/.next/static/chunks/350-56599dc3bf06066d.js +1 -0
- package/web/.next/static/chunks/486-13e08542f3a1cbac.js +1 -0
- package/web/.next/static/chunks/491-6b4e6201f5110dff.js +1 -0
- package/web/.next/static/chunks/517-ccaf8cc5c733cbd8.js +1 -0
- package/web/.next/static/chunks/545.bfbd92b7632c09de.js +1 -0
- package/web/.next/static/chunks/{805-d946ebedb7e85f73.js → 562-7c2e33d838e869bf.js} +1 -1
- package/web/.next/static/chunks/581-3ad5c4343dad3f50.js +1 -0
- package/web/.next/static/chunks/586-f7017d585a84738d.js +1 -0
- package/web/.next/static/chunks/657.b4c39e676c3e20dc.js +1 -0
- package/web/.next/static/chunks/701.6abcaf5e60463eef.js +6 -0
- package/web/.next/static/chunks/705-a16e1fe09be1ac7f.js +1 -0
- package/web/.next/static/chunks/706-fa2be0f10ffebe6a.js +1 -0
- package/web/.next/static/chunks/837-33c7480524f48577.js +1 -0
- package/web/.next/static/chunks/839-23c5508622fae942.js +1 -0
- package/web/.next/static/chunks/844-fe55292e0701f436.js +1 -0
- package/web/.next/static/chunks/89-b194fcffe5120c44.js +1 -0
- package/web/.next/static/chunks/97.594543c2196de524.js +1 -0
- package/web/.next/static/chunks/app/_not-found/page-238a7b80bda6b4ae.js +1 -0
- package/web/.next/static/chunks/app/agents/[id]/page-4af6c246c4ca7a97.js +1 -0
- package/web/.next/static/chunks/app/agents/page-f4c4c80f955f3967.js +1 -0
- package/web/.next/static/chunks/app/connections/page-0936beaf02d863c6.js +1 -0
- package/web/.next/static/chunks/app/layout-87ebb70bd9cb8443.js +1 -0
- package/web/.next/static/chunks/app/page-a7f292117b7d76bf.js +1 -0
- package/web/.next/static/chunks/app/relations/page-fba25393c8688714.js +1 -0
- package/web/.next/static/chunks/app/resources/[id]/page-d110d89bb8b5fca7.js +1 -0
- package/web/.next/static/chunks/app/resources/page-bbd0b450d3f3131d.js +1 -0
- package/web/.next/static/chunks/app/schedules/page-a1124397fc760413.js +1 -0
- package/web/.next/static/chunks/app/sessions/page-0e322811b0cde724.js +1 -0
- package/web/.next/static/chunks/app/skills/page-22ea0ef89d9f7a13.js +1 -0
- package/web/.next/static/chunks/app/types/page-18c4e1db2724d41d.js +1 -0
- package/web/.next/static/chunks/fcafe1ac.366e4950d3b0d4b7.js +18 -0
- package/web/.next/static/chunks/framework-bbac72adbbe2d328.js +1 -0
- package/web/.next/static/chunks/main-app-6086068e5fb5eacd.js +1 -0
- package/web/.next/static/chunks/main-c15f3fee674e0303.js +1 -0
- package/web/.next/static/chunks/pages/_app-d1670a2c6ce392fc.js +1 -0
- package/web/.next/static/chunks/pages/_error-95aac014d202fa7a.js +1 -0
- package/web/.next/static/chunks/webpack-ef515e41a83a16ac.js +1 -0
- package/web/.next/static/css/945795da867f5250.css +1 -0
- package/web/.next/static/css/dd4e32c285f75776.css +1 -0
- package/web/.next/static/p4S3i9kvO359GW91DQiHu/_buildManifest.js +1 -0
- package/web/.next/trace +29 -23
- package/web/.next/types/app/resources/[id]/page.ts +84 -0
- package/web/.next/types/app/resources/page.ts +84 -0
- package/web/.next/types/app/schedules/page.ts +84 -0
- package/web/.next/types/routes.d.ts +7 -2
- package/web/.next/types/validator.ts +27 -0
- package/web/next.config.mjs +4 -0
- package/web/.next/server/chunks/149.js +0 -11
- package/web/.next/server/chunks/247.js +0 -1
- package/web/.next/server/chunks/851.js +0 -1
- package/web/.next/server/chunks/932.js +0 -2
- package/web/.next/static/84BpRSahg2BTwk3mX6AG8/_buildManifest.js +0 -1
- package/web/.next/static/chunks/0937d497-44abb0f9355ace92.js +0 -1
- package/web/.next/static/chunks/2b9ff8ee-c3b61ac1e608c57d.js +0 -1
- package/web/.next/static/chunks/351-65da8c1a6e25f93b.js +0 -1
- package/web/.next/static/chunks/463-6ae35cdefdc59169.js +0 -1
- package/web/.next/static/chunks/493-92266798be872b8f.js +0 -1
- package/web/.next/static/chunks/517-eb26c72f6059aa26.js +0 -1
- package/web/.next/static/chunks/657.f1e03b7b65eba1d9.js +0 -1
- package/web/.next/static/chunks/687-3a4205f51be38602.js +0 -1
- package/web/.next/static/chunks/837-fd7623601dc78ed8.js +0 -1
- package/web/.next/static/chunks/850-318f32484989dd59.js +0 -1
- package/web/.next/static/chunks/89-ace2e1aa63babfdf.js +0 -1
- package/web/.next/static/chunks/951-e250b541b67da7ce.js +0 -1
- package/web/.next/static/chunks/app/_not-found/page-bbd54d7b9a55d7ae.js +0 -1
- package/web/.next/static/chunks/app/agents/[id]/page-5eaf094fec246f1b.js +0 -1
- package/web/.next/static/chunks/app/agents/page-e9b17f1c3439ef98.js +0 -1
- package/web/.next/static/chunks/app/connections/page-c6afd90abd82d47a.js +0 -1
- package/web/.next/static/chunks/app/layout-c415d5a847cd94b3.js +0 -1
- package/web/.next/static/chunks/app/page-6fc87fc5a128a204.js +0 -1
- package/web/.next/static/chunks/app/relations/page-0355a93a8eb67989.js +0 -1
- package/web/.next/static/chunks/app/sessions/page-beae9a74a07d69e0.js +0 -1
- package/web/.next/static/chunks/app/skills/page-4f839ac913e26d32.js +0 -1
- package/web/.next/static/chunks/app/types/page-4f6b99af3232f861.js +0 -1
- package/web/.next/static/chunks/framework-0e68884e76d9a8d3.js +0 -1
- package/web/.next/static/chunks/main-app-d6f73ccd41f325c5.js +0 -1
- package/web/.next/static/chunks/main-cd22509db15764e2.js +0 -1
- package/web/.next/static/chunks/pages/_app-0f75d689c738a6fc.js +0 -1
- package/web/.next/static/chunks/pages/_error-d481f6d49fb4455d.js +0 -1
- package/web/.next/static/chunks/webpack-d40de72e52f65f79.js +0 -1
- package/web/.next/static/css/abe1a6216491f944.css +0 -1
- /package/web/.next/static/{84BpRSahg2BTwk3mX6AG8 → p4S3i9kvO359GW91DQiHu}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { DATA_DIR, agents, resolved } from '../db.js';
|
|
4
|
+
import { OBJECTS_SKILL_ID } from '../skills/defaults.js';
|
|
5
|
+
import { bus } from '../runtime.js';
|
|
6
|
+
import { renderTemplate, listTemplates } from './templates.js';
|
|
7
|
+
import { composeServiceAction, inspectDocker, recreateDocker, dockerLogs, dockerState, removeDocker, restartDocker, startDocker, stopDocker } from './docker.js';
|
|
8
|
+
import { recordRun, listHistory, clearHistory } from './http-history.js';
|
|
9
|
+
import { dbColumns, dbQuery, dbSchema, dbState, dbTest, forgetDatabase } from './database.js';
|
|
10
|
+
import { addSaved, clearQueries, forgetQueries, listQueries, listSaved, recordQuery, removeSaved, updateSaved } from './db-history.js';
|
|
11
|
+
import { exportCollection, parseHttpFile, createHttpFile, exportHttp, describeHttp, dropSession, inspectHttp, readHttpFile, runHttp, scanHttp, writeHttpFile, sessionClear, sessionOf, sessionSet, sessionView } from './http.js';
|
|
12
|
+
import { ObjectError, parseDatabase } from './model.js';
|
|
13
|
+
import { forgetServer, serverLogs, serverState, startServer, stopServer } from './runner.js';
|
|
14
|
+
import { forgetSupervision, resetSupervision, supervise } from './supervisor.js';
|
|
15
|
+
import { maskConfig, objectsStore } from './store.js';
|
|
16
|
+
export { ObjectError } from './model.js';
|
|
17
|
+
export { objectsStore } from './store.js';
|
|
18
|
+
/** Last known state of each object, refreshed in the background so listing never waits on `docker` or a port check. */
|
|
19
|
+
const states = new Map();
|
|
20
|
+
const POLL_MS = Number(process.env.HIVE_AM_OBJECT_POLL_MS) || 3000;
|
|
21
|
+
const changed = () => bus.emit('msg', { kind: 'objects_changed' });
|
|
22
|
+
const same = (a, b) => !!a && !!b && a.status === b.status && a.detail === b.detail && a.pid === b.pid;
|
|
23
|
+
/** How many requests a folder has: scanning reads every file, so it is remembered for a while. */
|
|
24
|
+
const counts = new Map();
|
|
25
|
+
function httpState(o) {
|
|
26
|
+
const c = o.config, hit = counts.get(o.id);
|
|
27
|
+
if (hit && Date.now() - hit.at < 20_000)
|
|
28
|
+
return { status: hit.ok ? 'ready' : 'error', detail: hit.text };
|
|
29
|
+
let r;
|
|
30
|
+
try {
|
|
31
|
+
const s = scanHttp(c.folder);
|
|
32
|
+
r = { at: Date.now(), ok: true, text: `${s.files.reduce((n, f) => n + f.requests.length, 0)} requests in ${s.files.length} files` };
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
r = { at: Date.now(), ok: false, text: e instanceof Error ? e.message : 'unreadable' };
|
|
36
|
+
}
|
|
37
|
+
counts.set(o.id, r);
|
|
38
|
+
return { status: r.ok ? 'ready' : 'error', detail: r.text };
|
|
39
|
+
}
|
|
40
|
+
/** A cluster is what its members are: running when all are, starting while any is, an error when one failed or only some are up. */
|
|
41
|
+
function clusterState(o) {
|
|
42
|
+
const members = o.config.members.map((id) => ({ row: objectsStore.get(id), state: states.get(id) })).filter((m) => m.row);
|
|
43
|
+
if (!members.length)
|
|
44
|
+
return { status: 'stopped', detail: 'no members' };
|
|
45
|
+
const by = (s) => members.filter((m) => m.state?.status === s);
|
|
46
|
+
const up = by('running').length, total = members.length;
|
|
47
|
+
const names = (list) => list.map((m) => m.row.name).join(', ');
|
|
48
|
+
if (by('error').length)
|
|
49
|
+
return { status: 'error', detail: `${names(by('error'))} failed · ${up}/${total} running` };
|
|
50
|
+
if (by('starting').length)
|
|
51
|
+
return { status: 'starting', detail: `${up}/${total} running` };
|
|
52
|
+
if (up === total)
|
|
53
|
+
return { status: 'running', detail: `${up}/${total} running` };
|
|
54
|
+
if (up === 0)
|
|
55
|
+
return { status: 'stopped', detail: `0/${total} running` };
|
|
56
|
+
return { status: 'error', detail: `only ${up}/${total} running · stopped: ${names(members.filter((m) => m.state?.status !== 'running'))}` };
|
|
57
|
+
}
|
|
58
|
+
/** The state of a server after what it is configured to do by itself (restart it, check its health, trim its log). */
|
|
59
|
+
async function serverStateSupervised(o) {
|
|
60
|
+
const cfg = o.config;
|
|
61
|
+
const base = await serverState(o.id, cfg);
|
|
62
|
+
return supervise(o, cfg, base, { start: () => startServer(o.id, cfg), restart: async () => { await stopServer(o.id, cfg); await startServer(o.id, cfg); } });
|
|
63
|
+
}
|
|
64
|
+
export async function stateOf(o) {
|
|
65
|
+
const s = o.kind === 'server' ? await serverStateSupervised(o)
|
|
66
|
+
: o.kind === 'docker' ? await dockerState(o.id, o.config)
|
|
67
|
+
: o.kind === 'http' ? httpState(o) : o.kind === 'database' ? await dbState(o.id, o.config) : clusterState(o);
|
|
68
|
+
states.set(o.id, s);
|
|
69
|
+
return s;
|
|
70
|
+
}
|
|
71
|
+
export async function refreshStates() {
|
|
72
|
+
const all = objectsStore.list();
|
|
73
|
+
let diff = false;
|
|
74
|
+
for (const id of [...states.keys()])
|
|
75
|
+
if (!all.some((o) => o.id === id)) {
|
|
76
|
+
states.delete(id);
|
|
77
|
+
diff = true;
|
|
78
|
+
}
|
|
79
|
+
// Clusters last: they are made of what the others just reported.
|
|
80
|
+
const one = async (o) => { const before = states.get(o.id); const now = await stateOf(o).catch((e) => ({ status: 'unknown', detail: e instanceof Error ? e.message : 'failed' })); states.set(o.id, now); if (!same(before, now))
|
|
81
|
+
diff = true; };
|
|
82
|
+
await Promise.all(all.filter((o) => o.kind !== 'cluster').map(one));
|
|
83
|
+
for (const o of all.filter((x) => x.kind === 'cluster'))
|
|
84
|
+
await one(o);
|
|
85
|
+
return diff;
|
|
86
|
+
}
|
|
87
|
+
export const viewOf = (o, managers = objectsStore.managers()) => ({ ...maskConfig(o), state: states.get(o.id) ?? { status: 'unknown', detail: 'checking…' }, manager_id: managers.get(o.id) ?? null });
|
|
88
|
+
export const listObjects = () => { const managers = objectsStore.managers(); return objectsStore.list().map((o) => viewOf(o, managers)); };
|
|
89
|
+
let timer, busy = false;
|
|
90
|
+
/** Checks the objects every few seconds and tells the interface when one changed (it fetches the list again). */
|
|
91
|
+
export function armObjects() {
|
|
92
|
+
if (timer)
|
|
93
|
+
return;
|
|
94
|
+
const tick = async () => { if (busy)
|
|
95
|
+
return; busy = true; try {
|
|
96
|
+
if (await refreshStates())
|
|
97
|
+
changed();
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
busy = false;
|
|
101
|
+
} };
|
|
102
|
+
void tick();
|
|
103
|
+
timer = setInterval(() => void tick(), POLL_MS);
|
|
104
|
+
timer.unref?.();
|
|
105
|
+
}
|
|
106
|
+
const get = (id) => objectsStore.get(id) ?? (() => { throw new ObjectError('Resource not found'); })();
|
|
107
|
+
/** Starts, stops or restarts one server or container. */
|
|
108
|
+
async function act(o, action) {
|
|
109
|
+
if (o.kind === 'server') {
|
|
110
|
+
resetSupervision(o.id); // a person decided: the automatic restarts start over
|
|
111
|
+
const c = o.config;
|
|
112
|
+
if (action === 'start')
|
|
113
|
+
await startServer(o.id, c);
|
|
114
|
+
else if (action === 'stop')
|
|
115
|
+
await stopServer(o.id, c);
|
|
116
|
+
else {
|
|
117
|
+
await stopServer(o.id, c);
|
|
118
|
+
await startServer(o.id, c);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const c = o.config;
|
|
123
|
+
if (action === 'start')
|
|
124
|
+
await startDocker(o.id, c);
|
|
125
|
+
else if (action === 'stop')
|
|
126
|
+
await stopDocker(o.id, c);
|
|
127
|
+
else
|
|
128
|
+
await restartDocker(o.id, c);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Members of a cluster one after another: start in the order they are listed, stop in the opposite order (what depends on the others goes
|
|
133
|
+
* first and last). One failing does not keep the rest from being tried; the failures are reported together.
|
|
134
|
+
*/
|
|
135
|
+
async function actOnCluster(o, action) {
|
|
136
|
+
const members = o.config.members.map((id) => objectsStore.get(id)).filter((m) => !!m);
|
|
137
|
+
const errors = [];
|
|
138
|
+
const each = async (list, what) => {
|
|
139
|
+
for (const m of list) {
|
|
140
|
+
try {
|
|
141
|
+
const now = await stateOf(m);
|
|
142
|
+
if (what === 'start' && (now.status === 'running' || now.status === 'starting'))
|
|
143
|
+
continue; // already up
|
|
144
|
+
await act(m, what);
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
errors.push(`${m.name}: ${e instanceof Error ? e.message : 'failed'}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
if (action === 'start')
|
|
152
|
+
await each(members, 'start');
|
|
153
|
+
else if (action === 'stop')
|
|
154
|
+
await each([...members].reverse(), 'stop');
|
|
155
|
+
else {
|
|
156
|
+
await each([...members].reverse(), 'stop');
|
|
157
|
+
await each(members, 'start');
|
|
158
|
+
}
|
|
159
|
+
if (errors.length)
|
|
160
|
+
throw new ObjectError(errors.join(' · '));
|
|
161
|
+
}
|
|
162
|
+
export async function objectAction(id, action) {
|
|
163
|
+
const o = get(id);
|
|
164
|
+
if (action === 'recreate' || action === 'rebuild') {
|
|
165
|
+
if (o.kind !== 'docker')
|
|
166
|
+
throw new ObjectError('Only a Docker resource can be created again');
|
|
167
|
+
try {
|
|
168
|
+
await recreateDocker(o.id, o.config, action === 'rebuild');
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
await refreshStates().catch(() => undefined);
|
|
172
|
+
changed();
|
|
173
|
+
}
|
|
174
|
+
return viewOf(o);
|
|
175
|
+
}
|
|
176
|
+
if (action !== 'start' && action !== 'stop' && action !== 'restart')
|
|
177
|
+
throw new ObjectError('Unknown action');
|
|
178
|
+
if (o.kind === 'http')
|
|
179
|
+
throw new ObjectError('HTTP requests are not started or stopped: open them and run a request.');
|
|
180
|
+
if (o.kind === 'database')
|
|
181
|
+
throw new ObjectError('A database is not started or stopped: open it and run a query.');
|
|
182
|
+
try {
|
|
183
|
+
if (o.kind === 'cluster')
|
|
184
|
+
await actOnCluster(o, action);
|
|
185
|
+
else
|
|
186
|
+
await act(o, action);
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
await refreshStates().catch(() => undefined);
|
|
190
|
+
changed();
|
|
191
|
+
}
|
|
192
|
+
return viewOf(o);
|
|
193
|
+
}
|
|
194
|
+
/** The configuration of a container (or of one service of a compose project) as Docker reports it. */
|
|
195
|
+
export async function objectInspect(id, service) {
|
|
196
|
+
const o = get(id);
|
|
197
|
+
if (o.kind !== 'docker')
|
|
198
|
+
throw new ObjectError('Only a Docker resource can be inspected');
|
|
199
|
+
return inspectDocker(o.id, o.config, service);
|
|
200
|
+
}
|
|
201
|
+
export { listTemplates };
|
|
202
|
+
/** Writes the compose file of a template under the data folder and makes it a Docker object (it is not started: the first run may pull images). */
|
|
203
|
+
export function createFromTemplate(p) {
|
|
204
|
+
const name = String(p.name ?? '').trim();
|
|
205
|
+
if (!name)
|
|
206
|
+
throw new ObjectError('The name is required');
|
|
207
|
+
const compose = renderTemplate(String(p.template ?? ''), (p.values && typeof p.values === 'object' ? p.values : {}));
|
|
208
|
+
const slug = name.toLowerCase().normalize('NFD').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || String(p.template);
|
|
209
|
+
const dir = join(DATA_DIR, 'stacks', slug);
|
|
210
|
+
if (existsSync(dir))
|
|
211
|
+
throw new ObjectError(`There is already a stack called "${slug}"`);
|
|
212
|
+
mkdirSync(dir, { recursive: true });
|
|
213
|
+
const file = join(dir, 'docker-compose.yml');
|
|
214
|
+
writeFileSync(file, compose, { encoding: 'utf8', flag: 'wx' });
|
|
215
|
+
try {
|
|
216
|
+
return createObject({ name, kind: 'docker', colony_id: p.colony_id || null, config: { mode: 'compose', file, project: `hive-${slug}` } });
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
rmSync(dir, { recursive: true, force: true });
|
|
220
|
+
throw e;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** One service of a compose project (start, stop, restart), without touching the others. */
|
|
224
|
+
export async function objectServiceAction(id, service, action) {
|
|
225
|
+
const o = get(id);
|
|
226
|
+
if (o.kind !== 'docker')
|
|
227
|
+
throw new ObjectError('Only a compose project has services');
|
|
228
|
+
if (!['start', 'stop', 'restart', 'recreate', 'rebuild'].includes(String(action)))
|
|
229
|
+
throw new ObjectError('Unknown action');
|
|
230
|
+
try {
|
|
231
|
+
await composeServiceAction(o.config, String(service ?? ''), action);
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
await refreshStates().catch(() => undefined);
|
|
235
|
+
changed();
|
|
236
|
+
}
|
|
237
|
+
return viewOf(o);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* The latest output. A cluster reads all its members and puts `[name]` in front of each line; its cursor is the cursors of the members
|
|
241
|
+
* together (a JSON text), so a following call returns only what each of them printed since.
|
|
242
|
+
*/
|
|
243
|
+
export async function objectLogs(id, q) {
|
|
244
|
+
const o = get(id);
|
|
245
|
+
if (o.kind === 'http' || o.kind === 'database')
|
|
246
|
+
return { text: '', cursor: '' };
|
|
247
|
+
if (o.kind === 'cluster') {
|
|
248
|
+
const members = o.config.members.map((m) => objectsStore.get(m)).filter((m) => !!m);
|
|
249
|
+
let before = {};
|
|
250
|
+
try {
|
|
251
|
+
before = q.after ? JSON.parse(q.after) : {};
|
|
252
|
+
}
|
|
253
|
+
catch { /* a fresh start */ }
|
|
254
|
+
const per = Math.max(15, Math.ceil((q.tail ?? 300) / Math.max(1, members.length)));
|
|
255
|
+
const parts = await Promise.all(members.map(async (m) => {
|
|
256
|
+
const r = await objectLogs(m.id, before[m.id] !== undefined ? { after: before[m.id] } : { tail: per }).catch(() => ({ text: '', cursor: before[m.id] ?? '' }));
|
|
257
|
+
return { m, r };
|
|
258
|
+
}));
|
|
259
|
+
const text = parts.map(({ m, r }) => r.text.split('\n').filter((l) => l.trim()).map((l) => `[${m.name}] ${l}`).join('\n')).filter(Boolean).join('\n');
|
|
260
|
+
return { text, cursor: JSON.stringify(Object.fromEntries(parts.map(({ m, r }) => [m.id, r.cursor]))) };
|
|
261
|
+
}
|
|
262
|
+
if (o.kind === 'server')
|
|
263
|
+
return serverLogs(o.id, { tail: q.tail, after: q.after !== undefined && q.after !== '' ? Number(q.after) : undefined });
|
|
264
|
+
return dockerLogs(o.id, o.config, q);
|
|
265
|
+
}
|
|
266
|
+
export const statsOf = async (id) => { const { objectStats } = await import('./stats.js'); return objectStats(get(id)); };
|
|
267
|
+
/** The requests of an HTTP object, and running one of them. */
|
|
268
|
+
export const httpScan = (id) => { const o = get(id); if (o.kind !== 'http')
|
|
269
|
+
throw new ObjectError('That is not an HTTP resource'); return scanHttp(o.config.folder, o.config.variables); };
|
|
270
|
+
export const httpDescribe = (id, file, index, env) => { const o = get(id); if (o.kind !== 'http')
|
|
271
|
+
throw new ObjectError('That is not an HTTP resource'); return describeHttp(o.config.folder, file, index, env ?? o.config.env, o.config.variables, sessionOf(o.id)); };
|
|
272
|
+
export const httpRun = async (id, file, index, env) => {
|
|
273
|
+
const o = get(id);
|
|
274
|
+
if (o.kind !== 'http')
|
|
275
|
+
throw new ObjectError('That is not an HTTP resource');
|
|
276
|
+
const cfg = o.config, useEnv = env ?? cfg.env ?? '';
|
|
277
|
+
let name = `#${index}`, method = '';
|
|
278
|
+
try {
|
|
279
|
+
const it = parseHttpFile(readHttpFile(cfg.folder, file), file).requests[index];
|
|
280
|
+
if (it) {
|
|
281
|
+
name = it.name;
|
|
282
|
+
method = it.method;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch { /* the run reports it */ }
|
|
286
|
+
try {
|
|
287
|
+
const run = await runHttp(cfg.folder, file, index, useEnv || undefined, cfg.variables, sessionOf(o.id));
|
|
288
|
+
recordRun(id, { file, index, name, method, env: useEnv, run });
|
|
289
|
+
return run;
|
|
290
|
+
}
|
|
291
|
+
catch (e) {
|
|
292
|
+
recordRun(id, { file, index, name, method, env: useEnv, error: e instanceof Error ? e.message : String(e) });
|
|
293
|
+
throw e;
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
export const httpDocs = (id, format, file) => {
|
|
297
|
+
const o = httpObjectOf(id);
|
|
298
|
+
const hist = listHistory(id);
|
|
299
|
+
return exportCollection(o.config.folder, o.name, format, file || undefined, (f, i) => { const h = hist.find((x) => x.file === f && x.index === i && x.run && x.run.status < 400 && !x.run.binary); return h?.run ? { status: h.run.status, contentType: h.run.contentType, body: h.run.body } : undefined; });
|
|
300
|
+
};
|
|
301
|
+
export const httpHistory = (id) => { httpObjectOf(id); return listHistory(id); };
|
|
302
|
+
export const httpHistoryClear = (id) => { httpObjectOf(id); clearHistory(id); return []; };
|
|
303
|
+
export const httpInspect = (id, file, index, env) => { const o = get(id); if (o.kind !== 'http')
|
|
304
|
+
throw new ObjectError('That is not an HTTP resource'); return inspectHttp(o.config.folder, file, index, env ?? o.config.env, o.config.variables, sessionOf(o.id)); };
|
|
305
|
+
export const httpExport = (id, file, index, format, env, redact = false) => { const o = get(id); if (o.kind !== 'http')
|
|
306
|
+
throw new ObjectError('That is not an HTTP resource'); return exportHttp(o.config.folder, file, index, format, env ?? o.config.env, o.config.variables, sessionOf(o.id), redact); };
|
|
307
|
+
export const httpFileRead = (id, file) => { const o = get(id); if (o.kind !== 'http')
|
|
308
|
+
throw new ObjectError('That is not an HTTP resource'); return { file, content: readHttpFile(o.config.folder, file) }; };
|
|
309
|
+
export const httpFileCreate = (id, file, content) => { const o = get(id); if (o.kind !== 'http')
|
|
310
|
+
throw new ObjectError('That is not an HTTP resource'); return { file: createHttpFile(o.config.folder, file, content) }; };
|
|
311
|
+
export const httpFileWrite = (id, file, content) => { const o = get(id); if (o.kind !== 'http')
|
|
312
|
+
throw new ObjectError('That is not an HTTP resource'); writeHttpFile(o.config.folder, file, content); return { ok: true }; };
|
|
313
|
+
/** The values kept while an HTTP object is used (see http.ts): look at them, set one, forget one or all. */
|
|
314
|
+
const httpObjectOf = (id) => { const o = get(id); if (o.kind !== 'http')
|
|
315
|
+
throw new ObjectError('That is not an HTTP resource'); return o; };
|
|
316
|
+
const httpObject = (id) => { const o = get(id); if (o.kind !== 'http')
|
|
317
|
+
throw new ObjectError('That is not an HTTP resource'); return o; };
|
|
318
|
+
export const httpSession = (id) => sessionView(httpObject(id).id);
|
|
319
|
+
export const httpSessionSet = (id, name, value) => { sessionSet(httpObject(id).id, name, value); return sessionView(id); };
|
|
320
|
+
export const httpSessionClear = (id, name) => { sessionClear(httpObject(id).id, name); return sessionView(id); };
|
|
321
|
+
/** A database object: what its connection can show and run (see database.ts). Reading the schema or running a query uses the saved password. */
|
|
322
|
+
const dbOf = (id) => { const o = get(id); if (o.kind !== 'database')
|
|
323
|
+
throw new ObjectError('That is not a database resource'); return { o, cfg: o.config }; };
|
|
324
|
+
export const dbSchemaOf = (id, database) => dbSchema(dbOf(id).cfg, database);
|
|
325
|
+
export const dbColumnsOf = (id, p) => dbColumns(dbOf(id).cfg, p);
|
|
326
|
+
export async function dbRun(id, p) {
|
|
327
|
+
const { cfg } = dbOf(id);
|
|
328
|
+
try {
|
|
329
|
+
const r = await dbQuery(cfg, p);
|
|
330
|
+
recordQuery(id, { sql: p.sql, database: p.database, ok: true, ms: r.ms, rows: r.columns.length ? r.rows.length : undefined, affected: r.affected });
|
|
331
|
+
return r;
|
|
332
|
+
}
|
|
333
|
+
catch (e) {
|
|
334
|
+
recordQuery(id, { sql: p.sql, database: p.database, ok: false, ms: 0, error: e instanceof Error ? e.message : 'failed' });
|
|
335
|
+
throw e;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
export const dbHistory = (id) => { dbOf(id); return listQueries(id); };
|
|
339
|
+
export const dbHistoryClear = (id) => { dbOf(id); clearQueries(id); return []; };
|
|
340
|
+
export const dbSavedList = (id) => { dbOf(id); return listSaved(id); };
|
|
341
|
+
export const dbSavedAdd = (id, p) => {
|
|
342
|
+
dbOf(id);
|
|
343
|
+
const name = String(p.name ?? '').trim().slice(0, 80), sql = String(p.sql ?? '');
|
|
344
|
+
if (!name || !sql.trim())
|
|
345
|
+
throw new ObjectError('Give the query a name and some text');
|
|
346
|
+
try {
|
|
347
|
+
return addSaved(id, { name, sql, database: p.database ? String(p.database) : undefined });
|
|
348
|
+
}
|
|
349
|
+
catch (e) {
|
|
350
|
+
throw new ObjectError(e instanceof Error ? e.message : 'could not save');
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
export const dbSavedUpdate = (id, savedId, p) => {
|
|
354
|
+
dbOf(id);
|
|
355
|
+
const name = p.name === undefined ? undefined : String(p.name).trim().slice(0, 80), sql = p.sql === undefined ? undefined : String(p.sql);
|
|
356
|
+
if (name === '' || (sql !== undefined && !sql.trim()))
|
|
357
|
+
throw new ObjectError('Give the query a name and some text');
|
|
358
|
+
const e = updateSaved(id, savedId, { name, sql, database: p.database === undefined ? undefined : p.database ? String(p.database) : null });
|
|
359
|
+
if (!e)
|
|
360
|
+
throw new ObjectError('Saved query not found');
|
|
361
|
+
return e;
|
|
362
|
+
};
|
|
363
|
+
export const dbSavedRemove = (id, savedId) => { dbOf(id); if (!removeSaved(id, savedId))
|
|
364
|
+
throw new ObjectError('Saved query not found'); return listSaved(id); };
|
|
365
|
+
/** Only these can be looked after by an agent: it starts, stops and restarts them. */
|
|
366
|
+
const MANAGEABLE = ['server', 'docker', 'cluster'];
|
|
367
|
+
/** An agent without the "Colony resources" skill gets it when it is made the manager of something: without it the tools are not offered. */
|
|
368
|
+
function ensureSkill(agentId) {
|
|
369
|
+
const ag = agents.get(agentId);
|
|
370
|
+
if (!ag)
|
|
371
|
+
throw new ObjectError('That agent no longer exists');
|
|
372
|
+
if (resolved(ag).skill_ids.includes(OBJECTS_SKILL_ID))
|
|
373
|
+
return false;
|
|
374
|
+
agents.setSkills(agentId, [...ag.skill_ids, OBJECTS_SKILL_ID]);
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
/** Chooses the one agent that looks after a resource (`null` leaves it to the agents of its colony). */
|
|
378
|
+
export function setObjectManager(id, agentId) {
|
|
379
|
+
const o = get(id);
|
|
380
|
+
if (!MANAGEABLE.includes(o.kind))
|
|
381
|
+
throw new ObjectError('This kind of resource is not looked after by agents');
|
|
382
|
+
const a = agentId ? String(agentId) : null;
|
|
383
|
+
const skillAdded = a ? ensureSkill(a) : false;
|
|
384
|
+
objectsStore.setManager(id, a);
|
|
385
|
+
changed();
|
|
386
|
+
return { object: viewOf(o), skillAdded };
|
|
387
|
+
}
|
|
388
|
+
/** The same relation seen from the agent: the resources it looks after become exactly `objectIds` (the others go back to their colony). */
|
|
389
|
+
export function setAgentObjects(agentId, objectIds) {
|
|
390
|
+
if (!agents.get(agentId))
|
|
391
|
+
throw new ObjectError('That agent no longer exists');
|
|
392
|
+
if (!Array.isArray(objectIds))
|
|
393
|
+
throw new ObjectError('Choose the resources');
|
|
394
|
+
const want = new Set(objectIds.map(String));
|
|
395
|
+
const managers = objectsStore.managers();
|
|
396
|
+
for (const id of want) {
|
|
397
|
+
const o = get(id);
|
|
398
|
+
if (!MANAGEABLE.includes(o.kind))
|
|
399
|
+
throw new ObjectError('This kind of resource is not looked after by agents');
|
|
400
|
+
}
|
|
401
|
+
const skillAdded = want.size > 0 ? ensureSkill(agentId) : false;
|
|
402
|
+
for (const [oid, aid] of managers)
|
|
403
|
+
if (aid === agentId && !want.has(oid))
|
|
404
|
+
objectsStore.setManager(oid, null);
|
|
405
|
+
for (const id of want)
|
|
406
|
+
objectsStore.setManager(id, agentId);
|
|
407
|
+
changed();
|
|
408
|
+
return { skillAdded };
|
|
409
|
+
}
|
|
410
|
+
/** Tries a connection before it is saved: the config comes from the form; an empty password means the one already saved (of that object). */
|
|
411
|
+
export async function dbTestConfig(p) {
|
|
412
|
+
const raw = { ...(p.config ?? {}) };
|
|
413
|
+
if (p.id && !raw.password) {
|
|
414
|
+
const o = objectsStore.get(String(p.id));
|
|
415
|
+
if (o?.kind === 'database' && o.config.engine === raw.engine)
|
|
416
|
+
raw.password = o.config.password;
|
|
417
|
+
}
|
|
418
|
+
const cfg = parseDatabase(raw);
|
|
419
|
+
const r = await dbTest(cfg);
|
|
420
|
+
return r;
|
|
421
|
+
}
|
|
422
|
+
/** Deleting an object also removes what hive-am made for it (see docker.ts for what is left alone). */
|
|
423
|
+
export async function removeObject(id) {
|
|
424
|
+
const o = objectsStore.get(id);
|
|
425
|
+
if (!o)
|
|
426
|
+
return false;
|
|
427
|
+
if (o.kind === 'server') {
|
|
428
|
+
await stopServer(o.id, o.config).catch(() => undefined);
|
|
429
|
+
forgetServer(o.id);
|
|
430
|
+
}
|
|
431
|
+
else if (o.kind === 'docker')
|
|
432
|
+
await removeDocker(o.id, o.config);
|
|
433
|
+
states.delete(id);
|
|
434
|
+
counts.delete(id);
|
|
435
|
+
forgetSupervision(id);
|
|
436
|
+
dropSession(id);
|
|
437
|
+
clearHistory(id);
|
|
438
|
+
forgetDatabase(id);
|
|
439
|
+
forgetQueries(id);
|
|
440
|
+
const ok = objectsStore.remove(id);
|
|
441
|
+
changed();
|
|
442
|
+
return ok;
|
|
443
|
+
}
|
|
444
|
+
export const createObject = (p) => { const o = objectsStore.create(p); void stateOf(o).then(changed); changed(); return viewOf(o); };
|
|
445
|
+
export const updateObject = (id, p) => { const o = objectsStore.update(id, p); forgetDatabase(id); void stateOf(o).then(changed); changed(); return viewOf(o); };
|