@stage-labs/metro 0.1.0-beta.7 → 0.1.0-beta.71
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/api.js +14 -0
- package/dist/bedrock-eventstream.js +120 -0
- package/dist/bedrock-proxy.js +260 -0
- package/dist/bedrock-upstream.js +72 -0
- package/dist/bedrock.js +37 -0
- package/dist/claude-settings.js +40 -0
- package/dist/claude.js +107 -0
- package/dist/cli.js +101 -0
- package/dist/control.js +74 -0
- package/dist/hold.js +211 -0
- package/dist/local.js +93 -0
- package/dist/node-name.js +51 -0
- package/dist/plugin.js +80 -0
- package/dist/provider-flags.js +9 -0
- package/dist/runtime-install.js +84 -0
- package/dist/runtime.js +70 -0
- package/dist/serve.js +147 -0
- package/dist/service.js +224 -0
- package/dist/tail.js +119 -0
- package/dist/update.js +45 -0
- package/dist/version.js +97 -0
- package/package.json +14 -25
- package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +118 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +220 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/package.json +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/addressed.ts +25 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +377 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +302 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +95 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +128 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +280 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +204 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/bundle-api.ts +111 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-api.ts +97 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-files.ts +329 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +39 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +307 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +98 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +70 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +330 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/control-api.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +120 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +390 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-key.ts +33 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-registry.ts +13 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-routes.ts +54 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +7 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/is-record.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-cli-api.ts +63 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +187 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-owner.ts +14 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/machine-api.ts +61 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/mode-api.ts +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/model-api.ts +169 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +159 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +117 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +109 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +226 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +295 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/return-to.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-deps.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/server-types.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/servers-api.ts +80 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/signed-identity.ts +36 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +320 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/terminal-api.ts +71 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/terminal-tickets.ts +40 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/terminal-ws.ts +114 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +352 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/update-api.ts +116 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +294 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-api.ts +78 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-types.ts +12 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/version.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +19 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/address.ts +6 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +43 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +84 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +144 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +39 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +69 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +329 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +155 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/local-connectors.ts +277 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +253 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +33 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/servers.ts +83 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/stations.ts +12 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/vault.ts +109 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/bedrock.ts +252 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-auth.ts +231 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-device.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-stream.ts +288 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-translate.ts +203 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex.ts +211 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/eventstream.ts +123 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/forward.ts +169 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/gateway.ts +193 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/model-config.ts +180 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +141 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +216 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +191 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +47 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +215 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +215 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +400 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +237 -0
- package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +60 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +184 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
- package/runtime/node_modules/@metro-labs/telegram/package.json +26 -0
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
- package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
- package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +156 -0
- package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
- package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
- package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
- package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
- package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
- package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
- package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
- package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +201 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +109 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +58 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
- package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
- package/runtime/node_modules/@metro-labs/webhook/src/station.ts +37 -0
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +27 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +66 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +327 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +138 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +185 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +112 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +335 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +80 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +53 -0
- package/runtime/runtime.json +3 -0
- package/runtime/stations.json +36 -0
- package/runtime/trains/.keep +0 -0
- package/LICENSE +0 -21
- package/README.md +0 -193
- package/dist/cache.js +0 -75
- package/dist/cli/actions.js +0 -139
- package/dist/cli/config.js +0 -185
- package/dist/cli/index.js +0 -159
- package/dist/cli/skill.js +0 -62
- package/dist/cli/util.js +0 -72
- package/dist/codex-rc.js +0 -236
- package/dist/dispatcher.js +0 -95
- package/dist/history.js +0 -84
- package/dist/ipc.js +0 -72
- package/dist/log.js +0 -15
- package/dist/paths.js +0 -68
- package/dist/stations/discord.js +0 -200
- package/dist/stations/index.js +0 -73
- package/dist/stations/telegram-md.js +0 -34
- package/dist/stations/telegram-upload.js +0 -113
- package/dist/stations/telegram.js +0 -213
- package/docs/agents.md +0 -185
- package/docs/uri-scheme.md +0 -71
- package/skills/metro/SKILL.md +0 -241
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { localStations } from './local.js';
|
|
6
|
+
import { SERVER_ENTRY, findBun, runtimeDir } from './runtime.js';
|
|
7
|
+
export const MANIFEST_FILE = 'stations.json';
|
|
8
|
+
const METRO_SOURCES = join('node_modules', '@metro-labs');
|
|
9
|
+
const INSTALL_TIMEOUT_MS = 15 * 60_000;
|
|
10
|
+
export function runtimeStore() {
|
|
11
|
+
const env = process.env.METRO_RUNTIME_STORE?.trim() ?? '';
|
|
12
|
+
return env === '' ? join(homedir(), '.metro', 'runtime') : env;
|
|
13
|
+
}
|
|
14
|
+
const isRecord = (v) => typeof v === 'object' && v !== null;
|
|
15
|
+
const ranges = (raw) => isRecord(raw)
|
|
16
|
+
? Object.fromEntries(Object.entries(raw).filter((e) => typeof e[1] === 'string'))
|
|
17
|
+
: {};
|
|
18
|
+
export function readManifest(path) {
|
|
19
|
+
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
20
|
+
if (!isRecord(raw))
|
|
21
|
+
throw new Error(`${path} is not a runtime manifest`);
|
|
22
|
+
const stations = isRecord(raw.stations) ? raw.stations : {};
|
|
23
|
+
return {
|
|
24
|
+
core: ranges(raw.core),
|
|
25
|
+
stations: Object.fromEntries(Object.entries(stations).map(([name, deps]) => [name, ranges(deps)])),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function dependenciesFor(manifest, stations) {
|
|
29
|
+
const out = { ...manifest.core };
|
|
30
|
+
for (const station of new Set(stations))
|
|
31
|
+
Object.assign(out, manifest.stations[station] ?? {});
|
|
32
|
+
return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b)));
|
|
33
|
+
}
|
|
34
|
+
const packageText = (deps) => `${JSON.stringify({ name: 'metro-runtime', private: true, dependencies: deps }, null, 2)}\n`;
|
|
35
|
+
function readOrNull(path) {
|
|
36
|
+
try {
|
|
37
|
+
return readFileSync(path, 'utf8');
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function syncSources(sources, store) {
|
|
44
|
+
const stamp = readOrNull(join(sources, 'runtime.json'));
|
|
45
|
+
if (stamp !== null && readOrNull(join(store, 'runtime.json')) === stamp)
|
|
46
|
+
return false;
|
|
47
|
+
rmSync(join(store, METRO_SOURCES), { recursive: true, force: true });
|
|
48
|
+
cpSync(join(sources, METRO_SOURCES), join(store, METRO_SOURCES), { recursive: true });
|
|
49
|
+
if (stamp !== null)
|
|
50
|
+
writeFileSync(join(store, 'runtime.json'), stamp);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
export function installDependencies(store, deps, bun, log) {
|
|
54
|
+
const wanted = packageText(deps);
|
|
55
|
+
const marker = join(store, 'node_modules', '.metro-installed');
|
|
56
|
+
if (existsSync(marker) && readOrNull(join(store, 'package.json')) === wanted)
|
|
57
|
+
return false;
|
|
58
|
+
writeFileSync(join(store, 'package.json'), wanted);
|
|
59
|
+
log(`Installing the channel SDKs this machine needs into ${store}`);
|
|
60
|
+
const run = spawnSync(bun, ['install', '--no-summary', '--no-progress'], {
|
|
61
|
+
cwd: store,
|
|
62
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
63
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
64
|
+
});
|
|
65
|
+
if (run.error !== undefined || run.status !== 0)
|
|
66
|
+
throw new Error(`bun install failed in ${store}: ${run.error?.message ?? `exit ${String(run.status)}`}`);
|
|
67
|
+
writeFileSync(marker, `${new Date().toISOString()}\n`);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
export function prepareRuntime(opts = {}) {
|
|
71
|
+
const sources = opts.sources ?? runtimeDir();
|
|
72
|
+
const manifestPath = join(sources, MANIFEST_FILE);
|
|
73
|
+
if (!existsSync(manifestPath))
|
|
74
|
+
return { dir: sources, entry: join(sources, SERVER_ENTRY), trains: join(sources, 'trains'), manifest: null };
|
|
75
|
+
const store = opts.store ?? runtimeStore();
|
|
76
|
+
const log = opts.log ??
|
|
77
|
+
((line) => {
|
|
78
|
+
process.stderr.write(`${line}\n`);
|
|
79
|
+
});
|
|
80
|
+
mkdirSync(join(store, 'trains'), { recursive: true });
|
|
81
|
+
syncSources(sources, store);
|
|
82
|
+
installDependencies(store, dependenciesFor(readManifest(manifestPath), localStations(opts.agents)), opts.bun ?? findBun(), log);
|
|
83
|
+
return { dir: store, entry: join(store, SERVER_ENTRY), trains: join(store, 'trains'), manifest: manifestPath };
|
|
84
|
+
}
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const AGENT_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{10}$/;
|
|
6
|
+
export const SERVER_ENTRY = join('node_modules', '@metro-labs', 'mcp', 'src', 'server.ts');
|
|
7
|
+
export class MissingRuntime extends Error {
|
|
8
|
+
}
|
|
9
|
+
export function runtimeDir() {
|
|
10
|
+
const explicit = process.env.METRO_RUNTIME_DIR?.trim();
|
|
11
|
+
const dir = explicit !== undefined && explicit !== ''
|
|
12
|
+
? explicit
|
|
13
|
+
: join(dirname(dirname(fileURLToPath(import.meta.url))), 'runtime');
|
|
14
|
+
if (!existsSync(join(dir, SERVER_ENTRY)))
|
|
15
|
+
throw new MissingRuntime(`no metro daemon at ${dir}. Reinstall with: npm i -g @stage-labs/metro@beta`);
|
|
16
|
+
return dir;
|
|
17
|
+
}
|
|
18
|
+
class MissingBun extends Error {
|
|
19
|
+
}
|
|
20
|
+
export function assertAgentId(id) {
|
|
21
|
+
if (id === undefined || !AGENT_RE.test(id))
|
|
22
|
+
throw new Error(`'${id ?? ''}' is not an agent id — copy it from the agent's page in the web UI`);
|
|
23
|
+
return id;
|
|
24
|
+
}
|
|
25
|
+
export function findBun() {
|
|
26
|
+
const found = spawnSync('bun', ['--version'], { stdio: 'ignore' });
|
|
27
|
+
if (found.error !== undefined || found.status !== 0)
|
|
28
|
+
throw new MissingBun('metro serve needs Bun, which is not on PATH.\n' +
|
|
29
|
+
'Install it with: curl -fsSL https://bun.sh/install | bash');
|
|
30
|
+
return 'bun';
|
|
31
|
+
}
|
|
32
|
+
export const localPort = () => Number(process.env.METRO_WEBHOOK_PORT) || 8420;
|
|
33
|
+
export const localUrl = () => `http://127.0.0.1:${String(localPort())}`;
|
|
34
|
+
export const RESTART_CODE = 75;
|
|
35
|
+
export const HOLD_CODE = 76;
|
|
36
|
+
function spawnOnce(plan) {
|
|
37
|
+
const child = spawn(plan.command, plan.args, {
|
|
38
|
+
cwd: plan.cwd,
|
|
39
|
+
env: plan.env,
|
|
40
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
41
|
+
});
|
|
42
|
+
const relay = (sig) => () => {
|
|
43
|
+
child.kill(sig);
|
|
44
|
+
};
|
|
45
|
+
const onInt = relay('SIGINT');
|
|
46
|
+
const onTerm = relay('SIGTERM');
|
|
47
|
+
process.on('SIGINT', onInt);
|
|
48
|
+
process.on('SIGTERM', onTerm);
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
child.on('exit', (code) => {
|
|
51
|
+
process.off('SIGINT', onInt);
|
|
52
|
+
process.off('SIGTERM', onTerm);
|
|
53
|
+
resolve(code ?? 0);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export async function spawnPlan(plan, hold) {
|
|
58
|
+
for (;;) {
|
|
59
|
+
const code = await spawnOnce(plan());
|
|
60
|
+
if (code === RESTART_CODE) {
|
|
61
|
+
process.stderr.write('metro was updated; restarting the daemon on the new version\n');
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (code !== HOLD_CODE || hold === undefined)
|
|
65
|
+
return code;
|
|
66
|
+
if ((await hold()) === 'exit')
|
|
67
|
+
return 0;
|
|
68
|
+
process.stderr.write('starting the daemon again\n');
|
|
69
|
+
}
|
|
70
|
+
}
|
package/dist/serve.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { agentsDir } from './local.js';
|
|
5
|
+
import { currentVersion } from './version.js';
|
|
6
|
+
import { serveLockedBy, serveStateDir } from './control.js';
|
|
7
|
+
import { findBun, localPort, SERVER_ENTRY, spawnPlan } from './runtime.js';
|
|
8
|
+
import { prepareRuntime } from './runtime-install.js';
|
|
9
|
+
import { ensureNodeName } from './node-name.js';
|
|
10
|
+
import { holdUntilStart } from './hold.js';
|
|
11
|
+
const SCRUBBED = new Set(['METRO_RUN_TOKEN', 'METRO_AGENT', 'DATABASE_URL']);
|
|
12
|
+
const PORT_FLAG = /^--port=(.*)$/;
|
|
13
|
+
const OWNER_FLAG = /^--owner=(.*)$/;
|
|
14
|
+
const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
15
|
+
const USAGE = 'usage: metro serve [--port <n>] [--owner <address>]';
|
|
16
|
+
const TUNNEL_FLAG = /^--(no-)?tunnel(=.*)?$/;
|
|
17
|
+
const TAILSCALE_APP = '/Applications/Tailscale.app/Contents/MacOS/Tailscale';
|
|
18
|
+
function portOf(raw) {
|
|
19
|
+
const n = Number(raw);
|
|
20
|
+
if (raw === undefined || raw === '' || !Number.isInteger(n) || n < 1 || n > 65535)
|
|
21
|
+
throw new Error(`'${raw ?? ''}' is not a port — ${USAGE}`);
|
|
22
|
+
return n;
|
|
23
|
+
}
|
|
24
|
+
function ownerOf(raw) {
|
|
25
|
+
if (raw === undefined || !ADDRESS.test(raw))
|
|
26
|
+
throw new Error(`'${raw ?? ''}' is not an Ethereum address — ${USAGE}`);
|
|
27
|
+
return raw.toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
export function parseServeArgs(argv) {
|
|
30
|
+
let port = localPort();
|
|
31
|
+
let owner = null;
|
|
32
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
33
|
+
const arg = argv[i] ?? '';
|
|
34
|
+
if (TUNNEL_FLAG.test(arg))
|
|
35
|
+
throw new Error('--tunnel is gone: metro serve always publishes through Tailscale Funnel — ' + USAGE);
|
|
36
|
+
const inlineOwner = OWNER_FLAG.exec(arg);
|
|
37
|
+
if (inlineOwner) {
|
|
38
|
+
owner = ownerOf(inlineOwner[1]);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (arg === '--owner') {
|
|
42
|
+
owner = ownerOf(argv[i + 1]);
|
|
43
|
+
i += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const inline = PORT_FLAG.exec(arg);
|
|
47
|
+
if (inline) {
|
|
48
|
+
port = portOf(inline[1]);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (arg === '--port' || arg === '-p') {
|
|
52
|
+
port = portOf(argv[i + 1]);
|
|
53
|
+
i += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`unknown argument '${arg}' — ${USAGE}`);
|
|
57
|
+
}
|
|
58
|
+
return { port, owner };
|
|
59
|
+
}
|
|
60
|
+
export function requireOwner(owner, dir = agentsDir()) {
|
|
61
|
+
if (owner !== null || existsSync(join(dir, '.owner')))
|
|
62
|
+
return;
|
|
63
|
+
throw new Error('no owner is set for this machine, so no wallet could sign in.\n' +
|
|
64
|
+
'Pass the wallet that owns it once; it is remembered in ' +
|
|
65
|
+
join(dir, '.owner') +
|
|
66
|
+
':\n metro serve --owner <address>');
|
|
67
|
+
}
|
|
68
|
+
export function readOwner(dir = agentsDir()) {
|
|
69
|
+
try {
|
|
70
|
+
const raw = readFileSync(join(dir, '.owner'), 'utf8').trim().toLowerCase();
|
|
71
|
+
return ADDRESS.test(raw) ? raw : null;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function holdInfo(port, owner, tailscaleBin) {
|
|
78
|
+
return {
|
|
79
|
+
port,
|
|
80
|
+
host: process.env.METRO_HTTP_HOST ?? '127.0.0.1',
|
|
81
|
+
owner: owner ?? readOwner(),
|
|
82
|
+
version: currentVersion(),
|
|
83
|
+
funnel: tailscaleBin,
|
|
84
|
+
lockFile: join(process.env.METRO_STATE_DIR ?? serveStateDir(), '.tail-lock'),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function tailscaleStatus(bin) {
|
|
88
|
+
const run = spawnSync(bin, ['status', '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
89
|
+
if (run.error !== undefined || run.status !== 0)
|
|
90
|
+
return { ok: false, state: 'not running' };
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(run.stdout);
|
|
93
|
+
return parsed.BackendState === 'Running' ? { ok: true } : { ok: false, state: String(parsed.BackendState) };
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return { ok: false, state: 'unreadable' };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export function findTailscale(candidates = ['tailscale', TAILSCALE_APP]) {
|
|
100
|
+
const bin = candidates.find((c) => spawnSync(c, ['version'], { stdio: 'ignore' }).status === 0);
|
|
101
|
+
if (bin === undefined)
|
|
102
|
+
throw new Error('metro serve needs Tailscale on this machine: the daemon is published through Tailscale Funnel.\n' +
|
|
103
|
+
'macOS: brew install --cask tailscale (or the App Store app)\n' +
|
|
104
|
+
'Linux: curl -fsSL https://tailscale.com/install.sh | sh\n' +
|
|
105
|
+
'Then sign the machine in: tailscale up');
|
|
106
|
+
const status = tailscaleStatus(bin);
|
|
107
|
+
if (!status.ok)
|
|
108
|
+
throw new Error(`Tailscale is installed but this machine is not connected (${status.state}). Sign it in first: tailscale up\n` +
|
|
109
|
+
'Funnel also has to be enabled once on your tailnet: https://tailscale.com/kb/1223/funnel');
|
|
110
|
+
return bin;
|
|
111
|
+
}
|
|
112
|
+
export function servePlan(opts) {
|
|
113
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !SCRUBBED.has(key)));
|
|
114
|
+
return {
|
|
115
|
+
command: findBun(),
|
|
116
|
+
args: [SERVER_ENTRY],
|
|
117
|
+
cwd: opts.runtime.dir,
|
|
118
|
+
env: {
|
|
119
|
+
...env,
|
|
120
|
+
METRO_MODE: 'local',
|
|
121
|
+
METRO_VERSION: currentVersion(),
|
|
122
|
+
METRO_CLI_BIN: process.argv[1] ?? '',
|
|
123
|
+
METRO_WEBHOOK_PORT: String(opts.port),
|
|
124
|
+
METRO_HTTP_HOST: process.env.METRO_HTTP_HOST ?? '127.0.0.1',
|
|
125
|
+
METRO_TRAINS_DIR: opts.runtime.trains,
|
|
126
|
+
...(opts.runtime.manifest === null
|
|
127
|
+
? {}
|
|
128
|
+
: { METRO_RUNTIME_STORE: opts.runtime.dir, METRO_RUNTIME_MANIFEST: opts.runtime.manifest }),
|
|
129
|
+
METRO_STATE_DIR: process.env.METRO_STATE_DIR ?? serveStateDir(),
|
|
130
|
+
METRO_TUNNEL: 'tailscale',
|
|
131
|
+
METRO_TAILSCALE_BIN: opts.tailscaleBin,
|
|
132
|
+
...(opts.owner === null ? {} : { METRO_OWNER: opts.owner }),
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export function serve(argv) {
|
|
137
|
+
const { port, owner } = parseServeArgs(argv);
|
|
138
|
+
const running = serveLockedBy();
|
|
139
|
+
if (running !== null)
|
|
140
|
+
throw new Error(`a metro serve daemon is already running on this machine (pid ${String(running)}). ` +
|
|
141
|
+
'Stop it first: metro stop');
|
|
142
|
+
requireOwner(owner);
|
|
143
|
+
const tailscaleBin = findTailscale();
|
|
144
|
+
const node = ensureNodeName(tailscaleBin);
|
|
145
|
+
process.stderr.write(`Starting a metro daemon of your own on http://127.0.0.1:${String(port)}, published as ${node} on your tailnet\n`);
|
|
146
|
+
return spawnPlan(() => servePlan({ runtime: prepareRuntime(), port, owner, tailscaleBin }), () => holdUntilStart(holdInfo(port, owner, tailscaleBin)));
|
|
147
|
+
}
|
package/dist/service.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir, platform, userInfo } from 'node:os';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { serveLockedBy } from './control.js';
|
|
6
|
+
import { findBun } from './runtime.js';
|
|
7
|
+
import { findTailscale, parseServeArgs, requireOwner } from './serve.js';
|
|
8
|
+
const USAGE = 'usage: metro service install [--port <n>] [--owner <address>] | uninstall | status';
|
|
9
|
+
const SERVICE = 'metro';
|
|
10
|
+
const LABEL = 'box.metro.serve';
|
|
11
|
+
const CARRIED = /^(METRO_.*|XDG_CACHE_HOME|PATH|HOME)$/;
|
|
12
|
+
const PLAIN_WORD = /^[A-Za-z0-9_/.:=+@,-]+$/;
|
|
13
|
+
const carriedEnv = (env) => Object.entries(env)
|
|
14
|
+
.filter((entry) => CARRIED.test(entry[0]) && typeof entry[1] === 'string' && entry[1] !== '')
|
|
15
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
16
|
+
const cacheDir = (host) => {
|
|
17
|
+
const xdg = host.env.XDG_CACHE_HOME?.trim();
|
|
18
|
+
return xdg === undefined || xdg === '' ? join(host.home, '.cache') : xdg;
|
|
19
|
+
};
|
|
20
|
+
function unitWord(arg) {
|
|
21
|
+
const escaped = arg.replaceAll('%', '%%');
|
|
22
|
+
if (PLAIN_WORD.test(arg))
|
|
23
|
+
return escaped;
|
|
24
|
+
return `"${escaped.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
25
|
+
}
|
|
26
|
+
export function systemdUnit(host, exec, target) {
|
|
27
|
+
const env = carriedEnv(host.env).map(([key, value]) => `Environment=${unitWord(`${key}=${value}`)}`);
|
|
28
|
+
return [
|
|
29
|
+
'[Unit]',
|
|
30
|
+
'Description=Metro daemon (metro serve)',
|
|
31
|
+
'After=network-online.target tailscaled.service',
|
|
32
|
+
'Wants=network-online.target',
|
|
33
|
+
'',
|
|
34
|
+
'[Service]',
|
|
35
|
+
`ExecStart=${exec.map(unitWord).join(' ')}`,
|
|
36
|
+
'Restart=always',
|
|
37
|
+
'RestartSec=2',
|
|
38
|
+
`WorkingDirectory=${unitWord(host.home)}`,
|
|
39
|
+
...env,
|
|
40
|
+
'',
|
|
41
|
+
'[Install]',
|
|
42
|
+
`WantedBy=${target}`,
|
|
43
|
+
'',
|
|
44
|
+
].join('\n');
|
|
45
|
+
}
|
|
46
|
+
const xml = (text) => text
|
|
47
|
+
.replaceAll('&', '&')
|
|
48
|
+
.replaceAll('<', '<')
|
|
49
|
+
.replaceAll('>', '>')
|
|
50
|
+
.replaceAll('"', '"')
|
|
51
|
+
.replaceAll("'", ''');
|
|
52
|
+
export function launchdPlist(host, exec, logFile) {
|
|
53
|
+
const env = carriedEnv(host.env).map(([key, value]) => ` <key>${xml(key)}</key><string>${xml(value)}</string>`);
|
|
54
|
+
return [
|
|
55
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
56
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
57
|
+
'<plist version="1.0">',
|
|
58
|
+
'<dict>',
|
|
59
|
+
` <key>Label</key><string>${LABEL}</string>`,
|
|
60
|
+
' <key>ProgramArguments</key>',
|
|
61
|
+
' <array>',
|
|
62
|
+
...exec.map((arg) => ` <string>${xml(arg)}</string>`),
|
|
63
|
+
' </array>',
|
|
64
|
+
' <key>EnvironmentVariables</key>',
|
|
65
|
+
' <dict>',
|
|
66
|
+
...env,
|
|
67
|
+
' </dict>',
|
|
68
|
+
` <key>WorkingDirectory</key><string>${xml(host.home)}</string>`,
|
|
69
|
+
' <key>RunAtLoad</key><true/>',
|
|
70
|
+
' <key>KeepAlive</key><true/>',
|
|
71
|
+
` <key>StandardOutPath</key><string>${xml(logFile)}</string>`,
|
|
72
|
+
` <key>StandardErrorPath</key><string>${xml(logFile)}</string>`,
|
|
73
|
+
'</dict>',
|
|
74
|
+
'</plist>',
|
|
75
|
+
'',
|
|
76
|
+
].join('\n');
|
|
77
|
+
}
|
|
78
|
+
function systemdPlan(host, exec) {
|
|
79
|
+
const system = host.uid === 0;
|
|
80
|
+
const ctl = system ? ['systemctl'] : ['systemctl', '--user'];
|
|
81
|
+
const file = system
|
|
82
|
+
? `/etc/systemd/system/${SERVICE}.service`
|
|
83
|
+
: join(host.home, '.config', 'systemd', 'user', `${SERVICE}.service`);
|
|
84
|
+
return {
|
|
85
|
+
kind: 'systemd',
|
|
86
|
+
file,
|
|
87
|
+
content: systemdUnit(host, exec, system ? 'multi-user.target' : 'default.target'),
|
|
88
|
+
dirs: [],
|
|
89
|
+
install: [{ args: [...ctl, 'daemon-reload'] }, { args: [...ctl, 'enable', '--now', SERVICE] }],
|
|
90
|
+
uninstall: [{ args: [...ctl, 'disable', '--now', SERVICE], mayFail: true }],
|
|
91
|
+
reload: [{ args: [...ctl, 'daemon-reload'], mayFail: true }],
|
|
92
|
+
status: { args: [...ctl, 'is-active', SERVICE], mayFail: true },
|
|
93
|
+
hints: [
|
|
94
|
+
`Logs: journalctl ${system ? '' : '--user '}-u ${SERVICE} -f`,
|
|
95
|
+
...(system
|
|
96
|
+
? []
|
|
97
|
+
: [`A user service stops with your login session; keep it up without one: loginctl enable-linger ${host.user}`]),
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function launchdPlan(host, exec) {
|
|
102
|
+
const file = join(host.home, 'Library', 'LaunchAgents', `${LABEL}.plist`);
|
|
103
|
+
const domain = `gui/${String(host.uid)}`;
|
|
104
|
+
const logFile = join(cacheDir(host), 'metro', 'serve', 'service.log');
|
|
105
|
+
return {
|
|
106
|
+
kind: 'launchd',
|
|
107
|
+
file,
|
|
108
|
+
content: launchdPlist(host, exec, logFile),
|
|
109
|
+
dirs: [dirname(logFile)],
|
|
110
|
+
install: [
|
|
111
|
+
{ args: ['launchctl', 'bootout', `${domain}/${LABEL}`], mayFail: true },
|
|
112
|
+
{ args: ['launchctl', 'bootstrap', domain, file] },
|
|
113
|
+
],
|
|
114
|
+
uninstall: [{ args: ['launchctl', 'bootout', `${domain}/${LABEL}`], mayFail: true }],
|
|
115
|
+
reload: [],
|
|
116
|
+
status: { args: ['launchctl', 'print', `${domain}/${LABEL}`], mayFail: true },
|
|
117
|
+
hints: [`Logs: tail -f ${logFile}`],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function servicePlan(host, serveArgs) {
|
|
121
|
+
const exec = [host.node, host.cli, 'serve', ...serveArgs];
|
|
122
|
+
if (host.platform === 'linux')
|
|
123
|
+
return systemdPlan(host, exec);
|
|
124
|
+
if (host.platform === 'darwin')
|
|
125
|
+
return launchdPlan(host, exec);
|
|
126
|
+
throw new Error(`metro service supports Linux (systemd) and macOS (launchd), not ${host.platform}`);
|
|
127
|
+
}
|
|
128
|
+
function install(serveArgs, deps) {
|
|
129
|
+
const { owner } = parseServeArgs(serveArgs);
|
|
130
|
+
deps.preflight(owner);
|
|
131
|
+
const pid = deps.running();
|
|
132
|
+
if (pid !== null)
|
|
133
|
+
throw new Error(`a metro serve is running on this machine (pid ${String(pid)}). Stop it first (metro stop), then install: the service takes over from there`);
|
|
134
|
+
const plan = servicePlan(deps.host, serveArgs);
|
|
135
|
+
for (const dir of plan.dirs)
|
|
136
|
+
deps.mkdir(dir);
|
|
137
|
+
deps.write(plan.file, plan.content);
|
|
138
|
+
for (const command of plan.install)
|
|
139
|
+
deps.run(command);
|
|
140
|
+
deps.out(`Installed ${plan.file}: metro serve now starts at boot and after a crash, and the Server page on metro.box can stop, start and restart it.`);
|
|
141
|
+
for (const hint of plan.hints)
|
|
142
|
+
deps.out(hint);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
function uninstall(deps) {
|
|
146
|
+
const plan = servicePlan(deps.host, []);
|
|
147
|
+
if (!deps.exists(plan.file)) {
|
|
148
|
+
deps.out('metro is not installed as a service on this machine');
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
for (const command of plan.uninstall)
|
|
152
|
+
deps.run(command);
|
|
153
|
+
deps.remove(plan.file);
|
|
154
|
+
for (const command of plan.reload)
|
|
155
|
+
deps.run(command);
|
|
156
|
+
deps.out(`Removed ${plan.file}; a running daemon was stopped, and metro serve no longer starts on its own`);
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
function status(deps) {
|
|
160
|
+
const plan = servicePlan(deps.host, []);
|
|
161
|
+
if (!deps.exists(plan.file)) {
|
|
162
|
+
deps.out(`not installed (metro service install writes ${plan.file})`);
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
const active = deps.run(plan.status).status === 0;
|
|
166
|
+
deps.out(active
|
|
167
|
+
? `running as a ${plan.kind} service (${plan.file})`
|
|
168
|
+
: `installed as a ${plan.kind} service (${plan.file}), not running`);
|
|
169
|
+
return active ? 0 : 1;
|
|
170
|
+
}
|
|
171
|
+
function realDeps() {
|
|
172
|
+
const who = userInfo();
|
|
173
|
+
return {
|
|
174
|
+
host: {
|
|
175
|
+
platform: platform(),
|
|
176
|
+
uid: who.uid,
|
|
177
|
+
user: who.username,
|
|
178
|
+
home: homedir(),
|
|
179
|
+
node: process.execPath,
|
|
180
|
+
cli: resolve(process.argv[1] ?? ''),
|
|
181
|
+
env: process.env,
|
|
182
|
+
},
|
|
183
|
+
run: (command) => {
|
|
184
|
+
const [bin = '', ...args] = command.args;
|
|
185
|
+
const result = spawnSync(bin, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
186
|
+
const code = result.status ?? 1;
|
|
187
|
+
if (code !== 0 && command.mayFail !== true) {
|
|
188
|
+
const said = [result.stderr, result.stdout, result.error?.message].find((t) => typeof t === 'string' && t.trim() !== '') ?? '';
|
|
189
|
+
throw new Error(`${command.args.join(' ')} failed (exit ${String(code)}): ${said.trim()}`);
|
|
190
|
+
}
|
|
191
|
+
return { status: code, output: `${result.stdout}${result.stderr}` };
|
|
192
|
+
},
|
|
193
|
+
running: serveLockedBy,
|
|
194
|
+
preflight: (owner) => {
|
|
195
|
+
requireOwner(owner);
|
|
196
|
+
findBun();
|
|
197
|
+
findTailscale();
|
|
198
|
+
},
|
|
199
|
+
mkdir: (dir) => {
|
|
200
|
+
mkdirSync(dir, { recursive: true });
|
|
201
|
+
},
|
|
202
|
+
write: (file, content) => {
|
|
203
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
204
|
+
writeFileSync(file, content, { mode: 0o644 });
|
|
205
|
+
},
|
|
206
|
+
remove: (file) => {
|
|
207
|
+
rmSync(file, { force: true });
|
|
208
|
+
},
|
|
209
|
+
exists: existsSync,
|
|
210
|
+
out: (line) => {
|
|
211
|
+
process.stderr.write(`${line}\n`);
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
export function service(argv, deps = realDeps()) {
|
|
216
|
+
const [verb, ...rest] = argv;
|
|
217
|
+
if (verb === 'install')
|
|
218
|
+
return Promise.resolve(install(rest, deps));
|
|
219
|
+
if (verb === 'uninstall' && rest.length === 0)
|
|
220
|
+
return Promise.resolve(uninstall(deps));
|
|
221
|
+
if (verb === 'status' && rest.length === 0)
|
|
222
|
+
return Promise.resolve(status(deps));
|
|
223
|
+
throw new Error(USAGE);
|
|
224
|
+
}
|
package/dist/tail.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { localAgents } from './local.js';
|
|
2
|
+
import { assertAgentId, localUrl } from './runtime.js';
|
|
3
|
+
const BACKOFF_MS = [1_000, 2_000, 5_000];
|
|
4
|
+
const STALL_MS = 90_000;
|
|
5
|
+
export const backoffAt = (attempt) => BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)] ?? 5_000;
|
|
6
|
+
export function sseDataLines(buffer, chunk) {
|
|
7
|
+
const lines = (buffer + chunk).split('\n');
|
|
8
|
+
const rest = lines.pop() ?? '';
|
|
9
|
+
const events = [];
|
|
10
|
+
for (const raw of lines) {
|
|
11
|
+
const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
|
|
12
|
+
if (!line.startsWith('data:'))
|
|
13
|
+
continue;
|
|
14
|
+
const value = line.slice(5);
|
|
15
|
+
events.push(value.startsWith(' ') ? value.slice(1) : value);
|
|
16
|
+
}
|
|
17
|
+
return { rest, events };
|
|
18
|
+
}
|
|
19
|
+
function keySource(agentId) {
|
|
20
|
+
const fromEnv = process.env.METRO_AGENT_KEY?.trim();
|
|
21
|
+
if (fromEnv !== undefined && fromEnv !== '')
|
|
22
|
+
return { resolve: () => Promise.resolve(fromEnv), fixed: true };
|
|
23
|
+
return {
|
|
24
|
+
resolve: () => {
|
|
25
|
+
const agent = localAgents().find((a) => a.id === agentId);
|
|
26
|
+
if (agent === undefined)
|
|
27
|
+
throw new Error(`no agent ${agentId} on this machine — create or restore it in the web UI of metro serve, or set METRO_AGENT_KEY`);
|
|
28
|
+
return Promise.resolve(agent.key);
|
|
29
|
+
},
|
|
30
|
+
fixed: false,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const armStall = (control) => setTimeout(() => {
|
|
34
|
+
control.abort();
|
|
35
|
+
}, STALL_MS);
|
|
36
|
+
async function connect(key, signal) {
|
|
37
|
+
try {
|
|
38
|
+
return await fetch(`${localUrl()}/api/tail`, {
|
|
39
|
+
headers: { authorization: `Bearer ${key}` },
|
|
40
|
+
signal,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function pump(body, control, stall) {
|
|
48
|
+
const reader = body.getReader();
|
|
49
|
+
const decoder = new TextDecoder();
|
|
50
|
+
let buffer = '';
|
|
51
|
+
try {
|
|
52
|
+
for (;;) {
|
|
53
|
+
const { done, value } = await reader.read();
|
|
54
|
+
if (done)
|
|
55
|
+
return 'ended';
|
|
56
|
+
clearTimeout(stall.timer);
|
|
57
|
+
stall.timer = armStall(control);
|
|
58
|
+
const out = sseDataLines(buffer, decoder.decode(value, { stream: true }));
|
|
59
|
+
buffer = out.rest;
|
|
60
|
+
for (const event of out.events)
|
|
61
|
+
process.stdout.write(`${event}\n`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return 'ended';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function streamOnce(key) {
|
|
69
|
+
const control = new AbortController();
|
|
70
|
+
const stall = { timer: armStall(control) };
|
|
71
|
+
try {
|
|
72
|
+
const res = await connect(key, control.signal);
|
|
73
|
+
if (res === null)
|
|
74
|
+
return 'unreachable';
|
|
75
|
+
if (res.status === 401)
|
|
76
|
+
return 'unauthorized';
|
|
77
|
+
if (!res.ok || res.body === null)
|
|
78
|
+
return 'unreachable';
|
|
79
|
+
return await pump(res.body, control, stall);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
clearTimeout(stall.timer);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function note(state, message) {
|
|
86
|
+
if (state.last === message)
|
|
87
|
+
return;
|
|
88
|
+
state.last = message;
|
|
89
|
+
process.stderr.write(`metro: ${message}\n`);
|
|
90
|
+
}
|
|
91
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
92
|
+
async function settle(end, source, key, agentId, state) {
|
|
93
|
+
if (end === 'unauthorized') {
|
|
94
|
+
if (source.fixed)
|
|
95
|
+
throw new Error('the key in METRO_AGENT_KEY was refused by the local daemon');
|
|
96
|
+
note(state, 'key refused — fetching the current one');
|
|
97
|
+
return source.resolve();
|
|
98
|
+
}
|
|
99
|
+
if (end === 'unreachable')
|
|
100
|
+
note(state, `no metro daemon on ${localUrl()} — run 'metro serve' (agent ${agentId})`);
|
|
101
|
+
else
|
|
102
|
+
note(state, 'stream ended — reconnecting');
|
|
103
|
+
return key;
|
|
104
|
+
}
|
|
105
|
+
export async function tailEvents(argv) {
|
|
106
|
+
const agentId = assertAgentId(argv[0]);
|
|
107
|
+
const source = keySource(agentId);
|
|
108
|
+
let key = await source.resolve();
|
|
109
|
+
const state = { last: '' };
|
|
110
|
+
let attempt = 0;
|
|
111
|
+
for (;;) {
|
|
112
|
+
const end = await streamOnce(key);
|
|
113
|
+
if (end === 'ended')
|
|
114
|
+
attempt = 0;
|
|
115
|
+
key = await settle(end, source, key, agentId, state);
|
|
116
|
+
await delay(backoffAt(attempt));
|
|
117
|
+
attempt += 1;
|
|
118
|
+
}
|
|
119
|
+
}
|