@cursor/july 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +40 -7
- package/README.md +33 -24
- package/dist/bin/agent-serve.d.ts +3 -1
- package/dist/bin/agent-serve.d.ts.map +1 -1
- package/dist/bin/agent-serve.js +466 -140
- package/dist/channels/github/api.d.ts.map +1 -1
- package/dist/channels/github/api.js +31 -14
- package/dist/channels/github/cursor-account.d.ts +43 -0
- package/dist/channels/github/cursor-account.d.ts.map +1 -0
- package/dist/channels/github/cursor-account.js +95 -0
- package/dist/channels/github/github-channel.d.ts.map +1 -1
- package/dist/channels/github/github-channel.js +46 -9
- package/dist/channels/github/index.d.ts +2 -2
- package/dist/channels/github/index.js +2 -2
- package/dist/channels/github/types.d.ts +17 -0
- package/dist/channels/github/types.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.d.ts +8 -2
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +8 -0
- package/dist/channels/slack/types.d.ts +24 -3
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/channels/slack/types.js +15 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +8 -8
- package/dist/docs/assets/{ab.md.COdXkces.js → ab.md.BMCZ6Hd7.js} +3 -3
- package/dist/docs/assets/{ab.md.COdXkces.lean.js → ab.md.BMCZ6Hd7.lean.js} +1 -1
- package/dist/docs/assets/{app.DqfFEmJd.js → app.BR0RbIdx.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DtIOQzGj.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BaLEdS15.js → VPLocalSearchBox.qjsVTneI.js} +1 -1
- package/dist/docs/assets/chunks/{theme.CZRvu_0q.js → theme.2Kg8jIp_.js} +2 -2
- package/dist/docs/assets/{deployment.md.Dx1TYNk5.js → deployment.md.DTKwE15Z.js} +3 -3
- package/dist/docs/assets/{deployment.md.Dx1TYNk5.lean.js → deployment.md.DTKwE15Z.lean.js} +1 -1
- package/dist/docs/assets/{evals.md.DPZ_MAnI.js → evals.md.DAgEc_hL.js} +3 -3
- package/dist/docs/assets/{guides_agent-to-agent.md.CrtrsySy.js → guides_agent-to-agent.md.Bpzgq2Pq.js} +1 -1
- package/dist/docs/assets/{guides_cloud-runtime.md.CYlNMTNp.js → guides_cloud-runtime.md.gVzabdQL.js} +1 -1
- package/dist/docs/assets/{guides_github.md.DwbKhCeS.js → guides_github.md.DOOCpqsW.js} +11 -4
- package/dist/docs/assets/{guides_github.md.DwbKhCeS.lean.js → guides_github.md.DOOCpqsW.lean.js} +1 -1
- package/dist/docs/assets/{guides_human-in-the-loop.md.Dvuctx7s.js → guides_human-in-the-loop.md.DlUqsp1S.js} +2 -2
- package/dist/docs/assets/{guides_slack.md.bv41fHfW.js → guides_slack.md.CCwqHvSV.js} +4 -4
- package/dist/docs/assets/{guides_slack.md.bv41fHfW.lean.js → guides_slack.md.CCwqHvSV.lean.js} +1 -1
- package/dist/docs/assets/{guides_webhooks.md.hFTik3lf.js → guides_webhooks.md.B1EswtUu.js} +2 -2
- package/dist/docs/assets/index.md.m81y7TY7.js +20 -0
- package/dist/docs/assets/{index.md.BPKcj5AI.lean.js → index.md.m81y7TY7.lean.js} +1 -1
- package/dist/docs/assets/quickstart.md.BU6Iwi_9.js +204 -0
- package/dist/docs/assets/quickstart.md.BU6Iwi_9.lean.js +1 -0
- package/dist/docs/assets/{reference_agent-config.md.Bpd7HQwf.js → reference_agent-config.md.DrW2JUM8.js} +4 -4
- package/dist/docs/assets/{reference_agent-config.md.Bpd7HQwf.lean.js → reference_agent-config.md.DrW2JUM8.lean.js} +1 -1
- package/dist/docs/assets/{reference_channels.md.D7JTR03W.js → reference_channels.md.DdmiKgqf.js} +4 -4
- package/dist/docs/assets/{reference_channels.md.D7JTR03W.lean.js → reference_channels.md.DdmiKgqf.lean.js} +1 -1
- package/dist/docs/assets/{reference_cli.md.DA730zCu.js → reference_cli.md.Bv6pOxcF.js} +11 -6
- package/dist/docs/assets/{reference_cli.md.DA730zCu.lean.js → reference_cli.md.Bv6pOxcF.lean.js} +1 -1
- package/dist/docs/assets/{reference_connections.md.C3vNH_DE.js → reference_connections.md.zaEYCLHT.js} +1 -1
- package/dist/docs/assets/{reference_hooks.md.BCEc3MyM.js → reference_hooks.md.DyLVfE1O.js} +1 -1
- package/dist/docs/assets/{reference_hooks.md.BCEc3MyM.lean.js → reference_hooks.md.DyLVfE1O.lean.js} +1 -1
- package/dist/docs/assets/{reference_http-api.md.DBAahtdz.js → reference_http-api.md.Dx_nmDG6.js} +1 -1
- package/dist/docs/assets/{reference_instructions.md.BC05LEQ8.js → reference_instructions.md.CgoV-YEb.js} +9 -7
- package/dist/docs/assets/{reference_instructions.md.BC05LEQ8.lean.js → reference_instructions.md.CgoV-YEb.lean.js} +1 -1
- package/dist/docs/assets/{reference_schedules.md.D7qijxLk.js → reference_schedules.md.w_F2mXB6.js} +2 -2
- package/dist/docs/assets/{reference_skills.md.VQnlBT3Q.js → reference_skills.md.B_jHN7JL.js} +3 -3
- package/dist/docs/assets/{reference_skills.md.VQnlBT3Q.lean.js → reference_skills.md.B_jHN7JL.lean.js} +1 -1
- package/dist/docs/assets/{reference_subagents.md.CIRAVcPK.js → reference_subagents.md.zWAMNfi1.js} +1 -1
- package/dist/docs/assets/{reference_tools.md.DF5kwlt0.js → reference_tools.md.CqgJroI0.js} +2 -2
- package/dist/docs/assets/scaffolding-agents.md.C3pTrmoE.js +1 -0
- package/dist/docs/assets/scaffolding-agents.md.C3pTrmoE.lean.js +1 -0
- package/dist/docs/assets/storage.md.CVnInNiN.js +17 -0
- package/dist/docs/assets/storage.md.CVnInNiN.lean.js +1 -0
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +4 -4
- package/dist/docs/deployment.html +7 -7
- package/dist/docs/evals.html +7 -7
- package/dist/docs/guides/agent-to-agent.html +6 -6
- package/dist/docs/guides/cloud-runtime.html +5 -5
- package/dist/docs/guides/github.html +14 -7
- package/dist/docs/guides/human-in-the-loop.html +6 -6
- package/dist/docs/guides/slack.html +8 -8
- package/dist/docs/guides/webhooks.html +6 -6
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +5 -5
- package/dist/docs/index.html +7 -7
- package/dist/docs/quickstart.html +195 -26
- package/dist/docs/reference/agent-config.html +7 -7
- package/dist/docs/reference/channels.html +8 -8
- package/dist/docs/reference/cli.html +14 -9
- package/dist/docs/reference/connections.html +5 -5
- package/dist/docs/reference/hooks.html +5 -5
- package/dist/docs/reference/http-api.html +6 -6
- package/dist/docs/reference/instructions.html +13 -11
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +4 -4
- package/dist/docs/reference/schedules.html +7 -7
- package/dist/docs/reference/sessions.html +4 -4
- package/dist/docs/reference/skills.html +6 -6
- package/dist/docs/reference/subagents.html +6 -6
- package/dist/docs/reference/tools.html +7 -7
- package/dist/docs/scaffolding-agents.html +5 -5
- package/dist/docs/storage.html +41 -0
- package/dist/docs/troubleshooting.html +4 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/internal/chat-client.d.ts +29 -3
- package/dist/internal/chat-client.d.ts.map +1 -1
- package/dist/internal/chat-client.js +180 -27
- package/dist/internal/cli-ax.d.ts +75 -2
- package/dist/internal/cli-ax.d.ts.map +1 -1
- package/dist/internal/cli-ax.js +600 -52
- package/dist/internal/cli-cursor.d.ts.map +1 -1
- package/dist/internal/cli-cursor.js +8 -36
- package/dist/internal/cli-deploy.d.ts +108 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -0
- package/dist/internal/cli-deploy.js +1009 -0
- package/dist/internal/cursor/backend-client.d.ts +10 -1
- package/dist/internal/cursor/backend-client.d.ts.map +1 -1
- package/dist/internal/cursor/backend-client.js +79 -1
- package/dist/internal/cursor/credentials.d.ts +9 -0
- package/dist/internal/cursor/credentials.d.ts.map +1 -1
- package/dist/internal/cursor/credentials.js +12 -0
- package/dist/internal/cursor/github-credentials.d.ts +44 -0
- package/dist/internal/cursor/github-credentials.d.ts.map +1 -0
- package/dist/internal/cursor/github-credentials.js +195 -0
- package/dist/internal/deploy-client.d.ts +184 -0
- package/dist/internal/deploy-client.d.ts.map +1 -0
- package/dist/internal/deploy-client.js +395 -0
- package/dist/internal/deploy-source.d.ts +35 -0
- package/dist/internal/deploy-source.d.ts.map +1 -0
- package/dist/internal/deploy-source.js +118 -0
- package/dist/internal/discovery.d.ts +11 -0
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +116 -19
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +1 -0
- package/dist/internal/eval-run-store.d.ts +22 -3
- package/dist/internal/eval-run-store.d.ts.map +1 -1
- package/dist/internal/eval-run-store.js +39 -20
- package/dist/internal/eval-runner.d.ts +2 -0
- package/dist/internal/eval-runner.d.ts.map +1 -1
- package/dist/internal/eval-runner.js +2 -0
- package/dist/internal/event-mapper.d.ts +54 -1
- package/dist/internal/event-mapper.d.ts.map +1 -1
- package/dist/internal/event-mapper.js +151 -41
- package/dist/internal/handleAgentServeTrigger.d.ts.map +1 -1
- package/dist/internal/handleAgentServeTrigger.js +10 -12
- package/dist/internal/host-platforms.d.ts +7 -2
- package/dist/internal/host-platforms.d.ts.map +1 -1
- package/dist/internal/host-platforms.js +15 -10
- package/dist/internal/hosting.d.ts +37 -0
- package/dist/internal/hosting.d.ts.map +1 -0
- package/dist/internal/hosting.js +67 -0
- package/dist/internal/init-project.d.ts +88 -1
- package/dist/internal/init-project.d.ts.map +1 -1
- package/dist/internal/init-project.js +221 -31
- package/dist/internal/install-cursor-skills.d.ts +64 -0
- package/dist/internal/install-cursor-skills.d.ts.map +1 -0
- package/dist/internal/install-cursor-skills.js +274 -0
- package/dist/internal/logs-client.d.ts +60 -0
- package/dist/internal/logs-client.d.ts.map +1 -0
- package/dist/internal/logs-client.js +311 -0
- package/dist/internal/open-browser.d.ts +5 -0
- package/dist/internal/open-browser.d.ts.map +1 -0
- package/dist/internal/open-browser.js +34 -0
- package/dist/internal/playground/toolchain.d.ts.map +1 -1
- package/dist/internal/playground/toolchain.js +16 -11
- package/dist/internal/playground-proxy.d.ts +69 -0
- package/dist/internal/playground-proxy.d.ts.map +1 -0
- package/dist/internal/playground-proxy.js +468 -0
- package/dist/internal/prompt-context.d.ts +35 -0
- package/dist/internal/prompt-context.d.ts.map +1 -0
- package/dist/internal/prompt-context.js +71 -0
- package/dist/internal/reminder-runner.d.ts +7 -0
- package/dist/internal/reminder-runner.d.ts.map +1 -1
- package/dist/internal/reminder-runner.js +50 -6
- package/dist/internal/reminder-store.d.ts +2 -0
- package/dist/internal/reminder-store.d.ts.map +1 -1
- package/dist/internal/reminder-store.js +18 -0
- package/dist/internal/request-headers.d.ts +11 -0
- package/dist/internal/request-headers.d.ts.map +1 -0
- package/dist/internal/request-headers.js +14 -0
- package/dist/internal/resolve-prod-target.d.ts +42 -0
- package/dist/internal/resolve-prod-target.d.ts.map +1 -0
- package/dist/internal/resolve-prod-target.js +111 -0
- package/dist/internal/run-client.d.ts +2 -2
- package/dist/internal/run-client.d.ts.map +1 -1
- package/dist/internal/run-client.js +38 -23
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +182 -45
- package/dist/internal/session-engine.d.ts +50 -1
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +267 -23
- package/dist/internal/sessions-client.d.ts +21 -0
- package/dist/internal/sessions-client.d.ts.map +1 -0
- package/dist/internal/sessions-client.js +168 -0
- package/dist/internal/storage-coordinator.d.ts +139 -0
- package/dist/internal/storage-coordinator.d.ts.map +1 -0
- package/dist/internal/storage-coordinator.js +499 -0
- package/dist/internal/stream-progress.d.ts.map +1 -1
- package/dist/internal/stream-progress.js +31 -3
- package/dist/internal/terminal-style.d.ts +22 -0
- package/dist/internal/terminal-style.d.ts.map +1 -0
- package/dist/internal/terminal-style.js +49 -0
- package/dist/internal/trajectory.d.ts +10 -5
- package/dist/internal/trajectory.d.ts.map +1 -1
- package/dist/internal/trajectory.js +82 -10
- package/dist/internal/workspace.d.ts +11 -0
- package/dist/internal/workspace.d.ts.map +1 -1
- package/dist/internal/workspace.js +38 -4
- package/dist/playground/assets/cursor-icons-outline-BxTT_FVJ.woff2 +0 -0
- package/dist/playground/assets/index-D-vo2lV_.css +1 -0
- package/dist/playground/assets/index-x60b9q2j.js +312 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage.d.ts +204 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +153 -0
- package/dist/types.d.ts +70 -4
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +3 -2
- package/docs/guides/github.md +43 -8
- package/docs/guides/slack.md +1 -1
- package/docs/quickstart.md +350 -57
- package/docs/reference/cli.md +51 -3
- package/docs/reference/instructions.md +8 -6
- package/docs/scaffolding-agents.md +1 -1
- package/docs/storage.md +98 -0
- package/package.json +10 -1
- package/skills/ab/SKILL.md +8 -8
- package/skills/create-agent/SKILL.md +28 -22
- package/skills/debug/SKILL.md +11 -11
- package/skills/evals/SKILL.md +16 -16
- package/skills/framework-map/SKILL.md +6 -6
- package/skills/github/SKILL.md +22 -14
- package/skills/hillclimb/SKILL.md +10 -10
- package/skills/setup-slack/SKILL.md +13 -13
- package/src/bin/agent-serve.ts +552 -179
- package/src/channels/github/api.ts +42 -23
- package/src/channels/github/cursor-account.ts +165 -0
- package/src/channels/github/github-channel.ts +66 -6
- package/src/channels/github/index.ts +2 -2
- package/src/channels/github/types.ts +19 -0
- package/src/channels/slack/slack-channel.ts +17 -3
- package/src/channels/slack/types.ts +44 -3
- package/src/index.ts +13 -0
- package/src/internal/chat-client.ts +252 -37
- package/src/internal/cli-ax.ts +722 -72
- package/src/internal/cli-cursor.ts +14 -42
- package/src/internal/cli-deploy.ts +1319 -0
- package/src/internal/cursor/backend-client.ts +103 -1
- package/src/internal/cursor/credentials.ts +14 -0
- package/src/internal/cursor/github-credentials.ts +248 -0
- package/src/internal/deploy-client.ts +632 -0
- package/src/internal/deploy-source.ts +133 -0
- package/src/internal/discovery.ts +141 -17
- package/src/internal/distribution.ts +1 -0
- package/src/internal/eval-run-store.ts +50 -20
- package/src/internal/eval-runner.ts +5 -0
- package/src/internal/event-mapper.ts +222 -42
- package/src/internal/handleAgentServeTrigger.ts +10 -12
- package/src/internal/host-platforms.ts +28 -11
- package/src/internal/hosting.ts +77 -0
- package/src/internal/init-project.ts +333 -51
- package/src/internal/install-cursor-skills.ts +327 -0
- package/src/internal/logs-client.ts +442 -0
- package/src/internal/open-browser.ts +41 -0
- package/src/internal/playground/toolchain.ts +15 -13
- package/src/internal/playground-proxy.ts +576 -0
- package/src/internal/prompt-context.ts +95 -0
- package/src/internal/reminder-runner.ts +50 -6
- package/src/internal/reminder-store.ts +21 -0
- package/src/internal/request-headers.ts +23 -0
- package/src/internal/resolve-prod-target.ts +150 -0
- package/src/internal/run-client.ts +39 -36
- package/src/internal/server.ts +223 -28
- package/src/internal/session-engine.ts +307 -10
- package/src/internal/sessions-client.ts +182 -0
- package/src/internal/storage-coordinator.ts +615 -0
- package/src/internal/stream-progress.ts +36 -2
- package/src/internal/terminal-style.ts +74 -0
- package/src/internal/trajectory.ts +91 -13
- package/src/internal/workspace.ts +42 -4
- package/src/storage.ts +325 -0
- package/src/types.ts +82 -8
- package/dist/docs/assets/chunks/@localSearchIndexroot.CcVk1uKq.js +0 -1
- package/dist/docs/assets/index.md.BPKcj5AI.js +0 -20
- package/dist/docs/assets/quickstart.md.tVPiGK_L.js +0 -35
- package/dist/docs/assets/quickstart.md.tVPiGK_L.lean.js +0 -1
- package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.js +0 -1
- package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.lean.js +0 -1
- package/dist/playground/assets/cursor-icons-outline-oY2V_mvK.woff2 +0 -0
- package/dist/playground/assets/index-1K-hG-7p.css +0 -1
- package/dist/playground/assets/index-FlWjhg3x.js +0 -79
- /package/dist/docs/assets/{evals.md.DPZ_MAnI.lean.js → evals.md.DAgEc_hL.lean.js} +0 -0
- /package/dist/docs/assets/{guides_agent-to-agent.md.CrtrsySy.lean.js → guides_agent-to-agent.md.Bpzgq2Pq.lean.js} +0 -0
- /package/dist/docs/assets/{guides_cloud-runtime.md.CYlNMTNp.lean.js → guides_cloud-runtime.md.gVzabdQL.lean.js} +0 -0
- /package/dist/docs/assets/{guides_human-in-the-loop.md.Dvuctx7s.lean.js → guides_human-in-the-loop.md.DlUqsp1S.lean.js} +0 -0
- /package/dist/docs/assets/{guides_webhooks.md.hFTik3lf.lean.js → guides_webhooks.md.B1EswtUu.lean.js} +0 -0
- /package/dist/docs/assets/{reference_connections.md.C3vNH_DE.lean.js → reference_connections.md.zaEYCLHT.lean.js} +0 -0
- /package/dist/docs/assets/{reference_http-api.md.DBAahtdz.lean.js → reference_http-api.md.Dx_nmDG6.lean.js} +0 -0
- /package/dist/docs/assets/{reference_schedules.md.D7qijxLk.lean.js → reference_schedules.md.w_F2mXB6.lean.js} +0 -0
- /package/dist/docs/assets/{reference_subagents.md.CIRAVcPK.lean.js → reference_subagents.md.zWAMNfi1.lean.js} +0 -0
- /package/dist/docs/assets/{reference_tools.md.DF5kwlt0.lean.js → reference_tools.md.CqgJroI0.lean.js} +0 -0
|
@@ -0,0 +1,1319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agentkit deploy | deployments | deployment | stop | rotate-token |
|
|
3
|
+
* rotate-pod-credential | secrets set|list|unset` — Heroku-style deploys
|
|
4
|
+
* onto Cursor's managed agent-serve hosting.
|
|
5
|
+
*
|
|
6
|
+
* All intelligence is server-side (backend/server/src/agentServe/); these
|
|
7
|
+
* handlers resolve parameters (credentials, team, slug, the project's
|
|
8
|
+
* `hosting` manifest block), call the management API through
|
|
9
|
+
* `deploy-client.ts`, poll status, and render.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { basename, resolve } from "node:path";
|
|
13
|
+
import type { AgentHostingOptions } from "../types.js";
|
|
14
|
+
import { CursorBackendClient } from "./cursor/backend-client.js";
|
|
15
|
+
import {
|
|
16
|
+
cursorExternalApiUrl,
|
|
17
|
+
resolveApiKey,
|
|
18
|
+
SIGN_IN_HINT,
|
|
19
|
+
} from "./cursor/credentials.js";
|
|
20
|
+
import {
|
|
21
|
+
AgentServeDeployClient,
|
|
22
|
+
type DeployAccepted,
|
|
23
|
+
DeployApiError,
|
|
24
|
+
type Deployment,
|
|
25
|
+
type DeploymentDetail,
|
|
26
|
+
type DeploymentStatus,
|
|
27
|
+
type DeploySource,
|
|
28
|
+
type SecretInfo,
|
|
29
|
+
type StopAccepted,
|
|
30
|
+
slugFromDirectoryName,
|
|
31
|
+
TERMINAL_DEPLOY_STATUSES,
|
|
32
|
+
validateDeploymentSlug,
|
|
33
|
+
} from "./deploy-client.js";
|
|
34
|
+
import { type GitRunner, inferDeployGitSource } from "./deploy-source.js";
|
|
35
|
+
import {
|
|
36
|
+
type ChildAgentDir,
|
|
37
|
+
isAgentProjectDir,
|
|
38
|
+
listChildAgentDirs,
|
|
39
|
+
loadAgentProject,
|
|
40
|
+
} from "./discovery.js";
|
|
41
|
+
import { CLI_COMMAND_NAME as CLI } from "./distribution.js";
|
|
42
|
+
import {
|
|
43
|
+
isValidCursorEventRepo,
|
|
44
|
+
validateSettableSecretName,
|
|
45
|
+
} from "./hosting.js";
|
|
46
|
+
import { stderrPalette } from "./terminal-style.js";
|
|
47
|
+
|
|
48
|
+
const DEFAULT_POLL_INTERVAL_MS = 3_000;
|
|
49
|
+
const DEFAULT_DEPLOY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
50
|
+
|
|
51
|
+
/** One agent directory selected for deploy (single or multi-agent parent). */
|
|
52
|
+
export type DeployTarget = ChildAgentDir;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Interactive / test seam: pick which agents under a multi-agent parent to
|
|
56
|
+
* deploy. Return `null` to cancel.
|
|
57
|
+
*/
|
|
58
|
+
export type ChooseDeployTargets = (
|
|
59
|
+
agents: readonly DeployTarget[]
|
|
60
|
+
) => Promise<DeployTarget[] | null>;
|
|
61
|
+
|
|
62
|
+
export interface DeployCliOptions {
|
|
63
|
+
dir?: string;
|
|
64
|
+
slug?: string;
|
|
65
|
+
/**
|
|
66
|
+
* deploy `--all`: when `--dir` is a multi-agent parent, deploy every
|
|
67
|
+
* child agent (skips the interactive picker).
|
|
68
|
+
*/
|
|
69
|
+
all?: boolean;
|
|
70
|
+
/** `--team <id>`; resolved from the account when omitted. */
|
|
71
|
+
team?: string;
|
|
72
|
+
json?: boolean;
|
|
73
|
+
noWait?: boolean;
|
|
74
|
+
apiKey?: string;
|
|
75
|
+
/**
|
|
76
|
+
* deploy `--repo <url>`: https git repo the control plane builds from.
|
|
77
|
+
* Optional — inferred from `git remote origin` when omitted inside an
|
|
78
|
+
* agent project checkout.
|
|
79
|
+
*/
|
|
80
|
+
repo?: string;
|
|
81
|
+
/**
|
|
82
|
+
* deploy `--ref <ref>`: git ref to build. Optional — inferred from the
|
|
83
|
+
* current branch (or HEAD SHA if detached) when omitted; otherwise the
|
|
84
|
+
* server uses the repo's default branch.
|
|
85
|
+
*/
|
|
86
|
+
ref?: string;
|
|
87
|
+
/**
|
|
88
|
+
* deploy `--path <agentPath>`: agent directory relative to the repo root.
|
|
89
|
+
* Optional — inferred from `--dir` vs the git toplevel when omitted.
|
|
90
|
+
*/
|
|
91
|
+
path?: string;
|
|
92
|
+
/** deploy `--cursor-events-repo <owner/name>` (repeatable). */
|
|
93
|
+
cursorEventsRepos?: string[];
|
|
94
|
+
/** deploy `--allow-domain <domain>` (repeatable; unioned with the manifest). */
|
|
95
|
+
allowDomains?: string[];
|
|
96
|
+
/** Test seams. */
|
|
97
|
+
fetchImpl?: typeof fetch;
|
|
98
|
+
/** Inject git probing for deploy source inference (tests). */
|
|
99
|
+
git?: GitRunner;
|
|
100
|
+
/** Inject the multi-agent picker (tests / non-interactive hosts). */
|
|
101
|
+
chooseTargets?: ChooseDeployTargets;
|
|
102
|
+
pollIntervalMs?: number;
|
|
103
|
+
timeoutMs?: number;
|
|
104
|
+
out?: (text: string) => void;
|
|
105
|
+
err?: (text: string) => void;
|
|
106
|
+
/** `secrets set` value input (hidden prompt on a TTY, lines otherwise). */
|
|
107
|
+
stdin?: SecretInputStream;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Minimal stdin surface `secrets set` reads values from (test-injectable). */
|
|
111
|
+
export type SecretInputStream = NodeJS.ReadableStream & {
|
|
112
|
+
isTTY?: boolean;
|
|
113
|
+
setRawMode?: (mode: boolean) => void;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
interface DeployCliContext {
|
|
117
|
+
client: AgentServeDeployClient;
|
|
118
|
+
teamId: number;
|
|
119
|
+
json: boolean;
|
|
120
|
+
noWait: boolean;
|
|
121
|
+
pollIntervalMs: number;
|
|
122
|
+
timeoutMs: number;
|
|
123
|
+
out: (text: string) => void;
|
|
124
|
+
err: (text: string) => void;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface ResolveDeployAuthOptions {
|
|
128
|
+
team?: string;
|
|
129
|
+
apiKey?: string;
|
|
130
|
+
fetchImpl?: typeof fetch;
|
|
131
|
+
/** Quiet team-resolution chatter (default: write to stderr). */
|
|
132
|
+
err?: (text: string) => void;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ResolvedDeployAuth {
|
|
136
|
+
client: AgentServeDeployClient;
|
|
137
|
+
teamId: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Credentials → deploy client → team id. Shared by deploy commands and
|
|
142
|
+
* `--prod` AX routing. Returns an exit code (error already rendered) on
|
|
143
|
+
* failure.
|
|
144
|
+
*/
|
|
145
|
+
export async function resolveDeployAuth(
|
|
146
|
+
options: ResolveDeployAuthOptions
|
|
147
|
+
): Promise<ResolvedDeployAuth | number> {
|
|
148
|
+
const err =
|
|
149
|
+
options.err ?? ((text: string) => void process.stderr.write(text));
|
|
150
|
+
|
|
151
|
+
const resolved = await resolveApiKey(
|
|
152
|
+
options.apiKey === undefined ? undefined : { apiKey: options.apiKey }
|
|
153
|
+
);
|
|
154
|
+
if (resolved === undefined) {
|
|
155
|
+
err(`Not signed in to Cursor. ${SIGN_IN_HINT}\n`);
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
// The deployment management routes live on the external API host
|
|
159
|
+
// (api.cursor.com), not the default backend (api2.cursor.sh) — see
|
|
160
|
+
// cursorExternalApiUrl. GetMe below stays on the resolved backend.
|
|
161
|
+
const client = new AgentServeDeployClient({
|
|
162
|
+
backendUrl: cursorExternalApiUrl(),
|
|
163
|
+
apiKey: resolved.apiKey,
|
|
164
|
+
fetchImpl: options.fetchImpl,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
let teamId: number;
|
|
168
|
+
if (options.team !== undefined) {
|
|
169
|
+
teamId = Number.parseInt(options.team, 10);
|
|
170
|
+
if (Number.isNaN(teamId) || teamId <= 0) {
|
|
171
|
+
err(`Invalid --team: ${options.team} (expected a positive team id)\n`);
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
const account = new CursorBackendClient({
|
|
176
|
+
backendUrl: resolved.backendUrl,
|
|
177
|
+
apiKey: resolved.apiKey,
|
|
178
|
+
fetchImpl: options.fetchImpl,
|
|
179
|
+
});
|
|
180
|
+
let identity: Awaited<ReturnType<CursorBackendClient["getMe"]>>;
|
|
181
|
+
try {
|
|
182
|
+
identity = await account.getMe();
|
|
183
|
+
} catch (error) {
|
|
184
|
+
err(
|
|
185
|
+
`Could not resolve your team from the Cursor account: ${describeError(error)}\nPass --team <id> explicitly.\n`
|
|
186
|
+
);
|
|
187
|
+
return 1;
|
|
188
|
+
}
|
|
189
|
+
if (identity.teamId === undefined || identity.teamId <= 0) {
|
|
190
|
+
err(
|
|
191
|
+
"This Cursor account has no default team; pass --team <id> (find it in the Cursor dashboard URL).\n"
|
|
192
|
+
);
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
teamId = identity.teamId;
|
|
196
|
+
const dim = stderrPalette().dim;
|
|
197
|
+
err(
|
|
198
|
+
`${dim(
|
|
199
|
+
`Using team ${teamId}${
|
|
200
|
+
identity.teamName === undefined ? "" : ` (${identity.teamName})`
|
|
201
|
+
} — the account's team; pass --team to override.`
|
|
202
|
+
)}\n`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { client, teamId };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Shared preamble: credentials → client → team id. Returns an exit code
|
|
211
|
+
* (with the error already rendered) when resolution fails.
|
|
212
|
+
*/
|
|
213
|
+
async function prepare(
|
|
214
|
+
options: DeployCliOptions
|
|
215
|
+
): Promise<DeployCliContext | number> {
|
|
216
|
+
const out =
|
|
217
|
+
options.out ?? ((text: string) => void process.stdout.write(text));
|
|
218
|
+
const err =
|
|
219
|
+
options.err ?? ((text: string) => void process.stderr.write(text));
|
|
220
|
+
|
|
221
|
+
const auth = await resolveDeployAuth({
|
|
222
|
+
team: options.team,
|
|
223
|
+
apiKey: options.apiKey,
|
|
224
|
+
fetchImpl: options.fetchImpl,
|
|
225
|
+
err,
|
|
226
|
+
});
|
|
227
|
+
if (typeof auth === "number") {
|
|
228
|
+
return auth;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
client: auth.client,
|
|
233
|
+
teamId: auth.teamId,
|
|
234
|
+
json: options.json === true,
|
|
235
|
+
noWait: options.noWait === true,
|
|
236
|
+
pollIntervalMs: options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
|
|
237
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_DEPLOY_TIMEOUT_MS,
|
|
238
|
+
out,
|
|
239
|
+
err,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ============================================================================
|
|
244
|
+
// deploy
|
|
245
|
+
// ============================================================================
|
|
246
|
+
|
|
247
|
+
export async function cmdDeploy(options: DeployCliOptions): Promise<number> {
|
|
248
|
+
const ctx = await prepare(options);
|
|
249
|
+
if (typeof ctx === "number") {
|
|
250
|
+
return ctx;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const targets = await resolveDeployTargets(options, ctx);
|
|
254
|
+
if (typeof targets === "number") {
|
|
255
|
+
return targets;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// --path is per-agent (relative to the repo root). When deploying several
|
|
259
|
+
// agents, ignore a shared --path so each target can infer its own path.
|
|
260
|
+
let deployOptions = options;
|
|
261
|
+
if (targets.length > 1 && options.path !== undefined) {
|
|
262
|
+
if (!ctx.json) {
|
|
263
|
+
ctx.err(
|
|
264
|
+
`warning: ignoring --path ${JSON.stringify(options.path)} for a multi-agent deploy; each agent infers its own path from git.\n`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
deployOptions = { ...options, path: undefined };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (targets.length === 1) {
|
|
271
|
+
return deployOne(ctx, deployOptions, targets[0]!);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Multi-agent batch: deploy sequentially; --json emits an array.
|
|
275
|
+
const jsonResults: unknown[] = [];
|
|
276
|
+
let failures = 0;
|
|
277
|
+
for (const [index, target] of targets.entries()) {
|
|
278
|
+
if (!ctx.json) {
|
|
279
|
+
ctx.err(
|
|
280
|
+
`\n=== [${index + 1}/${targets.length}] Deploying ${target.slug} ===\n`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
const before = jsonResults.length;
|
|
284
|
+
const code = await deployOne(ctx, deployOptions, target, {
|
|
285
|
+
jsonCollect: ctx.json ? jsonResults : undefined,
|
|
286
|
+
});
|
|
287
|
+
if (code !== 0) {
|
|
288
|
+
failures += 1;
|
|
289
|
+
if (ctx.json && jsonResults.length === before) {
|
|
290
|
+
jsonResults.push({ slug: target.slug, error: "deploy failed" });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (ctx.json) {
|
|
295
|
+
ctx.out(`${JSON.stringify(jsonResults, null, 2)}\n`);
|
|
296
|
+
} else if (failures > 0) {
|
|
297
|
+
ctx.err(`\n${failures} of ${targets.length} deploy(s) failed.\n`);
|
|
298
|
+
} else {
|
|
299
|
+
ctx.err(`\nDeployed ${targets.length} agents.\n`);
|
|
300
|
+
}
|
|
301
|
+
return failures === 0 ? 0 : 1;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Resolve which agent directory(ies) to deploy from `--dir` / `--slug` /
|
|
306
|
+
* `--all`, with an interactive picker when a multi-agent parent is on a TTY.
|
|
307
|
+
*/
|
|
308
|
+
async function resolveDeployTargets(
|
|
309
|
+
options: DeployCliOptions,
|
|
310
|
+
ctx: DeployCliContext
|
|
311
|
+
): Promise<DeployTarget[] | number> {
|
|
312
|
+
const dir = resolve(options.dir ?? ".");
|
|
313
|
+
|
|
314
|
+
if (options.slug !== undefined) {
|
|
315
|
+
const slugError = validateDeploymentSlug(options.slug);
|
|
316
|
+
if (slugError !== undefined) {
|
|
317
|
+
ctx.err(`${slugError}\n`);
|
|
318
|
+
return 1;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (options.all === true && options.slug !== undefined) {
|
|
322
|
+
ctx.err("Pass either --slug or --all, not both.\n");
|
|
323
|
+
return 1;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (await isAgentProjectDir(dir)) {
|
|
327
|
+
if (options.all === true) {
|
|
328
|
+
ctx.err(
|
|
329
|
+
`--all requires a multi-agent parent directory (several child agents under --dir); ${dir} is a single agent project.\n`
|
|
330
|
+
);
|
|
331
|
+
return 1;
|
|
332
|
+
}
|
|
333
|
+
let slug: string;
|
|
334
|
+
if (options.slug !== undefined) {
|
|
335
|
+
slug = options.slug;
|
|
336
|
+
} else {
|
|
337
|
+
const derived = slugFromDirectoryName(basename(dir));
|
|
338
|
+
if (derived === undefined) {
|
|
339
|
+
ctx.err(
|
|
340
|
+
`Could not derive a deployment slug from the directory name ${JSON.stringify(basename(dir))}; pass --slug.\n`
|
|
341
|
+
);
|
|
342
|
+
return 1;
|
|
343
|
+
}
|
|
344
|
+
slug = derived;
|
|
345
|
+
if (!ctx.json) {
|
|
346
|
+
ctx.err(`Deploying as "${slug}" (from the directory name).\n`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return [{ slug, dir }];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let children: ChildAgentDir[];
|
|
353
|
+
try {
|
|
354
|
+
children = await listChildAgentDirs(dir);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
ctx.err(`${describeError(error)}\n`);
|
|
357
|
+
return 1;
|
|
358
|
+
}
|
|
359
|
+
// Deploy slugs are lowercase-only; normalize directory names the same way
|
|
360
|
+
// single-agent deploy does so MixedCase child dirs still deploy.
|
|
361
|
+
children = children.flatMap((child) => {
|
|
362
|
+
const slug = slugFromDirectoryName(basename(child.dir));
|
|
363
|
+
if (slug === undefined) {
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
return [{ slug, dir: child.dir }];
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
if (children.length === 0) {
|
|
370
|
+
if (options.all === true) {
|
|
371
|
+
ctx.err(
|
|
372
|
+
`--all requires child agent projects under --dir; none found in ${dir}.\n`
|
|
373
|
+
);
|
|
374
|
+
return 1;
|
|
375
|
+
}
|
|
376
|
+
// Static / bare-slug deploy against a non-agent directory.
|
|
377
|
+
let slug: string;
|
|
378
|
+
if (options.slug !== undefined) {
|
|
379
|
+
slug = options.slug;
|
|
380
|
+
} else {
|
|
381
|
+
const derived = slugFromDirectoryName(basename(dir));
|
|
382
|
+
if (derived === undefined) {
|
|
383
|
+
ctx.err(
|
|
384
|
+
`No agent projects under ${dir}, and could not derive a slug from ${JSON.stringify(basename(dir))}; pass --slug or --dir <agent>.\n`
|
|
385
|
+
);
|
|
386
|
+
return 1;
|
|
387
|
+
}
|
|
388
|
+
slug = derived;
|
|
389
|
+
if (!ctx.json) {
|
|
390
|
+
ctx.err(
|
|
391
|
+
`Deploying as "${slug}" (static; no agent project at ${dir}).\n`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return [{ slug, dir }];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (children.length === 1) {
|
|
399
|
+
const only = children[0]!;
|
|
400
|
+
if (options.all === true) {
|
|
401
|
+
// One child is fine for --all (degenerate multi-agent parent).
|
|
402
|
+
} else if (
|
|
403
|
+
options.slug !== undefined &&
|
|
404
|
+
options.slug.toLowerCase() !== only.slug
|
|
405
|
+
) {
|
|
406
|
+
ctx.err(
|
|
407
|
+
`No agent "${options.slug}" under ${dir}; found "${only.slug}".\n`
|
|
408
|
+
);
|
|
409
|
+
return 1;
|
|
410
|
+
}
|
|
411
|
+
if (!ctx.json) {
|
|
412
|
+
ctx.err(
|
|
413
|
+
`Deploying "${only.slug}" (only agent under ${basename(dir)}).\n`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
return [only];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Multiple children.
|
|
420
|
+
if (options.all === true) {
|
|
421
|
+
if (!ctx.json) {
|
|
422
|
+
ctx.err(
|
|
423
|
+
`Deploying all ${children.length} agents under ${basename(dir)}: ${children.map((c) => c.slug).join(", ")}.\n`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
return [...children];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (options.slug !== undefined) {
|
|
430
|
+
const wanted = options.slug.toLowerCase();
|
|
431
|
+
const match = children.find((child) => child.slug === wanted);
|
|
432
|
+
if (match === undefined) {
|
|
433
|
+
ctx.err(
|
|
434
|
+
`No agent "${options.slug}" under ${dir}.\n` +
|
|
435
|
+
formatAgentList(children) +
|
|
436
|
+
`Pass --slug <name> or --all.\n`
|
|
437
|
+
);
|
|
438
|
+
return 1;
|
|
439
|
+
}
|
|
440
|
+
return [match];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const chooser =
|
|
444
|
+
options.chooseTargets ??
|
|
445
|
+
((agents) =>
|
|
446
|
+
promptChooseDeployTargets(agents, {
|
|
447
|
+
err: ctx.err,
|
|
448
|
+
stdin: options.stdin ?? process.stdin,
|
|
449
|
+
interactive:
|
|
450
|
+
ctx.json !== true &&
|
|
451
|
+
(options.stdin ?? process.stdin).isTTY === true &&
|
|
452
|
+
process.stderr.isTTY === true,
|
|
453
|
+
}));
|
|
454
|
+
const chosen = await chooser(children);
|
|
455
|
+
if (chosen === null) {
|
|
456
|
+
ctx.err("Deploy cancelled.\n");
|
|
457
|
+
return 1;
|
|
458
|
+
}
|
|
459
|
+
if (chosen.length === 0) {
|
|
460
|
+
ctx.err(
|
|
461
|
+
`Multiple agents under ${dir}:\n` +
|
|
462
|
+
formatAgentList(children) +
|
|
463
|
+
`Pass --slug <name> or --all (or re-run on a TTY to pick interactively).\n`
|
|
464
|
+
);
|
|
465
|
+
return 1;
|
|
466
|
+
}
|
|
467
|
+
return chosen;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function formatAgentList(agents: readonly DeployTarget[]): string {
|
|
471
|
+
return agents.map((agent, i) => ` ${i + 1}) ${agent.slug}\n`).join("");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* TTY picker for multi-agent parents. Non-interactive callers get `[]`
|
|
476
|
+
* so {@link resolveDeployTargets} can print the --slug/--all hint.
|
|
477
|
+
*/
|
|
478
|
+
export async function promptChooseDeployTargets(
|
|
479
|
+
agents: readonly DeployTarget[],
|
|
480
|
+
options: {
|
|
481
|
+
err: (text: string) => void;
|
|
482
|
+
stdin: SecretInputStream;
|
|
483
|
+
interactive: boolean;
|
|
484
|
+
}
|
|
485
|
+
): Promise<DeployTarget[] | null> {
|
|
486
|
+
options.err(
|
|
487
|
+
`Multiple agents (${agents.length}):\n` +
|
|
488
|
+
formatAgentList(agents) +
|
|
489
|
+
` a) all\n`
|
|
490
|
+
);
|
|
491
|
+
if (!options.interactive) {
|
|
492
|
+
return [];
|
|
493
|
+
}
|
|
494
|
+
options.err('Deploy which? Enter a number, slug, "all", or "q" to cancel: ');
|
|
495
|
+
const readLine = createLineReader(options.stdin);
|
|
496
|
+
// createLineReader pauses until data; ensure stdin is flowing on a TTY.
|
|
497
|
+
options.stdin.resume?.();
|
|
498
|
+
const answer = (await readLine())?.trim().toLowerCase();
|
|
499
|
+
if (answer === undefined || answer === "q" || answer === "quit") {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
if (answer === "a" || answer === "all" || answer === "*") {
|
|
503
|
+
return [...agents];
|
|
504
|
+
}
|
|
505
|
+
const asIndex = Number.parseInt(answer, 10);
|
|
506
|
+
if (
|
|
507
|
+
!Number.isNaN(asIndex) &&
|
|
508
|
+
String(asIndex) === answer &&
|
|
509
|
+
asIndex >= 1 &&
|
|
510
|
+
asIndex <= agents.length
|
|
511
|
+
) {
|
|
512
|
+
return [agents[asIndex - 1]!];
|
|
513
|
+
}
|
|
514
|
+
const bySlug = agents.find((agent) => agent.slug.toLowerCase() === answer);
|
|
515
|
+
if (bySlug !== undefined) {
|
|
516
|
+
return [bySlug];
|
|
517
|
+
}
|
|
518
|
+
options.err(
|
|
519
|
+
`Unrecognized choice ${JSON.stringify(answer)} — expected 1–${agents.length}, a slug, "all", or "q". Aborting.\n`
|
|
520
|
+
);
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function deployOne(
|
|
525
|
+
ctx: DeployCliContext,
|
|
526
|
+
options: DeployCliOptions,
|
|
527
|
+
target: DeployTarget,
|
|
528
|
+
extras?: { jsonCollect?: unknown[] }
|
|
529
|
+
): Promise<number> {
|
|
530
|
+
const { dir, slug } = target;
|
|
531
|
+
|
|
532
|
+
// Fill --repo/--ref/--path from the local git checkout when omitted so
|
|
533
|
+
// `agentkit deploy` inside an agent directory is enough. Only probe git
|
|
534
|
+
// for real agent projects (avoids ambient monorepo remotes when someone
|
|
535
|
+
// deploys a bare --slug). Explicit flags always win.
|
|
536
|
+
const isAgent = await isAgentProjectDir(dir);
|
|
537
|
+
const needsGitInference =
|
|
538
|
+
isAgent &&
|
|
539
|
+
(options.repo === undefined ||
|
|
540
|
+
options.ref === undefined ||
|
|
541
|
+
options.path === undefined);
|
|
542
|
+
const inferred = needsGitInference
|
|
543
|
+
? await inferDeployGitSource(dir, {
|
|
544
|
+
...(options.git === undefined ? {} : { git: options.git }),
|
|
545
|
+
})
|
|
546
|
+
: {};
|
|
547
|
+
const gitRepoUrl = options.repo ?? inferred.gitRepoUrl;
|
|
548
|
+
const gitRef = options.ref ?? inferred.gitRef;
|
|
549
|
+
const agentPath = options.path ?? inferred.agentPath;
|
|
550
|
+
|
|
551
|
+
if (gitRepoUrl !== undefined && !gitRepoUrl.startsWith("https://")) {
|
|
552
|
+
ctx.err(
|
|
553
|
+
`Invalid --repo: ${gitRepoUrl} (expected an https:// git repository URL)\n`
|
|
554
|
+
);
|
|
555
|
+
return 1;
|
|
556
|
+
}
|
|
557
|
+
for (const repo of options.cursorEventsRepos ?? []) {
|
|
558
|
+
if (!isValidCursorEventRepo(repo)) {
|
|
559
|
+
ctx.err(`Invalid --cursor-events-repo: ${repo} (expected owner/name)\n`);
|
|
560
|
+
return 1;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (!ctx.json && gitRepoUrl !== undefined) {
|
|
565
|
+
const inferredBits: string[] = [];
|
|
566
|
+
if (options.repo === undefined && inferred.gitRepoUrl !== undefined) {
|
|
567
|
+
inferredBits.push("repo");
|
|
568
|
+
}
|
|
569
|
+
if (options.ref === undefined && inferred.gitRef !== undefined) {
|
|
570
|
+
inferredBits.push("ref");
|
|
571
|
+
}
|
|
572
|
+
if (options.path === undefined && inferred.agentPath !== undefined) {
|
|
573
|
+
inferredBits.push("path");
|
|
574
|
+
}
|
|
575
|
+
ctx.err(
|
|
576
|
+
` source: ${gitRepoUrl}${gitRef === undefined ? "" : ` @ ${gitRef}`}\n` +
|
|
577
|
+
(agentPath === undefined ? "" : ` path: ${agentPath}\n`) +
|
|
578
|
+
(inferredBits.length === 0
|
|
579
|
+
? ""
|
|
580
|
+
: ` (inferred ${inferredBits.join(", ")} from git; pass --repo/--ref/--path to override)\n`)
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// The package manifest (`defineAgent({ hosting })`) is the source of
|
|
585
|
+
// truth for egress domains and expected secret names; flags override or
|
|
586
|
+
// augment it. Directories without an agent project (static placeholder
|
|
587
|
+
// deploys) simply have no manifest.
|
|
588
|
+
const hosting = isAgent ? await readHostingManifest(dir) : undefined;
|
|
589
|
+
const egressAllowedDomains = [
|
|
590
|
+
...new Set([
|
|
591
|
+
...(hosting?.egressDomains ?? []),
|
|
592
|
+
...(options.allowDomains ?? []),
|
|
593
|
+
]),
|
|
594
|
+
];
|
|
595
|
+
|
|
596
|
+
// Today's only source shape: the control plane builds from a git ref (or
|
|
597
|
+
// serves the static placeholder). See DeploySource for the artifact seam.
|
|
598
|
+
const source: DeploySource = {
|
|
599
|
+
kind: "git-ref",
|
|
600
|
+
...(gitRepoUrl === undefined ? {} : { gitRepoUrl }),
|
|
601
|
+
...(gitRef === undefined ? {} : { gitRef }),
|
|
602
|
+
...(agentPath === undefined ? {} : { agentPath }),
|
|
603
|
+
...(options.cursorEventsRepos === undefined ||
|
|
604
|
+
options.cursorEventsRepos.length === 0
|
|
605
|
+
? {}
|
|
606
|
+
: { cursorEventRepos: options.cursorEventsRepos }),
|
|
607
|
+
...(egressAllowedDomains.length === 0 ? {} : { egressAllowedDomains }),
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
let accepted: DeployAccepted;
|
|
611
|
+
try {
|
|
612
|
+
accepted = await ctx.client.deploy({ teamId: ctx.teamId, slug, source });
|
|
613
|
+
} catch (error) {
|
|
614
|
+
return renderApiError(error, ctx.err);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const aliasUrl = ctx.client.aliasUrl(accepted.engineAlias);
|
|
618
|
+
if (!ctx.json && accepted.aliasToken !== undefined) {
|
|
619
|
+
ctx.out(aliasTokenBlock(accepted.aliasToken));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (ctx.noWait) {
|
|
623
|
+
if (extras?.jsonCollect !== undefined) {
|
|
624
|
+
extras.jsonCollect.push(accepted);
|
|
625
|
+
} else if (ctx.json) {
|
|
626
|
+
ctx.out(`${JSON.stringify(accepted, null, 2)}\n`);
|
|
627
|
+
} else {
|
|
628
|
+
ctx.out(
|
|
629
|
+
`Deploy accepted: ${slug} (team ${ctx.teamId}, generation ${accepted.generation}, ${accepted.deploymentKind})\n` +
|
|
630
|
+
` alias: ${aliasUrl}\n` +
|
|
631
|
+
`Check progress with \`${CLI} deployment ${slug} --team ${ctx.teamId}\`.\n`
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
return 0;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
let detail: DeploymentDetail;
|
|
638
|
+
try {
|
|
639
|
+
detail = await pollUntil(ctx, slug, TERMINAL_DEPLOY_STATUSES);
|
|
640
|
+
} catch (error) {
|
|
641
|
+
return renderApiError(error, ctx.err);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
warnMissingSecrets(ctx, slug, hosting?.secretNames, detail.secretNames);
|
|
645
|
+
|
|
646
|
+
const payload =
|
|
647
|
+
accepted.aliasToken === undefined
|
|
648
|
+
? detail
|
|
649
|
+
: { ...detail, aliasToken: accepted.aliasToken };
|
|
650
|
+
|
|
651
|
+
if (extras?.jsonCollect !== undefined) {
|
|
652
|
+
extras.jsonCollect.push(payload);
|
|
653
|
+
return detail.status === "running" ? 0 : 1;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (ctx.json) {
|
|
657
|
+
ctx.out(`${JSON.stringify(payload, null, 2)}\n`);
|
|
658
|
+
return detail.status === "running" ? 0 : 1;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (detail.status !== "running") {
|
|
662
|
+
ctx.err(
|
|
663
|
+
`Deployment ${slug} ended ${detail.status}${
|
|
664
|
+
detail.lastError === null || detail.lastError === undefined
|
|
665
|
+
? ""
|
|
666
|
+
: `: ${detail.lastError}`
|
|
667
|
+
}\n`
|
|
668
|
+
);
|
|
669
|
+
return 1;
|
|
670
|
+
}
|
|
671
|
+
ctx.out(
|
|
672
|
+
`Deployed ${slug} (team ${ctx.teamId})\n` +
|
|
673
|
+
` status: running\n` +
|
|
674
|
+
` generation: ${detail.generation}\n` +
|
|
675
|
+
` kind: ${detail.deploymentKind}\n` +
|
|
676
|
+
` alias: ${aliasUrl}\n` +
|
|
677
|
+
` playground: ${CLI} playground --prod --slug ${slug} --team ${ctx.teamId}\n`
|
|
678
|
+
);
|
|
679
|
+
return 0;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Read the `hosting` block from the project manifest at `dir`, or
|
|
684
|
+
* undefined when the directory is not an agent project (static deploys
|
|
685
|
+
* have nothing to discover).
|
|
686
|
+
*/
|
|
687
|
+
async function readHostingManifest(
|
|
688
|
+
dir: string
|
|
689
|
+
): Promise<AgentHostingOptions | undefined> {
|
|
690
|
+
if (!(await isAgentProjectDir(dir))) {
|
|
691
|
+
return undefined;
|
|
692
|
+
}
|
|
693
|
+
const project = await loadAgentProject(dir);
|
|
694
|
+
return project.agent.hosting;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Friendly post-deploy nudge: the manifest declares secrets the agent
|
|
699
|
+
* expects, but the deployment does not have them all set yet.
|
|
700
|
+
*/
|
|
701
|
+
function warnMissingSecrets(
|
|
702
|
+
ctx: DeployCliContext,
|
|
703
|
+
slug: string,
|
|
704
|
+
declared: string[] | undefined,
|
|
705
|
+
set: string[] | undefined
|
|
706
|
+
): void {
|
|
707
|
+
if (declared === undefined || declared.length === 0) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const setNames = new Set(set ?? []);
|
|
711
|
+
const missing = declared.filter((name) => !setNames.has(name));
|
|
712
|
+
if (missing.length === 0) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
ctx.err(
|
|
716
|
+
`warning: secrets declared in the agent manifest but not set on ${slug}: ${missing.join(", ")}\n` +
|
|
717
|
+
` set them with \`${CLI} secrets set ${slug} ${missing.join(" ")}\`\n`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// ============================================================================
|
|
722
|
+
// deployments (list)
|
|
723
|
+
// ============================================================================
|
|
724
|
+
|
|
725
|
+
export async function cmdDeployments(
|
|
726
|
+
options: DeployCliOptions
|
|
727
|
+
): Promise<number> {
|
|
728
|
+
const ctx = await prepare(options);
|
|
729
|
+
if (typeof ctx === "number") {
|
|
730
|
+
return ctx;
|
|
731
|
+
}
|
|
732
|
+
let deployments: Deployment[];
|
|
733
|
+
try {
|
|
734
|
+
deployments = await ctx.client.listDeployments({ teamId: ctx.teamId });
|
|
735
|
+
} catch (error) {
|
|
736
|
+
return renderApiError(error, ctx.err);
|
|
737
|
+
}
|
|
738
|
+
if (ctx.json) {
|
|
739
|
+
ctx.out(`${JSON.stringify({ deployments }, null, 2)}\n`);
|
|
740
|
+
return 0;
|
|
741
|
+
}
|
|
742
|
+
if (deployments.length === 0) {
|
|
743
|
+
ctx.out(
|
|
744
|
+
`No deployments on team ${ctx.teamId}. Create one with \`${CLI} deploy\`.\n`
|
|
745
|
+
);
|
|
746
|
+
return 0;
|
|
747
|
+
}
|
|
748
|
+
ctx.out(
|
|
749
|
+
`${renderTable(
|
|
750
|
+
["SLUG", "STATUS", "GEN", "KIND", "UPDATED"],
|
|
751
|
+
deployments.map((d) => [
|
|
752
|
+
d.slug,
|
|
753
|
+
d.status,
|
|
754
|
+
String(d.generation),
|
|
755
|
+
d.deploymentKind,
|
|
756
|
+
d.desiredUpdatedAt ?? "",
|
|
757
|
+
])
|
|
758
|
+
)}\n`
|
|
759
|
+
);
|
|
760
|
+
return 0;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// ============================================================================
|
|
764
|
+
// deployment <slug> (detail)
|
|
765
|
+
// ============================================================================
|
|
766
|
+
|
|
767
|
+
export async function cmdDeploymentStatus(
|
|
768
|
+
slug: string | undefined,
|
|
769
|
+
options: DeployCliOptions
|
|
770
|
+
): Promise<number> {
|
|
771
|
+
if (slug === undefined || slug === "") {
|
|
772
|
+
(options.err ?? ((t: string) => void process.stderr.write(t)))(
|
|
773
|
+
`Usage: ${CLI} deployment <slug> [--team <id>] [--json]\n`
|
|
774
|
+
);
|
|
775
|
+
return 1;
|
|
776
|
+
}
|
|
777
|
+
const ctx = await prepare(options);
|
|
778
|
+
if (typeof ctx === "number") {
|
|
779
|
+
return ctx;
|
|
780
|
+
}
|
|
781
|
+
let detail: DeploymentDetail;
|
|
782
|
+
try {
|
|
783
|
+
detail = await ctx.client.getDeployment({ teamId: ctx.teamId, slug });
|
|
784
|
+
} catch (error) {
|
|
785
|
+
return renderApiError(error, ctx.err);
|
|
786
|
+
}
|
|
787
|
+
if (ctx.json) {
|
|
788
|
+
ctx.out(`${JSON.stringify(detail, null, 2)}\n`);
|
|
789
|
+
return 0;
|
|
790
|
+
}
|
|
791
|
+
const aliasUrl = ctx.client.aliasUrl(detail.engineAlias);
|
|
792
|
+
const lines = [
|
|
793
|
+
`${detail.slug} (team ${detail.teamId})`,
|
|
794
|
+
` status: ${detail.status}`,
|
|
795
|
+
` generation: ${detail.generation}`,
|
|
796
|
+
` kind: ${detail.deploymentKind}`,
|
|
797
|
+
` alias: ${aliasUrl}`,
|
|
798
|
+
];
|
|
799
|
+
if (detail.gitRepoUrl !== null && detail.gitRepoUrl !== undefined) {
|
|
800
|
+
lines.push(` repo: ${detail.gitRepoUrl} @ ${detail.gitRef ?? ""}`);
|
|
801
|
+
}
|
|
802
|
+
if (
|
|
803
|
+
detail.serviceAccountId !== undefined &&
|
|
804
|
+
detail.serviceAccountId !== null
|
|
805
|
+
) {
|
|
806
|
+
lines.push(` service account: ${detail.serviceAccountId}`);
|
|
807
|
+
}
|
|
808
|
+
if (
|
|
809
|
+
detail.egressAllowedDomains !== undefined &&
|
|
810
|
+
detail.egressAllowedDomains.length > 0
|
|
811
|
+
) {
|
|
812
|
+
lines.push(` egress: ${detail.egressAllowedDomains.join(", ")}`);
|
|
813
|
+
}
|
|
814
|
+
if (detail.secretNames !== undefined && detail.secretNames.length > 0) {
|
|
815
|
+
lines.push(` secrets: ${detail.secretNames.join(", ")}`);
|
|
816
|
+
}
|
|
817
|
+
if (
|
|
818
|
+
detail.podCredentialMaskedKey !== undefined &&
|
|
819
|
+
detail.podCredentialMaskedKey !== null
|
|
820
|
+
) {
|
|
821
|
+
lines.push(` pod credential: ${detail.podCredentialMaskedKey}`);
|
|
822
|
+
}
|
|
823
|
+
if (detail.engineUrl !== null && detail.engineUrl !== undefined) {
|
|
824
|
+
lines.push(` engine: ${detail.engineUrl}`);
|
|
825
|
+
}
|
|
826
|
+
if (
|
|
827
|
+
detail.engineAccess !== null &&
|
|
828
|
+
detail.engineAccess !== undefined &&
|
|
829
|
+
typeof detail.engineAccess === "object"
|
|
830
|
+
) {
|
|
831
|
+
lines.push(
|
|
832
|
+
` engine access: ${detail.engineAccess.url} (expires ${detail.engineAccess.expiresAt})`
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
if (detail.lastError !== null && detail.lastError !== undefined) {
|
|
836
|
+
lines.push(` last error: ${detail.lastError}`);
|
|
837
|
+
}
|
|
838
|
+
if (detail.desiredUpdatedAt !== undefined) {
|
|
839
|
+
lines.push(` updated: ${detail.desiredUpdatedAt}`);
|
|
840
|
+
}
|
|
841
|
+
ctx.out(`${lines.join("\n")}\n`);
|
|
842
|
+
return 0;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// ============================================================================
|
|
846
|
+
// stop <slug>
|
|
847
|
+
// ============================================================================
|
|
848
|
+
|
|
849
|
+
export async function cmdStop(
|
|
850
|
+
slug: string | undefined,
|
|
851
|
+
options: DeployCliOptions
|
|
852
|
+
): Promise<number> {
|
|
853
|
+
if (slug === undefined || slug === "") {
|
|
854
|
+
(options.err ?? ((t: string) => void process.stderr.write(t)))(
|
|
855
|
+
`Usage: ${CLI} stop <slug> [--team <id>] [--json] [--no-wait]\n`
|
|
856
|
+
);
|
|
857
|
+
return 1;
|
|
858
|
+
}
|
|
859
|
+
const ctx = await prepare(options);
|
|
860
|
+
if (typeof ctx === "number") {
|
|
861
|
+
return ctx;
|
|
862
|
+
}
|
|
863
|
+
let accepted: StopAccepted;
|
|
864
|
+
try {
|
|
865
|
+
accepted = await ctx.client.stopDeployment({ teamId: ctx.teamId, slug });
|
|
866
|
+
} catch (error) {
|
|
867
|
+
return renderApiError(error, ctx.err);
|
|
868
|
+
}
|
|
869
|
+
if (ctx.noWait) {
|
|
870
|
+
if (ctx.json) {
|
|
871
|
+
ctx.out(`${JSON.stringify(accepted, null, 2)}\n`);
|
|
872
|
+
} else {
|
|
873
|
+
ctx.out(`Stop accepted for ${slug} (status: ${accepted.status}).\n`);
|
|
874
|
+
}
|
|
875
|
+
return 0;
|
|
876
|
+
}
|
|
877
|
+
let detail: DeploymentDetail;
|
|
878
|
+
try {
|
|
879
|
+
detail = await pollUntil(ctx, slug, new Set(["stopped"]));
|
|
880
|
+
} catch (error) {
|
|
881
|
+
return renderApiError(error, ctx.err);
|
|
882
|
+
}
|
|
883
|
+
if (ctx.json) {
|
|
884
|
+
ctx.out(`${JSON.stringify(detail, null, 2)}\n`);
|
|
885
|
+
} else {
|
|
886
|
+
ctx.out(`Stopped ${slug} (team ${ctx.teamId}).\n`);
|
|
887
|
+
}
|
|
888
|
+
return 0;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// ============================================================================
|
|
892
|
+
// rotate-token <slug>
|
|
893
|
+
// ============================================================================
|
|
894
|
+
|
|
895
|
+
export async function cmdRotateToken(
|
|
896
|
+
slug: string | undefined,
|
|
897
|
+
options: DeployCliOptions
|
|
898
|
+
): Promise<number> {
|
|
899
|
+
if (slug === undefined || slug === "") {
|
|
900
|
+
(options.err ?? ((t: string) => void process.stderr.write(t)))(
|
|
901
|
+
`Usage: ${CLI} rotate-token <slug> [--team <id>]\n`
|
|
902
|
+
);
|
|
903
|
+
return 1;
|
|
904
|
+
}
|
|
905
|
+
const ctx = await prepare(options);
|
|
906
|
+
if (typeof ctx === "number") {
|
|
907
|
+
return ctx;
|
|
908
|
+
}
|
|
909
|
+
let aliasToken: string;
|
|
910
|
+
try {
|
|
911
|
+
aliasToken = await ctx.client.rotateAliasToken({
|
|
912
|
+
teamId: ctx.teamId,
|
|
913
|
+
slug,
|
|
914
|
+
});
|
|
915
|
+
} catch (error) {
|
|
916
|
+
return renderApiError(error, ctx.err);
|
|
917
|
+
}
|
|
918
|
+
if (ctx.json) {
|
|
919
|
+
ctx.out(`${JSON.stringify({ aliasToken }, null, 2)}\n`);
|
|
920
|
+
return 0;
|
|
921
|
+
}
|
|
922
|
+
ctx.out(
|
|
923
|
+
`Rotated the alias token for ${slug}. The old token stopped working immediately.\n${aliasTokenBlock(aliasToken)}`
|
|
924
|
+
);
|
|
925
|
+
return 0;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// ============================================================================
|
|
929
|
+
// rotate-pod-credential <slug>
|
|
930
|
+
// ============================================================================
|
|
931
|
+
|
|
932
|
+
export async function cmdRotatePodCredential(
|
|
933
|
+
slug: string | undefined,
|
|
934
|
+
options: DeployCliOptions
|
|
935
|
+
): Promise<number> {
|
|
936
|
+
if (slug === undefined || slug === "") {
|
|
937
|
+
(options.err ?? ((t: string) => void process.stderr.write(t)))(
|
|
938
|
+
`Usage: ${CLI} rotate-pod-credential <slug> [--team <id>]\n`
|
|
939
|
+
);
|
|
940
|
+
return 1;
|
|
941
|
+
}
|
|
942
|
+
const ctx = await prepare(options);
|
|
943
|
+
if (typeof ctx === "number") {
|
|
944
|
+
return ctx;
|
|
945
|
+
}
|
|
946
|
+
let podCredentialMaskedKey: string;
|
|
947
|
+
try {
|
|
948
|
+
podCredentialMaskedKey = await ctx.client.rotatePodCredential({
|
|
949
|
+
teamId: ctx.teamId,
|
|
950
|
+
slug,
|
|
951
|
+
});
|
|
952
|
+
} catch (error) {
|
|
953
|
+
return renderApiError(error, ctx.err);
|
|
954
|
+
}
|
|
955
|
+
if (ctx.json) {
|
|
956
|
+
ctx.out(`${JSON.stringify({ podCredentialMaskedKey }, null, 2)}\n`);
|
|
957
|
+
return 0;
|
|
958
|
+
}
|
|
959
|
+
ctx.out(
|
|
960
|
+
`Rotated the pod credential for ${slug}: ${podCredentialMaskedKey}\n` +
|
|
961
|
+
`The running pod keeps the old credential until the next deploy — run \`${CLI} deploy --slug ${slug}\` to apply it.\n`
|
|
962
|
+
);
|
|
963
|
+
return 0;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// ============================================================================
|
|
967
|
+
// secrets set|list|unset
|
|
968
|
+
// ============================================================================
|
|
969
|
+
|
|
970
|
+
export async function cmdSecretsSet(
|
|
971
|
+
slug: string | undefined,
|
|
972
|
+
specs: string[],
|
|
973
|
+
options: DeployCliOptions
|
|
974
|
+
): Promise<number> {
|
|
975
|
+
const usageErr = options.err ?? ((t: string) => void process.stderr.write(t));
|
|
976
|
+
if (slug === undefined || slug === "" || specs.length === 0) {
|
|
977
|
+
usageErr(
|
|
978
|
+
`Usage: ${CLI} secrets set <slug> NAME[=VALUE] [NAME2[=VALUE2] ...] [--team <id>] [--json]\n` +
|
|
979
|
+
`NAMEs without =VALUE are prompted for (hidden input; reads stdin when piped).\n`
|
|
980
|
+
);
|
|
981
|
+
return 1;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Parse and validate every name before prompting for any value, so a
|
|
985
|
+
// typo in the last name cannot waste typed-in secrets.
|
|
986
|
+
const parsed: Array<{ name: string; value?: string }> = [];
|
|
987
|
+
for (const spec of specs) {
|
|
988
|
+
const eq = spec.indexOf("=");
|
|
989
|
+
const name = eq === -1 ? spec : spec.slice(0, eq);
|
|
990
|
+
const nameError = validateSettableSecretName(name);
|
|
991
|
+
if (nameError !== undefined) {
|
|
992
|
+
usageErr(`${nameError}\n`);
|
|
993
|
+
return 1;
|
|
994
|
+
}
|
|
995
|
+
parsed.push(eq === -1 ? { name } : { name, value: spec.slice(eq + 1) });
|
|
996
|
+
}
|
|
997
|
+
if (parsed.some((entry) => entry.value !== undefined)) {
|
|
998
|
+
usageErr(
|
|
999
|
+
`warning: secret values passed as arguments can end up in shell history; prefer \`${CLI} secrets set ${slug} NAME\` and enter the value at the hidden prompt.\n`
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const ctx = await prepare(options);
|
|
1004
|
+
if (typeof ctx === "number") {
|
|
1005
|
+
return ctx;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const stdin = options.stdin ?? process.stdin;
|
|
1009
|
+
const readValue = createSecretValueReader(stdin, ctx.err);
|
|
1010
|
+
const secrets: Array<{ name: string; value: string }> = [];
|
|
1011
|
+
for (const entry of parsed) {
|
|
1012
|
+
secrets.push({
|
|
1013
|
+
name: entry.name,
|
|
1014
|
+
value: entry.value ?? (await readValue(entry.name)),
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
let secretNames: string[];
|
|
1019
|
+
try {
|
|
1020
|
+
secretNames = await ctx.client.setSecrets({
|
|
1021
|
+
teamId: ctx.teamId,
|
|
1022
|
+
slug,
|
|
1023
|
+
secrets,
|
|
1024
|
+
});
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
return renderApiError(error, ctx.err);
|
|
1027
|
+
}
|
|
1028
|
+
if (ctx.json) {
|
|
1029
|
+
ctx.out(`${JSON.stringify({ secretNames }, null, 2)}\n`);
|
|
1030
|
+
return 0;
|
|
1031
|
+
}
|
|
1032
|
+
ctx.out(
|
|
1033
|
+
`Set ${secrets.length} secret${secrets.length === 1 ? "" : "s"} on ${slug}.\n` +
|
|
1034
|
+
`Secrets now set: ${secretNames.join(", ")}\n` +
|
|
1035
|
+
`The engine picks them up on its next deploy.\n`
|
|
1036
|
+
);
|
|
1037
|
+
return 0;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
export async function cmdSecretsList(
|
|
1041
|
+
slug: string | undefined,
|
|
1042
|
+
options: DeployCliOptions
|
|
1043
|
+
): Promise<number> {
|
|
1044
|
+
if (slug === undefined || slug === "") {
|
|
1045
|
+
(options.err ?? ((t: string) => void process.stderr.write(t)))(
|
|
1046
|
+
`Usage: ${CLI} secrets list <slug> [--team <id>] [--json]\n`
|
|
1047
|
+
);
|
|
1048
|
+
return 1;
|
|
1049
|
+
}
|
|
1050
|
+
const ctx = await prepare(options);
|
|
1051
|
+
if (typeof ctx === "number") {
|
|
1052
|
+
return ctx;
|
|
1053
|
+
}
|
|
1054
|
+
let secrets: SecretInfo[];
|
|
1055
|
+
try {
|
|
1056
|
+
secrets = await ctx.client.listSecrets({ teamId: ctx.teamId, slug });
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
return renderApiError(error, ctx.err);
|
|
1059
|
+
}
|
|
1060
|
+
if (ctx.json) {
|
|
1061
|
+
ctx.out(`${JSON.stringify({ secrets }, null, 2)}\n`);
|
|
1062
|
+
return 0;
|
|
1063
|
+
}
|
|
1064
|
+
if (secrets.length === 0) {
|
|
1065
|
+
ctx.out(
|
|
1066
|
+
`No secrets set on ${slug}. Add one with \`${CLI} secrets set ${slug} NAME\`.\n`
|
|
1067
|
+
);
|
|
1068
|
+
return 0;
|
|
1069
|
+
}
|
|
1070
|
+
ctx.out(
|
|
1071
|
+
`${renderTable(
|
|
1072
|
+
["NAME", "CREATED"],
|
|
1073
|
+
secrets.map((secret) => [secret.name, secret.createdAt])
|
|
1074
|
+
)}\n`
|
|
1075
|
+
);
|
|
1076
|
+
return 0;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
export async function cmdSecretsUnset(
|
|
1080
|
+
slug: string | undefined,
|
|
1081
|
+
name: string | undefined,
|
|
1082
|
+
options: DeployCliOptions
|
|
1083
|
+
): Promise<number> {
|
|
1084
|
+
if (slug === undefined || slug === "" || name === undefined || name === "") {
|
|
1085
|
+
(options.err ?? ((t: string) => void process.stderr.write(t)))(
|
|
1086
|
+
`Usage: ${CLI} secrets unset <slug> NAME [--team <id>]\n`
|
|
1087
|
+
);
|
|
1088
|
+
return 1;
|
|
1089
|
+
}
|
|
1090
|
+
const ctx = await prepare(options);
|
|
1091
|
+
if (typeof ctx === "number") {
|
|
1092
|
+
return ctx;
|
|
1093
|
+
}
|
|
1094
|
+
try {
|
|
1095
|
+
await ctx.client.deleteSecret({ teamId: ctx.teamId, slug, name });
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
if (error instanceof DeployApiError && error.status === 404) {
|
|
1098
|
+
ctx.err(
|
|
1099
|
+
`No secret named ${name} on deployment ${slug} — check \`${CLI} secrets list ${slug}\` (and the slug / --team).\n`
|
|
1100
|
+
);
|
|
1101
|
+
return 1;
|
|
1102
|
+
}
|
|
1103
|
+
return renderApiError(error, ctx.err);
|
|
1104
|
+
}
|
|
1105
|
+
if (ctx.json) {
|
|
1106
|
+
ctx.out(`${JSON.stringify({ removed: name }, null, 2)}\n`);
|
|
1107
|
+
} else {
|
|
1108
|
+
ctx.out(`Removed secret ${name} from ${slug}.\n`);
|
|
1109
|
+
}
|
|
1110
|
+
return 0;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* Returns a reader for interactive secret values: hidden (no-echo) prompt
|
|
1115
|
+
* when `stdin` is a TTY, otherwise one line per secret from the piped
|
|
1116
|
+
* stream. One reader per command so piped lines are consumed in order.
|
|
1117
|
+
*/
|
|
1118
|
+
function createSecretValueReader(
|
|
1119
|
+
stdin: SecretInputStream,
|
|
1120
|
+
err: (text: string) => void
|
|
1121
|
+
): (name: string) => Promise<string> {
|
|
1122
|
+
if (stdin.isTTY === true) {
|
|
1123
|
+
return (name) => promptHidden(stdin, err, `Value for ${name}: `);
|
|
1124
|
+
}
|
|
1125
|
+
const readLine = createLineReader(stdin);
|
|
1126
|
+
return async (name) => {
|
|
1127
|
+
const line = await readLine();
|
|
1128
|
+
if (line === undefined) {
|
|
1129
|
+
throw new DeployApiError(
|
|
1130
|
+
`stdin ended before a value for ${name} was provided (pipe one line per secret).`,
|
|
1131
|
+
0
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
return line;
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/** Raw-mode hidden prompt: echoes nothing, supports backspace and Ctrl-C. */
|
|
1139
|
+
function promptHidden(
|
|
1140
|
+
stdin: SecretInputStream,
|
|
1141
|
+
err: (text: string) => void,
|
|
1142
|
+
prompt: string
|
|
1143
|
+
): Promise<string> {
|
|
1144
|
+
return new Promise((resolvePromise, reject) => {
|
|
1145
|
+
err(prompt);
|
|
1146
|
+
stdin.setRawMode?.(true);
|
|
1147
|
+
stdin.resume();
|
|
1148
|
+
let value = "";
|
|
1149
|
+
const cleanup = (): void => {
|
|
1150
|
+
stdin.off("data", onData);
|
|
1151
|
+
stdin.setRawMode?.(false);
|
|
1152
|
+
stdin.pause();
|
|
1153
|
+
err("\n");
|
|
1154
|
+
};
|
|
1155
|
+
const onData = (chunk: Buffer | string): void => {
|
|
1156
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
1157
|
+
for (const ch of text) {
|
|
1158
|
+
if (ch === "\r" || ch === "\n") {
|
|
1159
|
+
cleanup();
|
|
1160
|
+
resolvePromise(value);
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
if (ch === "\u0003") {
|
|
1164
|
+
cleanup();
|
|
1165
|
+
reject(new DeployApiError("Aborted.", 0));
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (ch === "\u007f" || ch === "\b") {
|
|
1169
|
+
value = value.slice(0, -1);
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
value += ch;
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
stdin.on("data", onData);
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/** Buffered line reader over a piped stream; undefined once it ends. */
|
|
1180
|
+
function createLineReader(
|
|
1181
|
+
stream: NodeJS.ReadableStream
|
|
1182
|
+
): () => Promise<string | undefined> {
|
|
1183
|
+
let buffer = "";
|
|
1184
|
+
let ended = false;
|
|
1185
|
+
const waiters: Array<(line: string | undefined) => void> = [];
|
|
1186
|
+
|
|
1187
|
+
const drain = (): void => {
|
|
1188
|
+
while (waiters.length > 0) {
|
|
1189
|
+
const newline = buffer.indexOf("\n");
|
|
1190
|
+
if (newline !== -1) {
|
|
1191
|
+
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
1192
|
+
buffer = buffer.slice(newline + 1);
|
|
1193
|
+
waiters.shift()?.(line);
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
if (!ended) {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
// Stream ended: a trailing unterminated line still counts once.
|
|
1200
|
+
const rest = buffer;
|
|
1201
|
+
buffer = "";
|
|
1202
|
+
waiters.shift()?.(rest === "" ? undefined : rest.replace(/\r$/, ""));
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
|
|
1206
|
+
stream.on("data", (chunk: Buffer | string) => {
|
|
1207
|
+
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
1208
|
+
drain();
|
|
1209
|
+
});
|
|
1210
|
+
stream.on("end", () => {
|
|
1211
|
+
ended = true;
|
|
1212
|
+
drain();
|
|
1213
|
+
});
|
|
1214
|
+
stream.resume();
|
|
1215
|
+
|
|
1216
|
+
return () =>
|
|
1217
|
+
new Promise((resolvePromise) => {
|
|
1218
|
+
waiters.push(resolvePromise);
|
|
1219
|
+
drain();
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// ============================================================================
|
|
1224
|
+
// Polling + rendering helpers
|
|
1225
|
+
// ============================================================================
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* Poll deployment status every `pollIntervalMs` until it lands in
|
|
1229
|
+
* `terminal` (single-line progress on a TTY, one line per transition
|
|
1230
|
+
* otherwise). Throws DeployApiError on request failures or timeout.
|
|
1231
|
+
*/
|
|
1232
|
+
async function pollUntil(
|
|
1233
|
+
ctx: DeployCliContext,
|
|
1234
|
+
slug: string,
|
|
1235
|
+
terminal: ReadonlySet<DeploymentStatus>
|
|
1236
|
+
): Promise<DeploymentDetail> {
|
|
1237
|
+
const startedAt = Date.now();
|
|
1238
|
+
const tty = process.stderr.isTTY === true;
|
|
1239
|
+
let lastStatus: DeploymentStatus | undefined;
|
|
1240
|
+
let sawProgress = false;
|
|
1241
|
+
|
|
1242
|
+
const report = (status: DeploymentStatus): void => {
|
|
1243
|
+
if (ctx.json) {
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
const elapsed = Math.round((Date.now() - startedAt) / 1000);
|
|
1247
|
+
if (tty) {
|
|
1248
|
+
ctx.err(`\r\x1b[2K${slug}: ${status} (${elapsed}s)`);
|
|
1249
|
+
sawProgress = true;
|
|
1250
|
+
} else if (status !== lastStatus) {
|
|
1251
|
+
ctx.err(`${slug}: ${status}\n`);
|
|
1252
|
+
}
|
|
1253
|
+
lastStatus = status;
|
|
1254
|
+
};
|
|
1255
|
+
const finishProgressLine = (): void => {
|
|
1256
|
+
if (tty && sawProgress) {
|
|
1257
|
+
ctx.err("\n");
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
for (;;) {
|
|
1262
|
+
const detail = await ctx.client
|
|
1263
|
+
.getDeployment({ teamId: ctx.teamId, slug })
|
|
1264
|
+
.catch((error: unknown) => {
|
|
1265
|
+
finishProgressLine();
|
|
1266
|
+
throw error;
|
|
1267
|
+
});
|
|
1268
|
+
report(detail.status);
|
|
1269
|
+
if (terminal.has(detail.status)) {
|
|
1270
|
+
finishProgressLine();
|
|
1271
|
+
return detail;
|
|
1272
|
+
}
|
|
1273
|
+
if (Date.now() - startedAt >= ctx.timeoutMs) {
|
|
1274
|
+
finishProgressLine();
|
|
1275
|
+
throw new DeployApiError(
|
|
1276
|
+
`Timed out after ${Math.round(ctx.timeoutMs / 1000)}s waiting for ${slug} (last status: ${detail.status}). It may still converge — check \`${CLI} deployment ${slug} --team ${ctx.teamId}\`.`,
|
|
1277
|
+
0
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
await sleep(ctx.pollIntervalMs);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function aliasTokenBlock(aliasToken: string): string {
|
|
1285
|
+
return (
|
|
1286
|
+
`\n Alias token (shown ONCE — save it now, it cannot be retrieved again):\n\n` +
|
|
1287
|
+
` ${aliasToken}\n\n` +
|
|
1288
|
+
` Send it as the X-Agent-Alias-Token header on requests to the alias URL.\n` +
|
|
1289
|
+
` Lost tokens: \`${CLI} rotate-token <slug>\` mints a replacement.\n\n`
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function renderApiError(error: unknown, err: (text: string) => void): number {
|
|
1294
|
+
if (error instanceof DeployApiError) {
|
|
1295
|
+
err(`${error.message}\n`);
|
|
1296
|
+
return 1;
|
|
1297
|
+
}
|
|
1298
|
+
throw error;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function renderTable(header: string[], rows: string[][]): string {
|
|
1302
|
+
const widths = header.map((h, i) =>
|
|
1303
|
+
Math.max(h.length, ...rows.map((row) => (row[i] ?? "").length))
|
|
1304
|
+
);
|
|
1305
|
+
const renderRow = (row: string[]): string =>
|
|
1306
|
+
row
|
|
1307
|
+
.map((cell, i) => cell.padEnd(widths[i] ?? 0))
|
|
1308
|
+
.join(" ")
|
|
1309
|
+
.trimEnd();
|
|
1310
|
+
return [renderRow(header), ...rows.map(renderRow)].join("\n");
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function describeError(error: unknown): string {
|
|
1314
|
+
return error instanceof Error ? error.message : String(error);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function sleep(ms: number): Promise<void> {
|
|
1318
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
1319
|
+
}
|