@moondeck/station 0.0.1 → 1.0.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.
Files changed (326) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -2
  3. package/bin/uai-host.mjs +130 -0
  4. package/db/migrations/0000_host_tasks.sql +12 -0
  5. package/db/migrations/0001_host_ui.sql +11 -0
  6. package/db/migrations/0002_host_github_tokens.sql +8 -0
  7. package/db/migrations/0003_host_ssh_keys.sql +8 -0
  8. package/db/migrations/0004_host_owner_name.sql +1 -0
  9. package/db/migrations/0005_host_env_vars.sql +5 -0
  10. package/db/migrations/0006_host_project_env.sql +9 -0
  11. package/db/migrations/0007_host_github_token_kind.sql +7 -0
  12. package/db/migrations/0008_host_mcp_connections.sql +18 -0
  13. package/db/migrations/0009_host_agent_sessions.sql +15 -0
  14. package/db/migrations/0010_host_engine_accounts.sql +13 -0
  15. package/db/migrations/0011_codex_thread_id.sql +5 -0
  16. package/db/migrations/0012_agent_session_attach_key.sql +4 -0
  17. package/db/migrations/0013_github_credential_generations.sql +11 -0
  18. package/db/migrations/0014_host_inventory_event_index.sql +1 -0
  19. package/db/migrations/0015_host_settings.sql +9 -0
  20. package/db/migrations/0016_task_environment.sql +2 -0
  21. package/db/migrations/0017_engine_account_scope.sql +7 -0
  22. package/db/migrations/0018_engine_account_position.sql +3 -0
  23. package/db/migrations/0019_engine_account_store_ref.sql +3 -0
  24. package/db/migrations/0020_user_credential_store_ref.sql +6 -0
  25. package/db/migrations/0021_project_env_store_ref.sql +3 -0
  26. package/db/migrations/0022_task_project_ids.sql +5 -0
  27. package/db/migrations/0023_project_env_org.sql +4 -0
  28. package/db/migrations/0024_task_env_mode.sql +9 -0
  29. package/db/migrations/0025_project_env_authority.sql +147 -0
  30. package/db/migrations/0026_project_env_replica.sql +150 -0
  31. package/db/migrations/0027_cloud_credential_foundation.sql +51 -0
  32. package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
  33. package/db/migrations/0029_task_shared_files_mode.sql +5 -0
  34. package/db/migrations/0030_credential_cleanup_restops.sql +12 -0
  35. package/db/migrations/meta/_journal.json +223 -0
  36. package/db/schema.ts +850 -0
  37. package/images/standard/Dockerfile +347 -0
  38. package/images/standard/README.md +167 -0
  39. package/images/standard/container/code-server-settings.json +63 -0
  40. package/images/standard/container/corepack-version +1 -0
  41. package/images/standard/container/uai-init +704 -0
  42. package/images/standard/container/uai-materialize-runtimes +1527 -0
  43. package/images/standard/container/uai-task-forwarder.mjs +303 -0
  44. package/images/standard/tool-versions +2 -0
  45. package/lib/__fixtures__/skill-index/SKILL.md +10 -0
  46. package/lib/__fixtures__/skill-index/skills/accessibility-checklist.md +7 -0
  47. package/lib/__fixtures__/skill-index/skills/brand-voice.md +9 -0
  48. package/lib/__fixtures__/skill-index/skills/design-tokens.md +9 -0
  49. package/lib/agent-cli.ts +1507 -0
  50. package/lib/agent-errors.ts +22 -0
  51. package/lib/agent.ts +672 -0
  52. package/lib/agents/attach-compatibility.ts +51 -0
  53. package/lib/agents/claude.ts +918 -0
  54. package/lib/agents/codex-token-usage.fixture.json +130 -0
  55. package/lib/agents/codex.ts +1392 -0
  56. package/lib/agents/cursor-period-usage.ts +253 -0
  57. package/lib/agents/cursor.ts +562 -0
  58. package/lib/agents/dispatch.ts +78 -0
  59. package/lib/agents/durable-proc.ts +334 -0
  60. package/lib/agents/factory.ts +57 -0
  61. package/lib/agents/grok.ts +381 -0
  62. package/lib/agents/kimi.ts +329 -0
  63. package/lib/agents/machine-durable.ts +604 -0
  64. package/lib/agents/mock.ts +133 -0
  65. package/lib/agents/mode.ts +64 -0
  66. package/lib/agents/opencode.ts +508 -0
  67. package/lib/agents/output-source.ts +3 -0
  68. package/lib/agents/proc.ts +221 -0
  69. package/lib/agents/rate-limit.ts +337 -0
  70. package/lib/agents/registry.ts +182 -0
  71. package/lib/agents/session-state.ts +197 -0
  72. package/lib/agents/skill-index-preamble.fixture.ts +26 -0
  73. package/lib/agents/transport.ts +741 -0
  74. package/lib/agents/types.ts +254 -0
  75. package/lib/agents/uai.ts +106 -0
  76. package/lib/agents/usage.ts +44 -0
  77. package/lib/apple-one-off.ts +277 -0
  78. package/lib/apple-runtime-recycle.ts +237 -0
  79. package/lib/apple-uninstall-teardown.ts +224 -0
  80. package/lib/assignment-evidence.ts +94 -0
  81. package/lib/attachments.ts +48 -0
  82. package/lib/browser-testing.ts +1537 -0
  83. package/lib/capture-flow.ts +332 -0
  84. package/lib/cloud-credential-cleanup-adapter.ts +123 -0
  85. package/lib/cloud-credential-host.ts +512 -0
  86. package/lib/cloud-credential-leases.ts +2587 -0
  87. package/lib/cloud-credential-runtime.ts +1357 -0
  88. package/lib/cloud-state.ts +56 -0
  89. package/lib/codex-auth.ts +456 -0
  90. package/lib/command-db.ts +310 -0
  91. package/lib/compose-network-recovery.ts +283 -0
  92. package/lib/connection-secret-redaction.ts +25 -0
  93. package/lib/container-runtime.ts +1453 -0
  94. package/lib/credential-cleanup-inventory.ts +490 -0
  95. package/lib/credential-consumers/engine-anchor.ts +1880 -0
  96. package/lib/credential-consumers/engines.ts +797 -0
  97. package/lib/credential-consumers/files.ts +934 -0
  98. package/lib/credential-consumers/github.ts +702 -0
  99. package/lib/credential-consumers/mcp.ts +389 -0
  100. package/lib/credential-consumers/task-files.ts +127 -0
  101. package/lib/credential-consumers/task-secrets.ts +10 -0
  102. package/lib/credential-store-pickup.ts +149 -0
  103. package/lib/credential-store-scope.ts +112 -0
  104. package/lib/credential-store.ts +932 -0
  105. package/lib/credentials-activation-authority.ts +709 -0
  106. package/lib/credentials-purge.ts +1006 -0
  107. package/lib/custom-connection-capture.ts +395 -0
  108. package/lib/custom-connection-launch.ts +384 -0
  109. package/lib/custom-connection-runtime.ts +141 -0
  110. package/lib/db.ts +80 -0
  111. package/lib/disk-pressure.ts +42 -0
  112. package/lib/docker-exec.ts +156 -0
  113. package/lib/engine-accounts.ts +1970 -0
  114. package/lib/engine-connect.ts +321 -0
  115. package/lib/engine-credential-env.ts +52 -0
  116. package/lib/engine-login.ts +2933 -0
  117. package/lib/engines.ts +1342 -0
  118. package/lib/enrollment-state.ts +251 -0
  119. package/lib/env-file.ts +246 -0
  120. package/lib/env-schema.ts +891 -0
  121. package/lib/env.ts +195 -0
  122. package/lib/fs-aliases.ts +49 -0
  123. package/lib/git-clone-credentials.ts +334 -0
  124. package/lib/git-diff.ts +530 -0
  125. package/lib/git-identity.ts +715 -0
  126. package/lib/github-git-auth.ts +207 -0
  127. package/lib/github-origin.ts +28 -0
  128. package/lib/github-tokens.ts +2439 -0
  129. package/lib/host-cloud-url.ts +62 -0
  130. package/lib/host-config.ts +279 -0
  131. package/lib/host-env.ts +424 -0
  132. package/lib/host-logs.ts +965 -0
  133. package/lib/internal-connections.ts +5 -0
  134. package/lib/keyed-promise-tail.ts +23 -0
  135. package/lib/launch-mode.ts +25 -0
  136. package/lib/lease.ts +239 -0
  137. package/lib/legacy-host.ts +769 -0
  138. package/lib/legacy-runtime-retirement.ts +424 -0
  139. package/lib/legacy-runtime-v1.fixture.ts +627 -0
  140. package/lib/machine-exec.ts +238 -0
  141. package/lib/machine-gateway-tunnel.ts +344 -0
  142. package/lib/machine-keys.ts +65 -0
  143. package/lib/machine-project-env-loader.ts +152 -0
  144. package/lib/machine-provider-aws.ts +923 -0
  145. package/lib/machine-provider-local.ts +338 -0
  146. package/lib/machine-provider.ts +90 -0
  147. package/lib/managed-activation-watcher.ts +72 -0
  148. package/lib/managed-install-owner-watcher.ts +55 -0
  149. package/lib/managed-operation-drain.ts +57 -0
  150. package/lib/managed-runtime.ts +4296 -0
  151. package/lib/managed-update-scheduler.ts +125 -0
  152. package/lib/mcp-capture.ts +865 -0
  153. package/lib/mcp-config-lock.ts +2 -0
  154. package/lib/mcp-connections.ts +959 -0
  155. package/lib/mcp-gateway.ts +2594 -0
  156. package/lib/obs.ts +512 -0
  157. package/lib/openrouter-budget.ts +247 -0
  158. package/lib/orchestrator.ts +11409 -0
  159. package/lib/person-capture.ts +327 -0
  160. package/lib/preview-ports.ts +85 -0
  161. package/lib/preview-sidecar.ts +331 -0
  162. package/lib/project-env-authority.ts +1319 -0
  163. package/lib/project-env-feature.ts +74 -0
  164. package/lib/project-env-legacy-tags.ts +130 -0
  165. package/lib/project-env-loader.ts +242 -0
  166. package/lib/project-env-replica.ts +2094 -0
  167. package/lib/project-env-schema-capture.ts +342 -0
  168. package/lib/project-env-staging.ts +148 -0
  169. package/lib/project-env-task-policy.ts +176 -0
  170. package/lib/project-env-task-replica.ts +1752 -0
  171. package/lib/release-feed.ts +61 -0
  172. package/lib/release-manifest.ts +710 -0
  173. package/lib/release-trust.ts +46 -0
  174. package/lib/repo-clone.ts +38 -0
  175. package/lib/routing/shadow.ts +261 -0
  176. package/lib/runner-boot.ts +10 -0
  177. package/lib/runtime-activation-tail.ts +279 -0
  178. package/lib/runtime-archive.ts +1216 -0
  179. package/lib/runtime-authority.ts +166 -0
  180. package/lib/runtime-guard.ts +36 -0
  181. package/lib/runtime-provider-state.ts +169 -0
  182. package/lib/runtime-reconnect.ts +86 -0
  183. package/lib/runtime-state.ts +1033 -0
  184. package/lib/safe-http.ts +364 -0
  185. package/lib/secrets-proxy/address.ts +95 -0
  186. package/lib/secrets-proxy/audit.ts +30 -0
  187. package/lib/secrets-proxy/ca-store.ts +200 -0
  188. package/lib/secrets-proxy/ca.ts +470 -0
  189. package/lib/secrets-proxy/container-steps.ts +138 -0
  190. package/lib/secrets-proxy/der.ts +273 -0
  191. package/lib/secrets-proxy/engine.ts +1633 -0
  192. package/lib/secrets-proxy/host.ts +93 -0
  193. package/lib/secrets-proxy/hostname.ts +57 -0
  194. package/lib/secrets-proxy/placeholders.ts +196 -0
  195. package/lib/secrets-proxy/policy.ts +150 -0
  196. package/lib/secrets-proxy/readiness.ts +148 -0
  197. package/lib/secrets-proxy/runtime.ts +582 -0
  198. package/lib/secrets-proxy/scrub.ts +559 -0
  199. package/lib/secrets-proxy/sidecar-view.ts +139 -0
  200. package/lib/secrets-proxy/task-scripts.ts +262 -0
  201. package/lib/secrets-proxy/task-trust.ts +605 -0
  202. package/lib/secrets-proxy/trust-watch.ts +80 -0
  203. package/lib/secrets.ts +71 -0
  204. package/lib/shared-files-s3.ts +976 -0
  205. package/lib/shared-files.ts +156 -0
  206. package/lib/sidecar-assign.ts +49 -0
  207. package/lib/sidecar-guard.ts +58 -0
  208. package/lib/sidecar-resume.ts +178 -0
  209. package/lib/sidecar.ts +69 -0
  210. package/lib/signin-box-credentials.ts +465 -0
  211. package/lib/signin-box.ts +114 -0
  212. package/lib/skill-index-example.fixture.ts +56 -0
  213. package/lib/skill-layout.ts +417 -0
  214. package/lib/skills.ts +489 -0
  215. package/lib/ssh.ts +401 -0
  216. package/lib/standard-image-cleanup.ts +117 -0
  217. package/lib/standard-image-pin.ts +207 -0
  218. package/lib/standard-image.ts +1697 -0
  219. package/lib/stopped-task-status-queue.ts +120 -0
  220. package/lib/task-container-cli.ts +269 -0
  221. package/lib/task-credential-cleanup.ts +752 -0
  222. package/lib/task-diff-sandbox.ts +464 -0
  223. package/lib/task-diff.ts +91 -0
  224. package/lib/task-env-forwarder.ts +228 -0
  225. package/lib/task-env-source.ts +249 -0
  226. package/lib/task-environment/anchor.ts +31 -0
  227. package/lib/task-environment/apple-container.ts +860 -0
  228. package/lib/task-environment/docker.ts +1344 -0
  229. package/lib/task-environment/index.ts +466 -0
  230. package/lib/task-environment/legacy-adoption.ts +467 -0
  231. package/lib/task-environment/machine-environment.ts +61 -0
  232. package/lib/task-environment/machine-task-up.ts +1227 -0
  233. package/lib/task-environment/machine.ts +974 -0
  234. package/lib/task-environment/registry.ts +59 -0
  235. package/lib/task-environment/self-identity.ts +20 -0
  236. package/lib/task-environment/self.ts +566 -0
  237. package/lib/task-environment/types.ts +642 -0
  238. package/lib/task-environment/workspace-files.ts +685 -0
  239. package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
  240. package/lib/task-files-machine-retirement-outbox.ts +592 -0
  241. package/lib/task-files-machine-retirement.ts +116 -0
  242. package/lib/task-files-readiness.ts +127 -0
  243. package/lib/task-gateway-port.ts +22 -0
  244. package/lib/task-gateway-protocol.ts +190 -0
  245. package/lib/task-gateway-recovery.ts +47 -0
  246. package/lib/task-gateway-state.ts +512 -0
  247. package/lib/task-identity.ts +19 -0
  248. package/lib/task-inventory.ts +682 -0
  249. package/lib/task-machine-credentials-activation.ts +95 -0
  250. package/lib/task-machine-swap-safety.ts +729 -0
  251. package/lib/task-person-credentials.ts +76 -0
  252. package/lib/task-project-ops-host.ts +147 -0
  253. package/lib/task-project-ops.ts +1062 -0
  254. package/lib/task-secret-staging.ts +16 -0
  255. package/lib/task-status.ts +46 -0
  256. package/lib/task-token.ts +46 -0
  257. package/lib/token-broker-adopt.ts +344 -0
  258. package/lib/token-broker-lambda.ts +105 -0
  259. package/lib/token-broker-local.ts +392 -0
  260. package/lib/token-broker-revision.ts +310 -0
  261. package/lib/token-broker-ssm.ts +525 -0
  262. package/lib/token-broker.ts +218 -0
  263. package/lib/token-lanes/openai-oauth.ts +123 -0
  264. package/lib/token-lanes/registry.ts +45 -0
  265. package/lib/token-lanes/types.ts +163 -0
  266. package/lib/token-lanes/xai-oauth.ts +165 -0
  267. package/lib/transcript.ts +128 -0
  268. package/lib/tunnel-registry.ts +220 -0
  269. package/lib/tunnel-runtime.ts +235 -0
  270. package/lib/ulid.ts +16 -0
  271. package/lib/user-bus-env.ts +116 -0
  272. package/lib/user-credentials-store.ts +435 -0
  273. package/lib/vnode-pressure.ts +142 -0
  274. package/package.json +115 -8
  275. package/runner/runner.mjs +442 -0
  276. package/scripts/agent/_common.sh +633 -0
  277. package/scripts/agent/env-hook.sh +626 -0
  278. package/scripts/agent/moondeck-cli.sh +5 -0
  279. package/scripts/agent/task-down.sh +291 -0
  280. package/scripts/agent/task-status.sh +70 -0
  281. package/scripts/agent/task-up.sh +3761 -0
  282. package/scripts/agent/uai-env-exec.sh +26 -0
  283. package/scripts/install/darwin.ts +1048 -0
  284. package/scripts/install/linux.ts +958 -0
  285. package/scripts/install/types.ts +78 -0
  286. package/scripts/install/util.ts +254 -0
  287. package/scripts/install/win.ts +143 -0
  288. package/src/activation-respawn.ts +155 -0
  289. package/src/apple-tunnel-route.ts +104 -0
  290. package/src/capture-wire.ts +102 -0
  291. package/src/cli.ts +2583 -0
  292. package/src/cloud-credentials.ts +2144 -0
  293. package/src/cloud-origins.ts +23 -0
  294. package/src/credential-location.ts +248 -0
  295. package/src/credential-wire-validation.ts +25 -0
  296. package/src/custom-connections.ts +237 -0
  297. package/src/engine-account-paths.ts +11 -0
  298. package/src/engine-models.ts +39 -0
  299. package/src/engine-usage.ts +104 -0
  300. package/src/event-outbox.ts +185 -0
  301. package/src/index.ts +2899 -0
  302. package/src/load-env.ts +97 -0
  303. package/src/machine-tunnel-route.ts +156 -0
  304. package/src/main.ts +6349 -0
  305. package/src/never-proxied-domains.ts +31 -0
  306. package/src/outbound-frame.ts +31 -0
  307. package/src/paths.ts +81 -0
  308. package/src/project-env-latest.ts +130 -0
  309. package/src/project-env-policy.ts +396 -0
  310. package/src/project-env-wording.ts +53 -0
  311. package/src/protocol.ts +5545 -0
  312. package/src/runtime-bootstrap.ts +165 -0
  313. package/src/station-identity.ts +96 -0
  314. package/src/task-env-names.ts +28 -0
  315. package/src/task-secrets.ts +75 -0
  316. package/src/tunnel-dial.ts +102 -0
  317. package/src/ui/server.ts +852 -0
  318. package/src/ui/types.ts +173 -0
  319. package/src/workspace-path-policy.ts +41 -0
  320. package/ui/app.js +1102 -0
  321. package/ui/index.html +90 -0
  322. package/ui/style.css +685 -0
  323. package/ui/uai-favicon.svg +21 -0
  324. package/ui/uai-logo-black.svg +9 -0
  325. package/ui/uai-wheel.svg +9 -0
  326. package/bin/moondeck-station.mjs +0 -3
@@ -0,0 +1,633 @@
1
+ #!/usr/bin/env bash
2
+ # uai agent — shared helpers.
3
+ #
4
+ # Every agent script source's this file at the top. It establishes the
5
+ # strict-mode posture, structured JSON output contract, sqlite access
6
+ # helpers, and a uniform error trap.
7
+ #
8
+ # Output contract (docs/agent.md):
9
+ #
10
+ # success → stdout: {"ok":true,"data":{...}}
11
+ # failure → stderr: {"ok":false,"error":{"code":"...","message":"...","step":"..."}}
12
+ # exit non-zero.
13
+ #
14
+ # Environment:
15
+ # UAI_DB_PATH absolute path to uai.sqlite (required)
16
+ # UAI_OWNER_HOME host $HOME (for ~/.claude, ~/.codex) (default: $HOME)
17
+ # UAI_TTYD_BIND ttyd listen address (default: 127.0.0.1)
18
+ # UAI_LOG_DIR directory for ttyd / docker logs (default: $UAI_DATA_DIR/logs)
19
+ # UAI_DATA_DIR base data dir (default: dirname of UAI_DB_PATH)
20
+
21
+ set -euo pipefail
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Strict env.
25
+ # ---------------------------------------------------------------------------
26
+
27
+ : "${UAI_DB_PATH:?UAI_DB_PATH must be set (path to uai.sqlite)}"
28
+
29
+ UAI_DATA_DIR="${UAI_DATA_DIR:-$(dirname "$UAI_DB_PATH")}"
30
+ UAI_OWNER_HOME="${UAI_OWNER_HOME:-$HOME}"
31
+ # The station's storage tree (lib/env.ts: ~/.moondeck unless configured).
32
+ # task-up / task-down derive the per-project mirrors, per-task dirs,
33
+ # browsers and shared files from here. The station passes it explicitly
34
+ # (lib/agent.ts); there is no default, because a wrong one builds a task
35
+ # somewhere the station then refuses (rehearsal #2, 2026-11-02).
36
+ : "${UAI_WORKSPACE_ROOT:?UAI_WORKSPACE_ROOT must be set (the station passes its workspace root)}"
37
+ UAI_TTYD_BIND="${UAI_TTYD_BIND:-127.0.0.1}"
38
+ UAI_LOG_DIR="${UAI_LOG_DIR:-$UAI_DATA_DIR/logs}"
39
+ mkdir -p "$UAI_LOG_DIR"
40
+
41
+ # Optional dependencies — fail fast with a structured error if missing.
42
+ # docker is only a dependency of the docker runtime mode: the Apple runtime
43
+ # path must work on a Docker-free Mac (ADR-106), and its own CLI is validated
44
+ # by validate_task_runtime_selection instead.
45
+ agent_required_bins="sqlite3 jq git"
46
+ if [ "${UAI_TASK_RUNTIME:-docker}" != "apple-container" ]; then
47
+ agent_required_bins="$agent_required_bins docker"
48
+ fi
49
+ # Collect EVERY missing tool before failing: reporting them one at a time
50
+ # cost one full fix-and-Resume round trip per binary on a fresh Amazon
51
+ # Linux box (sqlite3, then jq — live 2026-08-21). Bash 3.2: plain string
52
+ # accumulation, no arrays.
53
+ missing_bins=""
54
+ for bin in $agent_required_bins; do
55
+ if ! command -v "$bin" >/dev/null 2>&1; then
56
+ missing_bins="$missing_bins $bin"
57
+ fi
58
+ done
59
+ if [ -n "$missing_bins" ]; then
60
+ missing_bins="${missing_bins# }"
61
+ printf '{"ok":false,"error":{"code":"MISSING_DEPENDENCY","message":"missing on PATH: %s — install with your package manager (dnf/apt/brew), then Resume"}}\n' "$missing_bins" >&2
62
+ exit 127
63
+ fi
64
+
65
+
66
+ # The docker BINARY existing does not make `docker compose` work: the v2
67
+ # compose plugin ships separately, and a bare-engine install (Amazon Linux
68
+ # 2023's docker package) answers `docker compose` with the top-level help
69
+ # text — the operator saw exactly that as their task error (live
70
+ # 2026-08-21). Called LAZILY by the task scripts right before their first
71
+ # compose use, never at source time: argument validation must precede any
72
+ # docker invocation (the task-down/task-up tests enforce that ordering).
73
+ uai_require_compose_plugin() {
74
+ [ "${UAI_TASK_RUNTIME:-docker}" = "apple-container" ] && return 0
75
+ if ! docker compose version >/dev/null 2>&1; then
76
+ printf '{"ok":false,"error":{"code":"MISSING_DEPENDENCY","message":"missing on PATH: docker-compose-plugin — install the Docker Compose v2 plugin, then Resume"}}\n' >&2
77
+ exit 127
78
+ fi
79
+ }
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # Logger. All log lines go to stderr so stdout stays reserved for JSON.
83
+ # ---------------------------------------------------------------------------
84
+
85
+ _uai_ts() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
86
+
87
+ log() { printf '[uai-agent %s] %s\n' "$(_uai_ts)" "$*" >&2; }
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # JSON output helpers.
91
+ # ---------------------------------------------------------------------------
92
+
93
+ # emit_ok '{"foo":"bar"}'
94
+ #
95
+ # NOTE: do not use `${1:-{}}` here — bash parses `{` inside the default
96
+ # as a literal and the matching `}` closes the substitution prematurely,
97
+ # so a real payload gets a trailing `}` appended. Use an explicit check.
98
+ emit_ok() {
99
+ local data="$1"
100
+ if [ -z "$data" ]; then data='{}'; fi
101
+ printf '{"ok":true,"data":%s}\n' "$data"
102
+ }
103
+
104
+ # emit_err <code> <message> [step]
105
+ #
106
+ # Persists status='error' on the current task (if UAI_TASK_ID is set),
107
+ # logs an `error` event, prints structured JSON to stderr, and exits 1.
108
+ emit_err() {
109
+ local code="$1" msg="$2" step="${3:-${UAI_CURRENT_STEP:-}}"
110
+ _uai_record_failure "$code" "$step"
111
+ local json
112
+ if [ -n "$step" ]; then
113
+ json=$(jq -nc --arg c "$code" --arg m "$msg" --arg s "$step" \
114
+ '{ok:false,error:{code:$c,message:$m,step:$s}}')
115
+ else
116
+ json=$(jq -nc --arg c "$code" --arg m "$msg" \
117
+ '{ok:false,error:{code:$c,message:$m}}')
118
+ fi
119
+ printf '%s\n' "$json" >&2
120
+ exit 1
121
+ }
122
+
123
+ # Docker can disappear after the TypeScript admission check but before a
124
+ # minutes-long compose/init command finishes. These codes describe steps whose
125
+ # failure may be a daemon outage. Keep the command snapshot retryable here; the
126
+ # host process re-probes `docker info` and records a terminal error only when
127
+ # the daemon is positively reachable and the operation itself really failed.
128
+ _uai_is_runtime_sensitive_error() {
129
+ case "$1" in
130
+ STANDARD_IMAGE_MISSING|COMPOSE_UP_FAILED|COMPOSE_DOWN_FAILED|CONTAINER_INIT_FAILED|DOCKER_PS_FAILED)
131
+ return 0
132
+ ;;
133
+ *)
134
+ return 1
135
+ ;;
136
+ esac
137
+ }
138
+
139
+ _uai_mark_retryable() {
140
+ if [ -n "${UAI_TASK_ID:-}" ] && [ -n "${UAI_DB_PATH:-}" ]; then
141
+ local restore=""
142
+ if [ -n "${UAI_RETRY_STATUS:-}" ]; then
143
+ restore=", status='$(sql_escape "$UAI_RETRY_STATUS")'"
144
+ fi
145
+ sqlite_exec "UPDATE uai_tasks SET locked_at=NULL${restore}, updated_at=(unixepoch()*1000) WHERE id='$(sql_escape "$UAI_TASK_ID")'" 2>/dev/null || true
146
+ fi
147
+ UAI_LOCK_HELD=""
148
+ }
149
+
150
+ _uai_record_failure() {
151
+ local code="$1" step="$2"
152
+ if _uai_is_runtime_sensitive_error "$code"; then
153
+ _uai_mark_retryable
154
+ else
155
+ _uai_mark_error "$code" "$step"
156
+ fi
157
+ }
158
+
159
+ # _uai_mark_error <code> <step> — best-effort persist of status=error.
160
+ # Shared between emit_err (explicit) and the ERR trap (set -e).
161
+ _uai_mark_error() {
162
+ local code="$1" step="$2"
163
+ if [ -n "${UAI_TASK_ID:-}" ] && [ -n "${UAI_DB_PATH:-}" ]; then
164
+ sqlite_exec "UPDATE uai_tasks SET status='error', locked_at=NULL, updated_at=(unixepoch()*1000) WHERE id='$(sql_escape "$UAI_TASK_ID")'" 2>/dev/null || true
165
+ local payload
166
+ payload=$(jq -nc --arg c "$code" --arg s "$step" '{code:$c,step:$s}')
167
+ sqlite_exec "INSERT INTO uai_task_events (id, task_id, kind, payload) VALUES ('$(sql_escape "$(_uai_event_id)")', '$(sql_escape "$UAI_TASK_ID")', 'error', '$(sql_escape "$payload")')" 2>/dev/null || true
168
+ fi
169
+ }
170
+
171
+ # ---------------------------------------------------------------------------
172
+ # SQLite helpers. The DB is local SQLite; concurrency is single-writer per
173
+ # task (enforced via lock_task below + Next.js).
174
+ # ---------------------------------------------------------------------------
175
+
176
+ # sqlite_exec "<sql>"
177
+ sqlite_exec() {
178
+ sqlite3 "$UAI_DB_PATH" "$1"
179
+ }
180
+
181
+ # sqlite_json "<sql>" → JSON array
182
+ sqlite_json() {
183
+ sqlite3 -json "$UAI_DB_PATH" "$1"
184
+ }
185
+
186
+ # sql_escape <value> → single-quote-safe string for inline SQL.
187
+ #
188
+ # We pipe through sed instead of using bash parameter expansion because
189
+ # bash 3.2 (macOS default) keeps backslashes in `${var//\'/\'\'}` —
190
+ # producing `you\'\'re` from `you're` and breaking SQL string parsing.
191
+ # sed handles it identically on bash 3.2 and 5+.
192
+ sql_escape() {
193
+ printf '%s' "$1" | sed "s/'/''/g"
194
+ }
195
+
196
+ # db_get_task <id> → JSON array (one row) or "[]"
197
+ db_get_task() {
198
+ local out
199
+ out=$(sqlite_json "SELECT * FROM uai_tasks WHERE id='$(sql_escape "$1")'")
200
+ printf '%s' "${out:-[]}"
201
+ }
202
+
203
+ # db_get_projects_for_task <taskId> → JSON array of the task's selected
204
+ # projects, in position order. Each element carries every uai_projects
205
+ # column plus the `position` from uai_task_projects. Empty array when the
206
+ # task has no selected projects (scratchpad).
207
+ db_get_projects_for_task() {
208
+ local out
209
+ out=$(sqlite_json "SELECT p.*, tp.position AS position FROM uai_projects p JOIN uai_task_projects tp ON tp.project_id = p.id WHERE tp.task_id = '$(sql_escape "$1")' ORDER BY tp.position ASC")
210
+ printf '%s' "${out:-[]}"
211
+ }
212
+
213
+ # db_update_task <id> "col=value" "col=value" ...
214
+ #
215
+ # Values must be pre-quoted by the caller (e.g. status='running', port=42).
216
+ db_update_task() {
217
+ local id="$1"; shift
218
+ local set_clause
219
+ set_clause=$(IFS=','; echo "$*")
220
+ sqlite_exec "UPDATE uai_tasks SET ${set_clause}, updated_at=(unixepoch()*1000) WHERE id='$(sql_escape "$id")'"
221
+ }
222
+
223
+ # emit_event <task_id> <kind> <payload-json>
224
+ emit_event() {
225
+ local tid="$1" kind="$2" payload="$3"
226
+ if [ -z "$payload" ]; then payload='{}'; fi
227
+ local eid
228
+ eid=$(_uai_event_id)
229
+ sqlite_exec "INSERT INTO uai_task_events (id, task_id, kind, payload) VALUES ('$(sql_escape "$eid")', '$(sql_escape "$tid")', '$(sql_escape "$kind")', '$(sql_escape "$payload")')"
230
+ }
231
+
232
+ # _uai_event_id — ULID-shaped fallback id generator (Crockford base32 prefix).
233
+ # Real ULIDs come from the Next.js layer; agent-side events use ev_<unix><rand>.
234
+ _uai_event_id() {
235
+ printf 'ev_%s%s' "$(date +%s%N | cut -c1-13)" "$(_uai_rand 10)"
236
+ }
237
+
238
+ _uai_rand() {
239
+ # Lowercase alphanumeric, length $1. Avoids depending on `uuidgen` etc.
240
+ local n="$1"
241
+ LC_ALL=C tr -dc 'a-z0-9' </dev/urandom | head -c "$n"
242
+ }
243
+
244
+ # ---------------------------------------------------------------------------
245
+ # Locking. The Task row carries `locked_at`. Next.js also checks this
246
+ # before invoking the agent; the shell-side lock is a safety net for
247
+ # concurrent invocations of the same script.
248
+ # ---------------------------------------------------------------------------
249
+
250
+ lock_task() {
251
+ local id="$1"
252
+ # Atomic CAS via UPDATE WHERE locked_at IS NULL; success = changes()==1.
253
+ local changes
254
+ changes=$(sqlite_exec "UPDATE uai_tasks SET locked_at=(unixepoch()*1000), updated_at=(unixepoch()*1000) WHERE id='$(sql_escape "$id")' AND locked_at IS NULL; SELECT changes();")
255
+ [ "$changes" = "1" ] || emit_err "TASK_LOCKED" "task $id is already locked" "lock"
256
+ UAI_LOCK_HELD="$id"
257
+ }
258
+
259
+ unlock_task() {
260
+ local id="$1"
261
+ sqlite_exec "UPDATE uai_tasks SET locked_at=NULL, updated_at=(unixepoch()*1000) WHERE id='$(sql_escape "$id")'"
262
+ UAI_LOCK_HELD=""
263
+ }
264
+
265
+ # ---------------------------------------------------------------------------
266
+ # Step tracking + ERR trap.
267
+ #
268
+ # `step <code> <label>` declares the next risky operation. If the script
269
+ # exits non-zero, the trap emits a structured error tagged with the
270
+ # current step's code + label, marks the task `status=error`, and
271
+ # releases the lock.
272
+ # ---------------------------------------------------------------------------
273
+
274
+ UAI_CURRENT_STEP=""
275
+ UAI_ERROR_CODE="UNKNOWN"
276
+ UAI_TASK_ID=""
277
+ UAI_LOCK_HELD=""
278
+ UAI_RETRY_STATUS=""
279
+ UAI_PROJECT_LOCK_PATH=""
280
+ UAI_PROJECT_LOCK_TOKEN=""
281
+
282
+ # Shared project mirrors are mutated by task-up and task-down across different
283
+ # task processes. A task-row lock cannot serialize those operations, so use a
284
+ # portable atomic directory lock (macOS does not ship `flock`). Never reclaim
285
+ # a stale lock automatically: checking a PID and unlinking a fixed path creates
286
+ # a TOCTOU window where a new owner's lock can be deleted. A stale directory is
287
+ # deliberately surfaced for an operator to inspect and remove.
288
+ acquire_project_lock() {
289
+ local projects_root="$1" project_id="$2"
290
+ local project_parent="$projects_root/$project_id"
291
+ local lock_path="$project_parent/.uai-repo.lock"
292
+ local owner_pid attempts=0
293
+ mkdir -p "$project_parent"
294
+ while ! mkdir "$lock_path" 2>/dev/null; do
295
+ if [ ! -d "$lock_path" ]; then
296
+ emit_err "FETCH_FAILED" \
297
+ "project $project_id has an obsolete or invalid repository lock at $lock_path. Confirm no task-up/task-down process is active, remove that lock manually, then retry." \
298
+ "acquire project cache lock"
299
+ fi
300
+ owner_pid=$(cat "$lock_path/pid" 2>/dev/null || true)
301
+ case "$owner_pid" in
302
+ ''|*[!0-9]*) ;;
303
+ *)
304
+ if ! kill -0 "$owner_pid" 2>/dev/null; then
305
+ emit_err "FETCH_FAILED" \
306
+ "project $project_id has a stale repository lock at $lock_path (owner pid $owner_pid). Confirm no task-up/task-down process is active, remove that lock directory manually, then retry." \
307
+ "acquire project cache lock"
308
+ fi
309
+ ;;
310
+ esac
311
+ attempts=$((attempts + 1))
312
+ if [ "$attempts" -ge 300 ]; then
313
+ emit_err "FETCH_FAILED" \
314
+ "project $project_id is busy in another host operation, or has an incomplete lock at $lock_path. Confirm no task-up/task-down process is active; if none is, remove that lock directory manually, then retry." \
315
+ "acquire project cache lock"
316
+ fi
317
+ sleep 0.1
318
+ done
319
+ UAI_PROJECT_LOCK_TOKEN="$$:${RANDOM:-0}"
320
+ printf '%s\n' "$$" > "$lock_path/pid"
321
+ printf '%s\n' "$UAI_PROJECT_LOCK_TOKEN" > "$lock_path/owner"
322
+ UAI_PROJECT_LOCK_PATH="$lock_path"
323
+ }
324
+
325
+ release_project_lock() {
326
+ [ -n "$UAI_PROJECT_LOCK_PATH" ] || return 0
327
+ local owner
328
+ owner=$(cat "$UAI_PROJECT_LOCK_PATH/owner" 2>/dev/null || true)
329
+ if [ -n "$UAI_PROJECT_LOCK_TOKEN" ] \
330
+ && [ "$owner" = "$UAI_PROJECT_LOCK_TOKEN" ]; then
331
+ rm -f "$UAI_PROJECT_LOCK_PATH/pid" "$UAI_PROJECT_LOCK_PATH/owner" \
332
+ 2>/dev/null || true
333
+ rmdir "$UAI_PROJECT_LOCK_PATH" 2>/dev/null || true
334
+ fi
335
+ UAI_PROJECT_LOCK_PATH=""
336
+ UAI_PROJECT_LOCK_TOKEN=""
337
+ }
338
+
339
+ _uai_on_exit() {
340
+ release_project_lock || true
341
+ }
342
+
343
+ step() {
344
+ UAI_ERROR_CODE="$1"
345
+ UAI_CURRENT_STEP="$2"
346
+ log "step: $UAI_CURRENT_STEP"
347
+ }
348
+
349
+ _uai_on_err() {
350
+ local rc=$?
351
+ # Disable further ERR trap recursion.
352
+ trap - ERR
353
+
354
+ local code="$UAI_ERROR_CODE"
355
+ local stepname="$UAI_CURRENT_STEP"
356
+
357
+ _uai_record_failure "$code" "$stepname"
358
+
359
+ local out
360
+ out=$(jq -nc --arg c "$code" --arg m "agent step failed" --arg s "$stepname" --argjson rc "$rc" \
361
+ '{ok:false,error:{code:$c,message:$m,step:$s,exit_code:$rc}}')
362
+ printf '%s\n' "$out" >&2
363
+ exit "$rc"
364
+ }
365
+
366
+ setup_err_trap() {
367
+ trap _uai_on_err ERR
368
+ trap _uai_on_exit EXIT
369
+ }
370
+
371
+ # ---------------------------------------------------------------------------
372
+ # Compose project name + container name conventions.
373
+ # ---------------------------------------------------------------------------
374
+
375
+ # Belt-and-braces: docker compose only accepts lowercase project names.
376
+ # We also lowercase ulids at generation (lib/ulid.ts) but defensive
377
+ # lowercasing here protects against any caller passing mixed case.
378
+ _uai_lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
379
+ compose_project_for_task() { printf 'task-%s' "$(_uai_lower "$1")"; }
380
+ # Compose v2 names containers as `<project>-<service>-<replica>`. We
381
+ # never scale beyond one replica, so the suffix is always `-1`. If we
382
+ # ever do scale, swap to discovering the container name via the
383
+ # `com.docker.compose.project` + `com.docker.compose.service` labels.
384
+ app_container_for_task() { printf 'task-%s-app-1' "$(_uai_lower "$1")"; }
385
+ # ADR-106: the Apple runtime has no Compose, so its app container drops the
386
+ # replica suffix. Must match appleTaskContainerName in
387
+ # lib/task-environment/apple-container.ts.
388
+ apple_app_container_for_task() { printf 'moondeck-task-%s-app' "$(_uai_lower "$1")"; }
389
+ # ADR-118: per-task block volume carrying the pnpm store. Must match
390
+ # appleTaskStoreVolumeName in lib/task-environment/apple-container.ts.
391
+ apple_store_volume_for_task() { printf 'moondeck-store-%s' "$(_uai_lower "$1")"; }
392
+
393
+ # -----------------------------------------------------------------------------
394
+ # ADR-106 container runtime selection. The host publishes the selected backend
395
+ # in UAI_TASK_RUNTIME (+ UAI_CONTAINER_CLI for the bundled Apple toolchain);
396
+ # `docker` keeps every historical path byte-for-byte.
397
+ # -----------------------------------------------------------------------------
398
+ TASK_RUNTIME_MODE="${UAI_TASK_RUNTIME:-docker}"
399
+ CONTAINER_CLI="${UAI_CONTAINER_CLI:-}"
400
+
401
+ validate_task_runtime_selection() {
402
+ case "$TASK_RUNTIME_MODE" in
403
+ docker|apple-container) ;;
404
+ *)
405
+ emit_err "BAD_ARGS" \
406
+ "unsupported UAI_TASK_RUNTIME '$TASK_RUNTIME_MODE'" \
407
+ "select container runtime"
408
+ ;;
409
+ esac
410
+ if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
411
+ case "$CONTAINER_CLI" in
412
+ /*) ;;
413
+ *)
414
+ emit_err "BAD_ARGS" \
415
+ "UAI_CONTAINER_CLI must be an absolute path to the bundled container CLI" \
416
+ "select container runtime"
417
+ ;;
418
+ esac
419
+ if [ -L "$CONTAINER_CLI" ] || [ ! -f "$CONTAINER_CLI" ] \
420
+ || [ ! -x "$CONTAINER_CLI" ]; then
421
+ emit_err "BAD_ARGS" \
422
+ "the bundled container CLI at $CONTAINER_CLI is missing or not executable" \
423
+ "select container runtime"
424
+ fi
425
+ fi
426
+ }
427
+
428
+ # Single dispatcher for the verbs whose flags are identical across Docker and
429
+ # the Apple CLI (exec/cp/stop/inspect/image inspect/volume create). Verbs that
430
+ # differ (compose, run, build, port) branch explicitly at their call sites.
431
+ crt() {
432
+ if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
433
+ "$CONTAINER_CLI" "$@"
434
+ else
435
+ docker "$@"
436
+ fi
437
+ }
438
+
439
+ # Absence proof for the Apple runtime. The CLI exits nonzero for BOTH a
440
+ # missing container and a broken apiserver; only the not-found error that
441
+ # names this exact container proves absence. Everything else proves nothing
442
+ # and must never authorize destructive work.
443
+ apple_container_proven_absent() {
444
+ apple_absent_name="$1"
445
+ # One CLI response supplies the whole proof: nonzero exit, empty stdout,
446
+ # and the complete not-found error line naming this exact container (never
447
+ # a substring — a not-found answer for "$name-shadow" must not prove the
448
+ # absence of "$name"). Two separate invocations could each satisfy half
449
+ # while no single response satisfies both.
450
+ apple_absent_dir=$(mktemp -d "${TMPDIR:-/tmp}/uai-absent.XXXXXX") \
451
+ || return 1
452
+ apple_absent_rc=0
453
+ "$CONTAINER_CLI" inspect "$apple_absent_name" \
454
+ >"$apple_absent_dir/out" 2>"$apple_absent_dir/err" \
455
+ || apple_absent_rc=$?
456
+ apple_absent_proven=1
457
+ [ "$apple_absent_rc" -ne 0 ] || apple_absent_proven=0
458
+ [ ! -s "$apple_absent_dir/out" ] || apple_absent_proven=0
459
+ grep -Fqx "Error: container not found: $apple_absent_name" \
460
+ "$apple_absent_dir/err" 2>/dev/null || apple_absent_proven=0
461
+ rm -rf "$apple_absent_dir"
462
+ [ "$apple_absent_proven" -eq 1 ]
463
+ }
464
+
465
+ # ADR-118: same single-response absence discipline for a named volume. Only
466
+ # the exact whole-line not-found grammar naming this volume proves absence;
467
+ # any other failure (broken apiserver, timeout) proves nothing.
468
+ apple_volume_proven_absent() {
469
+ apple_vabsent_name="$1"
470
+ apple_vabsent_dir=$(mktemp -d "${TMPDIR:-/tmp}/uai-vabsent.XXXXXX") \
471
+ || return 1
472
+ apple_vabsent_rc=0
473
+ "$CONTAINER_CLI" volume inspect "$apple_vabsent_name" \
474
+ >"$apple_vabsent_dir/out" 2>"$apple_vabsent_dir/err" \
475
+ || apple_vabsent_rc=$?
476
+ apple_vabsent_proven=1
477
+ [ "$apple_vabsent_rc" -ne 0 ] || apple_vabsent_proven=0
478
+ [ ! -s "$apple_vabsent_dir/out" ] || apple_vabsent_proven=0
479
+ grep -Fqx "Error: volume not found: $apple_vabsent_name" \
480
+ "$apple_vabsent_dir/err" 2>/dev/null || apple_vabsent_proven=0
481
+ rm -rf "$apple_vabsent_dir"
482
+ [ "$apple_vabsent_proven" -eq 1 ]
483
+ }
484
+
485
+ # Canonicalise every GitHub URL shape to credential-free HTTPS. task-up selects
486
+ # the credential per task: the creator's connected GitHub token is primary;
487
+ # SSH is a fallback only when that user has no GitHub connection on this host.
488
+ # Keeping the stored/shared origin transport-neutral avoids binding a project
489
+ # to whichever user's credential happened to touch its mirror first.
490
+ #
491
+ # Mirrors normalizeRepoUrl() in lib/github/repo-url.ts — keep the two in step.
492
+ # Non-GitHub remotes are returned untouched: we only know how to speak for
493
+ # GitHub, and Uai supports any git remote.
494
+ #
495
+ # Bash 3.2 (macOS /bin/bash): no ${var,,}, no associative arrays.
496
+ normalize_github_repo_url() {
497
+ local input="$1" url after_scheme authority host_port host host_lc
498
+ local path owner rest repo repo_lc left
499
+ url=$(printf '%s' "$input" | sed \
500
+ -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
501
+ if [ "${url#*://}" != "$url" ]; then
502
+ after_scheme=${url#*://}
503
+ authority=${after_scheme%%/*}
504
+ [ "$after_scheme" != "$authority" ] || {
505
+ printf '%s' "$url"
506
+ return
507
+ }
508
+ host_port=${authority##*@}
509
+ host=${host_port%%:*}
510
+ host_lc=$(printf '%s' "$host" | tr '[:upper:]' '[:lower:]')
511
+ case "$host_lc" in
512
+ github.com|www.github.com) path=${after_scheme#*/} ;;
513
+ *)
514
+ printf '%s' "$url"
515
+ return
516
+ ;;
517
+ esac
518
+ elif [ "${url#*:}" != "$url" ]; then
519
+ left=${url%%:*}
520
+ host=${left##*@}
521
+ host_lc=$(printf '%s' "$host" | tr '[:upper:]' '[:lower:]')
522
+ case "$host_lc" in
523
+ github.com|www.github.com) path=${url#*:} ;;
524
+ *)
525
+ printf '%s' "$url"
526
+ return
527
+ ;;
528
+ esac
529
+ else
530
+ case "$url" in
531
+ */*/*|/*)
532
+ printf '%s' "$url"
533
+ return
534
+ ;;
535
+ */*) path=$url ;;
536
+ *)
537
+ printf '%s' "$url"
538
+ return
539
+ ;;
540
+ esac
541
+ fi
542
+
543
+ # Keep only owner/repo. Browser URLs can include /tree/branch/path, and a
544
+ # pasted clone URL can carry both `.git` and a trailing slash.
545
+ path=${path%%\?*}
546
+ path=${path%%\#*}
547
+ path=$(printf '%s' "$path" | sed -e 's#/*$##')
548
+ owner=${path%%/*}
549
+ rest=${path#*/}
550
+ if [ -z "$owner" ] || [ "$rest" = "$path" ]; then
551
+ printf '%s' "$url"
552
+ return
553
+ fi
554
+ repo=${rest%%/*}
555
+ repo=${repo%%\?*}
556
+ repo=${repo%%\#*}
557
+ repo_lc=$(printf '%s' "$repo" | tr '[:upper:]' '[:lower:]')
558
+ case "$repo_lc" in
559
+ *.git) repo=${repo%????} ;;
560
+ esac
561
+ if [ -z "$repo" ]; then
562
+ printf '%s' "$url"
563
+ return
564
+ fi
565
+ printf 'https://github.com/%s/%s.git' "$owner" "$repo"
566
+ }
567
+
568
+ # True for a reference whose explicit authority is github.com, even if its
569
+ # path is malformed. Callers use this to fail closed rather than handing a
570
+ # GitHub-looking URL to a generic Git command with ambient credentials.
571
+ is_github_hosted_url() {
572
+ local input="$1" url after_scheme authority host_port host host_lc left
573
+ url=$(printf '%s' "$input" | sed \
574
+ -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
575
+ if [ "${url#*://}" != "$url" ]; then
576
+ after_scheme=${url#*://}
577
+ authority=${after_scheme%%/*}
578
+ host_port=${authority##*@}
579
+ host=${host_port%%:*}
580
+ elif [ "${url#*:}" != "$url" ]; then
581
+ left=${url%%:*}
582
+ host=${left##*@}
583
+ else
584
+ case "$url" in
585
+ */*/*|/*|*://*) return 1 ;;
586
+ */*) return 0 ;;
587
+ *) return 1 ;;
588
+ esac
589
+ fi
590
+ host_lc=$(printf '%s' "$host" | tr '[:upper:]' '[:lower:]')
591
+ case "$host_lc" in
592
+ github.com|www.github.com) return 0 ;;
593
+ *) return 1 ;;
594
+ esac
595
+ }
596
+
597
+ # HTTPS spelling of a github.com repository. Empty for non-GitHub remotes.
598
+ github_https_repo_url() {
599
+ local https_url rest owner repo
600
+ https_url=$(normalize_github_repo_url "$1")
601
+ case "$https_url" in
602
+ https://github.com/*)
603
+ rest=${https_url#https://github.com/}
604
+ owner=${rest%%/*}
605
+ repo=${rest#*/}
606
+ if [ "$repo" = "$rest" ]; then
607
+ printf ''
608
+ else
609
+ case "$owner/$repo" in
610
+ /*|*/|*/*/*) printf '' ;;
611
+ *) printf '%s' "$https_url" ;;
612
+ esac
613
+ fi
614
+ ;;
615
+ *)
616
+ printf ''
617
+ ;;
618
+ esac
619
+ }
620
+
621
+ # SSH spelling used only when the task owner has not connected GitHub.
622
+ github_ssh_repo_url() {
623
+ local https_url
624
+ https_url=$(github_https_repo_url "$1")
625
+ case "$https_url" in
626
+ https://github.com/*)
627
+ printf 'git@github.com:%s' "${https_url#https://github.com/}"
628
+ ;;
629
+ *)
630
+ printf ''
631
+ ;;
632
+ esac
633
+ }