@cursor/july 0.1.1 → 0.1.2
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 +24 -2
- package/README.md +25 -15
- package/dist/bin/agent-serve.js +101 -12
- 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.Oje4vhlk.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.zwQ9RCQ7.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BaLEdS15.js → VPLocalSearchBox.H5XZ2zCB.js} +1 -1
- package/dist/docs/assets/chunks/{theme.CZRvu_0q.js → theme.CTR_TuaE.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.CfU8_uTC.js +192 -0
- package/dist/docs/assets/quickstart.md.CfU8_uTC.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_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 +180 -23
- package/dist/docs/reference/agent-config.html +7 -7
- package/dist/docs/reference/channels.html +8 -8
- package/dist/docs/reference/cli.html +4 -4
- 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/cli-deploy.d.ts +52 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -0
- package/dist/internal/cli-deploy.js +731 -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/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 +176 -0
- package/dist/internal/deploy-client.d.ts.map +1 -0
- package/dist/internal/deploy-client.js +375 -0
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +73 -5
- 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 +37 -19
- 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/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/server.d.ts.map +1 -1
- package/dist/internal/server.js +173 -42
- package/dist/internal/session-engine.d.ts +49 -0
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +246 -17
- 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/playground/assets/cursor-icons-outline-BxTT_FVJ.woff2 +0 -0
- package/dist/playground/assets/index-B1Qc9h2u.css +1 -0
- package/dist/playground/assets/{index-FlWjhg3x.js → index-CpDYCj8W.js} +42 -42
- 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 +44 -3
- 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 +329 -51
- package/docs/reference/instructions.md +8 -6
- package/docs/scaffolding-agents.md +1 -1
- package/docs/storage.md +98 -0
- package/package.json +8 -1
- package/skills/github/SKILL.md +9 -1
- package/src/bin/agent-serve.ts +139 -0
- 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/cli-deploy.ts +940 -0
- package/src/internal/cursor/backend-client.ts +103 -1
- package/src/internal/cursor/github-credentials.ts +248 -0
- package/src/internal/deploy-client.ts +591 -0
- package/src/internal/discovery.ts +88 -1
- package/src/internal/distribution.ts +1 -0
- package/src/internal/eval-run-store.ts +48 -19
- 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/reminder-runner.ts +50 -6
- package/src/internal/reminder-store.ts +21 -0
- package/src/internal/server.ts +213 -27
- package/src/internal/session-engine.ts +285 -7
- package/src/internal/storage-coordinator.ts +615 -0
- package/src/storage.ts +325 -0
- package/src/types.ts +40 -2
- 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/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,940 @@
|
|
|
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
|
+
cursorBackendUrl,
|
|
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 { isAgentProjectDir, loadAgentProject } from "./discovery.js";
|
|
35
|
+
import { CLI_COMMAND_NAME as CLI } from "./distribution.js";
|
|
36
|
+
import {
|
|
37
|
+
isValidCursorEventRepo,
|
|
38
|
+
validateSettableSecretName,
|
|
39
|
+
} from "./hosting.js";
|
|
40
|
+
|
|
41
|
+
const DEFAULT_POLL_INTERVAL_MS = 3_000;
|
|
42
|
+
const DEFAULT_DEPLOY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
43
|
+
|
|
44
|
+
export interface DeployCliOptions {
|
|
45
|
+
dir?: string;
|
|
46
|
+
slug?: string;
|
|
47
|
+
/** `--team <id>`; resolved from the account when omitted. */
|
|
48
|
+
team?: string;
|
|
49
|
+
json?: boolean;
|
|
50
|
+
noWait?: boolean;
|
|
51
|
+
apiKey?: string;
|
|
52
|
+
/** deploy `--repo <url>`: https git repo the control plane builds from. */
|
|
53
|
+
repo?: string;
|
|
54
|
+
/** deploy `--ref <ref>`: git ref (defaults to the default branch server-side). */
|
|
55
|
+
ref?: string;
|
|
56
|
+
/** deploy `--path <agentPath>`: agent directory relative to the repo root. */
|
|
57
|
+
path?: string;
|
|
58
|
+
/** deploy `--cursor-events-repo <owner/name>` (repeatable). */
|
|
59
|
+
cursorEventsRepos?: string[];
|
|
60
|
+
/** deploy `--allow-domain <domain>` (repeatable; unioned with the manifest). */
|
|
61
|
+
allowDomains?: string[];
|
|
62
|
+
/** Test seams. */
|
|
63
|
+
fetchImpl?: typeof fetch;
|
|
64
|
+
pollIntervalMs?: number;
|
|
65
|
+
timeoutMs?: number;
|
|
66
|
+
out?: (text: string) => void;
|
|
67
|
+
err?: (text: string) => void;
|
|
68
|
+
/** `secrets set` value input (hidden prompt on a TTY, lines otherwise). */
|
|
69
|
+
stdin?: SecretInputStream;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Minimal stdin surface `secrets set` reads values from (test-injectable). */
|
|
73
|
+
export type SecretInputStream = NodeJS.ReadableStream & {
|
|
74
|
+
isTTY?: boolean;
|
|
75
|
+
setRawMode?: (mode: boolean) => void;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
interface DeployCliContext {
|
|
79
|
+
client: AgentServeDeployClient;
|
|
80
|
+
teamId: number;
|
|
81
|
+
json: boolean;
|
|
82
|
+
noWait: boolean;
|
|
83
|
+
pollIntervalMs: number;
|
|
84
|
+
timeoutMs: number;
|
|
85
|
+
out: (text: string) => void;
|
|
86
|
+
err: (text: string) => void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Shared preamble: credentials → client → team id. Returns an exit code
|
|
91
|
+
* (with the error already rendered) when resolution fails.
|
|
92
|
+
*/
|
|
93
|
+
async function prepare(
|
|
94
|
+
options: DeployCliOptions
|
|
95
|
+
): Promise<DeployCliContext | number> {
|
|
96
|
+
const out =
|
|
97
|
+
options.out ?? ((text: string) => void process.stdout.write(text));
|
|
98
|
+
const err =
|
|
99
|
+
options.err ?? ((text: string) => void process.stderr.write(text));
|
|
100
|
+
|
|
101
|
+
const resolved = await resolveApiKey(
|
|
102
|
+
options.apiKey === undefined ? undefined : { apiKey: options.apiKey }
|
|
103
|
+
);
|
|
104
|
+
if (resolved === undefined) {
|
|
105
|
+
err(`Not signed in to Cursor. ${SIGN_IN_HINT}\n`);
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
const client = new AgentServeDeployClient({
|
|
109
|
+
backendUrl: resolved.backendUrl,
|
|
110
|
+
apiKey: resolved.apiKey,
|
|
111
|
+
...(options.fetchImpl === undefined
|
|
112
|
+
? {}
|
|
113
|
+
: { fetchImpl: options.fetchImpl }),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
let teamId: number;
|
|
117
|
+
if (options.team !== undefined) {
|
|
118
|
+
teamId = Number.parseInt(options.team, 10);
|
|
119
|
+
if (Number.isNaN(teamId) || teamId <= 0) {
|
|
120
|
+
err(`Invalid --team: ${options.team} (expected a positive team id)\n`);
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
const account = new CursorBackendClient({
|
|
125
|
+
backendUrl: resolved.backendUrl,
|
|
126
|
+
apiKey: resolved.apiKey,
|
|
127
|
+
...(options.fetchImpl === undefined
|
|
128
|
+
? {}
|
|
129
|
+
: { fetchImpl: options.fetchImpl }),
|
|
130
|
+
});
|
|
131
|
+
let identity: Awaited<ReturnType<CursorBackendClient["getMe"]>>;
|
|
132
|
+
try {
|
|
133
|
+
identity = await account.getMe();
|
|
134
|
+
} catch (error) {
|
|
135
|
+
err(
|
|
136
|
+
`Could not resolve your team from the Cursor account: ${describeError(error)}\nPass --team <id> explicitly.\n`
|
|
137
|
+
);
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
if (identity.teamId === undefined || identity.teamId <= 0) {
|
|
141
|
+
err(
|
|
142
|
+
"This Cursor account has no default team; pass --team <id> (find it in the Cursor dashboard URL).\n"
|
|
143
|
+
);
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
teamId = identity.teamId;
|
|
147
|
+
err(
|
|
148
|
+
`Using team ${teamId}${
|
|
149
|
+
identity.teamName === undefined ? "" : ` (${identity.teamName})`
|
|
150
|
+
} — the account's team; pass --team to override.\n`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
client,
|
|
156
|
+
teamId,
|
|
157
|
+
json: options.json === true,
|
|
158
|
+
noWait: options.noWait === true,
|
|
159
|
+
pollIntervalMs: options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
|
|
160
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_DEPLOY_TIMEOUT_MS,
|
|
161
|
+
out,
|
|
162
|
+
err,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ============================================================================
|
|
167
|
+
// deploy
|
|
168
|
+
// ============================================================================
|
|
169
|
+
|
|
170
|
+
export async function cmdDeploy(options: DeployCliOptions): Promise<number> {
|
|
171
|
+
const ctx = await prepare(options);
|
|
172
|
+
if (typeof ctx === "number") {
|
|
173
|
+
return ctx;
|
|
174
|
+
}
|
|
175
|
+
const dir = resolve(options.dir ?? ".");
|
|
176
|
+
let slug: string;
|
|
177
|
+
if (options.slug !== undefined) {
|
|
178
|
+
const slugError = validateDeploymentSlug(options.slug);
|
|
179
|
+
if (slugError !== undefined) {
|
|
180
|
+
ctx.err(`${slugError}\n`);
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
slug = options.slug;
|
|
184
|
+
} else {
|
|
185
|
+
const derived = slugFromDirectoryName(basename(dir));
|
|
186
|
+
if (derived === undefined) {
|
|
187
|
+
ctx.err(
|
|
188
|
+
`Could not derive a deployment slug from the directory name ${JSON.stringify(basename(dir))}; pass --slug.\n`
|
|
189
|
+
);
|
|
190
|
+
return 1;
|
|
191
|
+
}
|
|
192
|
+
slug = derived;
|
|
193
|
+
ctx.err(`Deploying as "${slug}" (from the directory name).\n`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (options.repo !== undefined && !options.repo.startsWith("https://")) {
|
|
197
|
+
ctx.err(
|
|
198
|
+
`Invalid --repo: ${options.repo} (expected an https:// git repository URL)\n`
|
|
199
|
+
);
|
|
200
|
+
return 1;
|
|
201
|
+
}
|
|
202
|
+
for (const repo of options.cursorEventsRepos ?? []) {
|
|
203
|
+
if (!isValidCursorEventRepo(repo)) {
|
|
204
|
+
ctx.err(`Invalid --cursor-events-repo: ${repo} (expected owner/name)\n`);
|
|
205
|
+
return 1;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// The package manifest (`defineAgent({ hosting })`) is the source of
|
|
210
|
+
// truth for egress domains and expected secret names; flags override or
|
|
211
|
+
// augment it. Directories without an agent project (static placeholder
|
|
212
|
+
// deploys) simply have no manifest.
|
|
213
|
+
const hosting = await readHostingManifest(dir);
|
|
214
|
+
const egressAllowedDomains = [
|
|
215
|
+
...new Set([
|
|
216
|
+
...(hosting?.egressDomains ?? []),
|
|
217
|
+
...(options.allowDomains ?? []),
|
|
218
|
+
]),
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
// Today's only source shape: the control plane builds from a git ref (or
|
|
222
|
+
// serves the static placeholder). See DeploySource for the artifact seam.
|
|
223
|
+
const source: DeploySource = {
|
|
224
|
+
kind: "git-ref",
|
|
225
|
+
...(options.repo === undefined ? {} : { gitRepoUrl: options.repo }),
|
|
226
|
+
...(options.ref === undefined ? {} : { gitRef: options.ref }),
|
|
227
|
+
...(options.path === undefined ? {} : { agentPath: options.path }),
|
|
228
|
+
...(options.cursorEventsRepos === undefined ||
|
|
229
|
+
options.cursorEventsRepos.length === 0
|
|
230
|
+
? {}
|
|
231
|
+
: { cursorEventRepos: options.cursorEventsRepos }),
|
|
232
|
+
...(egressAllowedDomains.length === 0 ? {} : { egressAllowedDomains }),
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
let accepted: DeployAccepted;
|
|
236
|
+
try {
|
|
237
|
+
accepted = await ctx.client.deploy({ teamId: ctx.teamId, slug, source });
|
|
238
|
+
} catch (error) {
|
|
239
|
+
return renderApiError(error, ctx.err);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const aliasUrl = ctx.client.aliasUrl(accepted.engineAlias);
|
|
243
|
+
if (!ctx.json && accepted.aliasToken !== undefined) {
|
|
244
|
+
ctx.out(aliasTokenBlock(accepted.aliasToken));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (ctx.noWait) {
|
|
248
|
+
if (ctx.json) {
|
|
249
|
+
ctx.out(`${JSON.stringify(accepted, null, 2)}\n`);
|
|
250
|
+
} else {
|
|
251
|
+
ctx.out(
|
|
252
|
+
`Deploy accepted: ${slug} (team ${ctx.teamId}, generation ${accepted.generation}, ${accepted.deploymentKind})\n` +
|
|
253
|
+
` alias: ${aliasUrl}\n` +
|
|
254
|
+
`Check progress with \`${CLI} deployment ${slug} --team ${ctx.teamId}\`.\n`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let detail: DeploymentDetail;
|
|
261
|
+
try {
|
|
262
|
+
detail = await pollUntil(ctx, slug, TERMINAL_DEPLOY_STATUSES);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
return renderApiError(error, ctx.err);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
warnMissingSecrets(ctx, slug, hosting?.secretNames, detail.secretNames);
|
|
268
|
+
|
|
269
|
+
if (ctx.json) {
|
|
270
|
+
ctx.out(
|
|
271
|
+
`${JSON.stringify(
|
|
272
|
+
accepted.aliasToken === undefined
|
|
273
|
+
? detail
|
|
274
|
+
: { ...detail, aliasToken: accepted.aliasToken },
|
|
275
|
+
null,
|
|
276
|
+
2
|
|
277
|
+
)}\n`
|
|
278
|
+
);
|
|
279
|
+
return detail.status === "running" ? 0 : 1;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (detail.status !== "running") {
|
|
283
|
+
ctx.err(
|
|
284
|
+
`Deployment ${slug} ended ${detail.status}${
|
|
285
|
+
detail.lastError === null || detail.lastError === undefined
|
|
286
|
+
? ""
|
|
287
|
+
: `: ${detail.lastError}`
|
|
288
|
+
}\n`
|
|
289
|
+
);
|
|
290
|
+
return 1;
|
|
291
|
+
}
|
|
292
|
+
ctx.out(
|
|
293
|
+
`Deployed ${slug} (team ${ctx.teamId})\n` +
|
|
294
|
+
` status: running\n` +
|
|
295
|
+
` generation: ${detail.generation}\n` +
|
|
296
|
+
` kind: ${detail.deploymentKind}\n` +
|
|
297
|
+
` alias: ${aliasUrl}\n` +
|
|
298
|
+
` playground: ${aliasUrl}/playground (send X-Agent-Alias-Token)\n`
|
|
299
|
+
);
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Read the `hosting` block from the project manifest at `dir`, or
|
|
305
|
+
* undefined when the directory is not an agent project (static deploys
|
|
306
|
+
* have nothing to discover).
|
|
307
|
+
*/
|
|
308
|
+
async function readHostingManifest(
|
|
309
|
+
dir: string
|
|
310
|
+
): Promise<AgentHostingOptions | undefined> {
|
|
311
|
+
if (!(await isAgentProjectDir(dir))) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
const project = await loadAgentProject(dir);
|
|
315
|
+
return project.agent.hosting;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Friendly post-deploy nudge: the manifest declares secrets the agent
|
|
320
|
+
* expects, but the deployment does not have them all set yet.
|
|
321
|
+
*/
|
|
322
|
+
function warnMissingSecrets(
|
|
323
|
+
ctx: DeployCliContext,
|
|
324
|
+
slug: string,
|
|
325
|
+
declared: string[] | undefined,
|
|
326
|
+
set: string[] | undefined
|
|
327
|
+
): void {
|
|
328
|
+
if (declared === undefined || declared.length === 0) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const setNames = new Set(set ?? []);
|
|
332
|
+
const missing = declared.filter((name) => !setNames.has(name));
|
|
333
|
+
if (missing.length === 0) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
ctx.err(
|
|
337
|
+
`warning: secrets declared in the agent manifest but not set on ${slug}: ${missing.join(", ")}\n` +
|
|
338
|
+
` set them with \`${CLI} secrets set ${slug} ${missing.join(" ")}\`\n`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ============================================================================
|
|
343
|
+
// deployments (list)
|
|
344
|
+
// ============================================================================
|
|
345
|
+
|
|
346
|
+
export async function cmdDeployments(
|
|
347
|
+
options: DeployCliOptions
|
|
348
|
+
): Promise<number> {
|
|
349
|
+
const ctx = await prepare(options);
|
|
350
|
+
if (typeof ctx === "number") {
|
|
351
|
+
return ctx;
|
|
352
|
+
}
|
|
353
|
+
let deployments: Deployment[];
|
|
354
|
+
try {
|
|
355
|
+
deployments = await ctx.client.listDeployments({ teamId: ctx.teamId });
|
|
356
|
+
} catch (error) {
|
|
357
|
+
return renderApiError(error, ctx.err);
|
|
358
|
+
}
|
|
359
|
+
if (ctx.json) {
|
|
360
|
+
ctx.out(`${JSON.stringify({ deployments }, null, 2)}\n`);
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
if (deployments.length === 0) {
|
|
364
|
+
ctx.out(
|
|
365
|
+
`No deployments on team ${ctx.teamId}. Create one with \`${CLI} deploy\`.\n`
|
|
366
|
+
);
|
|
367
|
+
return 0;
|
|
368
|
+
}
|
|
369
|
+
ctx.out(
|
|
370
|
+
`${renderTable(
|
|
371
|
+
["SLUG", "STATUS", "GEN", "KIND", "UPDATED"],
|
|
372
|
+
deployments.map((d) => [
|
|
373
|
+
d.slug,
|
|
374
|
+
d.status,
|
|
375
|
+
String(d.generation),
|
|
376
|
+
d.deploymentKind,
|
|
377
|
+
d.desiredUpdatedAt ?? "",
|
|
378
|
+
])
|
|
379
|
+
)}\n`
|
|
380
|
+
);
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ============================================================================
|
|
385
|
+
// deployment <slug> (detail)
|
|
386
|
+
// ============================================================================
|
|
387
|
+
|
|
388
|
+
export async function cmdDeploymentStatus(
|
|
389
|
+
slug: string | undefined,
|
|
390
|
+
options: DeployCliOptions
|
|
391
|
+
): Promise<number> {
|
|
392
|
+
if (slug === undefined || slug === "") {
|
|
393
|
+
(options.err ?? ((t: string) => void process.stderr.write(t)))(
|
|
394
|
+
`Usage: ${CLI} deployment <slug> [--team <id>] [--json]\n`
|
|
395
|
+
);
|
|
396
|
+
return 1;
|
|
397
|
+
}
|
|
398
|
+
const ctx = await prepare(options);
|
|
399
|
+
if (typeof ctx === "number") {
|
|
400
|
+
return ctx;
|
|
401
|
+
}
|
|
402
|
+
let detail: DeploymentDetail;
|
|
403
|
+
try {
|
|
404
|
+
detail = await ctx.client.getDeployment({ teamId: ctx.teamId, slug });
|
|
405
|
+
} catch (error) {
|
|
406
|
+
return renderApiError(error, ctx.err);
|
|
407
|
+
}
|
|
408
|
+
if (ctx.json) {
|
|
409
|
+
ctx.out(`${JSON.stringify(detail, null, 2)}\n`);
|
|
410
|
+
return 0;
|
|
411
|
+
}
|
|
412
|
+
const aliasUrl = ctx.client.aliasUrl(detail.engineAlias);
|
|
413
|
+
const lines = [
|
|
414
|
+
`${detail.slug} (team ${detail.teamId})`,
|
|
415
|
+
` status: ${detail.status}`,
|
|
416
|
+
` generation: ${detail.generation}`,
|
|
417
|
+
` kind: ${detail.deploymentKind}`,
|
|
418
|
+
` alias: ${aliasUrl}`,
|
|
419
|
+
];
|
|
420
|
+
if (detail.gitRepoUrl !== null && detail.gitRepoUrl !== undefined) {
|
|
421
|
+
lines.push(` repo: ${detail.gitRepoUrl} @ ${detail.gitRef ?? ""}`);
|
|
422
|
+
}
|
|
423
|
+
if (
|
|
424
|
+
detail.serviceAccountId !== undefined &&
|
|
425
|
+
detail.serviceAccountId !== null
|
|
426
|
+
) {
|
|
427
|
+
lines.push(` service account: ${detail.serviceAccountId}`);
|
|
428
|
+
}
|
|
429
|
+
if (
|
|
430
|
+
detail.egressAllowedDomains !== undefined &&
|
|
431
|
+
detail.egressAllowedDomains.length > 0
|
|
432
|
+
) {
|
|
433
|
+
lines.push(` egress: ${detail.egressAllowedDomains.join(", ")}`);
|
|
434
|
+
}
|
|
435
|
+
if (detail.secretNames !== undefined && detail.secretNames.length > 0) {
|
|
436
|
+
lines.push(` secrets: ${detail.secretNames.join(", ")}`);
|
|
437
|
+
}
|
|
438
|
+
if (
|
|
439
|
+
detail.podCredentialMaskedKey !== undefined &&
|
|
440
|
+
detail.podCredentialMaskedKey !== null
|
|
441
|
+
) {
|
|
442
|
+
lines.push(` pod credential: ${detail.podCredentialMaskedKey}`);
|
|
443
|
+
}
|
|
444
|
+
if (detail.engineUrl !== null && detail.engineUrl !== undefined) {
|
|
445
|
+
lines.push(` engine: ${detail.engineUrl}`);
|
|
446
|
+
}
|
|
447
|
+
if (
|
|
448
|
+
detail.engineAccess !== null &&
|
|
449
|
+
detail.engineAccess !== undefined &&
|
|
450
|
+
typeof detail.engineAccess === "object"
|
|
451
|
+
) {
|
|
452
|
+
lines.push(
|
|
453
|
+
` engine access: ${detail.engineAccess.url} (expires ${detail.engineAccess.expiresAt})`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
if (detail.lastError !== null && detail.lastError !== undefined) {
|
|
457
|
+
lines.push(` last error: ${detail.lastError}`);
|
|
458
|
+
}
|
|
459
|
+
if (detail.desiredUpdatedAt !== undefined) {
|
|
460
|
+
lines.push(` updated: ${detail.desiredUpdatedAt}`);
|
|
461
|
+
}
|
|
462
|
+
ctx.out(`${lines.join("\n")}\n`);
|
|
463
|
+
return 0;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ============================================================================
|
|
467
|
+
// stop <slug>
|
|
468
|
+
// ============================================================================
|
|
469
|
+
|
|
470
|
+
export async function cmdStop(
|
|
471
|
+
slug: string | undefined,
|
|
472
|
+
options: DeployCliOptions
|
|
473
|
+
): Promise<number> {
|
|
474
|
+
if (slug === undefined || slug === "") {
|
|
475
|
+
(options.err ?? ((t: string) => void process.stderr.write(t)))(
|
|
476
|
+
`Usage: ${CLI} stop <slug> [--team <id>] [--json] [--no-wait]\n`
|
|
477
|
+
);
|
|
478
|
+
return 1;
|
|
479
|
+
}
|
|
480
|
+
const ctx = await prepare(options);
|
|
481
|
+
if (typeof ctx === "number") {
|
|
482
|
+
return ctx;
|
|
483
|
+
}
|
|
484
|
+
let accepted: StopAccepted;
|
|
485
|
+
try {
|
|
486
|
+
accepted = await ctx.client.stopDeployment({ teamId: ctx.teamId, slug });
|
|
487
|
+
} catch (error) {
|
|
488
|
+
return renderApiError(error, ctx.err);
|
|
489
|
+
}
|
|
490
|
+
if (ctx.noWait) {
|
|
491
|
+
if (ctx.json) {
|
|
492
|
+
ctx.out(`${JSON.stringify(accepted, null, 2)}\n`);
|
|
493
|
+
} else {
|
|
494
|
+
ctx.out(`Stop accepted for ${slug} (status: ${accepted.status}).\n`);
|
|
495
|
+
}
|
|
496
|
+
return 0;
|
|
497
|
+
}
|
|
498
|
+
let detail: DeploymentDetail;
|
|
499
|
+
try {
|
|
500
|
+
detail = await pollUntil(ctx, slug, new Set(["stopped"]));
|
|
501
|
+
} catch (error) {
|
|
502
|
+
return renderApiError(error, ctx.err);
|
|
503
|
+
}
|
|
504
|
+
if (ctx.json) {
|
|
505
|
+
ctx.out(`${JSON.stringify(detail, null, 2)}\n`);
|
|
506
|
+
} else {
|
|
507
|
+
ctx.out(`Stopped ${slug} (team ${ctx.teamId}).\n`);
|
|
508
|
+
}
|
|
509
|
+
return 0;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ============================================================================
|
|
513
|
+
// rotate-token <slug>
|
|
514
|
+
// ============================================================================
|
|
515
|
+
|
|
516
|
+
export async function cmdRotateToken(
|
|
517
|
+
slug: string | undefined,
|
|
518
|
+
options: DeployCliOptions
|
|
519
|
+
): Promise<number> {
|
|
520
|
+
if (slug === undefined || slug === "") {
|
|
521
|
+
(options.err ?? ((t: string) => void process.stderr.write(t)))(
|
|
522
|
+
`Usage: ${CLI} rotate-token <slug> [--team <id>]\n`
|
|
523
|
+
);
|
|
524
|
+
return 1;
|
|
525
|
+
}
|
|
526
|
+
const ctx = await prepare(options);
|
|
527
|
+
if (typeof ctx === "number") {
|
|
528
|
+
return ctx;
|
|
529
|
+
}
|
|
530
|
+
let aliasToken: string;
|
|
531
|
+
try {
|
|
532
|
+
aliasToken = await ctx.client.rotateAliasToken({
|
|
533
|
+
teamId: ctx.teamId,
|
|
534
|
+
slug,
|
|
535
|
+
});
|
|
536
|
+
} catch (error) {
|
|
537
|
+
return renderApiError(error, ctx.err);
|
|
538
|
+
}
|
|
539
|
+
if (ctx.json) {
|
|
540
|
+
ctx.out(`${JSON.stringify({ aliasToken }, null, 2)}\n`);
|
|
541
|
+
return 0;
|
|
542
|
+
}
|
|
543
|
+
ctx.out(
|
|
544
|
+
`Rotated the alias token for ${slug}. The old token stopped working immediately.\n${aliasTokenBlock(aliasToken)}`
|
|
545
|
+
);
|
|
546
|
+
return 0;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// ============================================================================
|
|
550
|
+
// rotate-pod-credential <slug>
|
|
551
|
+
// ============================================================================
|
|
552
|
+
|
|
553
|
+
export async function cmdRotatePodCredential(
|
|
554
|
+
slug: string | undefined,
|
|
555
|
+
options: DeployCliOptions
|
|
556
|
+
): Promise<number> {
|
|
557
|
+
if (slug === undefined || slug === "") {
|
|
558
|
+
(options.err ?? ((t: string) => void process.stderr.write(t)))(
|
|
559
|
+
`Usage: ${CLI} rotate-pod-credential <slug> [--team <id>]\n`
|
|
560
|
+
);
|
|
561
|
+
return 1;
|
|
562
|
+
}
|
|
563
|
+
const ctx = await prepare(options);
|
|
564
|
+
if (typeof ctx === "number") {
|
|
565
|
+
return ctx;
|
|
566
|
+
}
|
|
567
|
+
let podCredentialMaskedKey: string;
|
|
568
|
+
try {
|
|
569
|
+
podCredentialMaskedKey = await ctx.client.rotatePodCredential({
|
|
570
|
+
teamId: ctx.teamId,
|
|
571
|
+
slug,
|
|
572
|
+
});
|
|
573
|
+
} catch (error) {
|
|
574
|
+
return renderApiError(error, ctx.err);
|
|
575
|
+
}
|
|
576
|
+
if (ctx.json) {
|
|
577
|
+
ctx.out(`${JSON.stringify({ podCredentialMaskedKey }, null, 2)}\n`);
|
|
578
|
+
return 0;
|
|
579
|
+
}
|
|
580
|
+
ctx.out(
|
|
581
|
+
`Rotated the pod credential for ${slug}: ${podCredentialMaskedKey}\n` +
|
|
582
|
+
`The running pod keeps the old credential until the next deploy — run \`${CLI} deploy --slug ${slug}\` to apply it.\n`
|
|
583
|
+
);
|
|
584
|
+
return 0;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ============================================================================
|
|
588
|
+
// secrets set|list|unset
|
|
589
|
+
// ============================================================================
|
|
590
|
+
|
|
591
|
+
export async function cmdSecretsSet(
|
|
592
|
+
slug: string | undefined,
|
|
593
|
+
specs: string[],
|
|
594
|
+
options: DeployCliOptions
|
|
595
|
+
): Promise<number> {
|
|
596
|
+
const usageErr = options.err ?? ((t: string) => void process.stderr.write(t));
|
|
597
|
+
if (slug === undefined || slug === "" || specs.length === 0) {
|
|
598
|
+
usageErr(
|
|
599
|
+
`Usage: ${CLI} secrets set <slug> NAME[=VALUE] [NAME2[=VALUE2] ...] [--team <id>] [--json]\n` +
|
|
600
|
+
`NAMEs without =VALUE are prompted for (hidden input; reads stdin when piped).\n`
|
|
601
|
+
);
|
|
602
|
+
return 1;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Parse and validate every name before prompting for any value, so a
|
|
606
|
+
// typo in the last name cannot waste typed-in secrets.
|
|
607
|
+
const parsed: Array<{ name: string; value?: string }> = [];
|
|
608
|
+
for (const spec of specs) {
|
|
609
|
+
const eq = spec.indexOf("=");
|
|
610
|
+
const name = eq === -1 ? spec : spec.slice(0, eq);
|
|
611
|
+
const nameError = validateSettableSecretName(name);
|
|
612
|
+
if (nameError !== undefined) {
|
|
613
|
+
usageErr(`${nameError}\n`);
|
|
614
|
+
return 1;
|
|
615
|
+
}
|
|
616
|
+
parsed.push(eq === -1 ? { name } : { name, value: spec.slice(eq + 1) });
|
|
617
|
+
}
|
|
618
|
+
if (parsed.some((entry) => entry.value !== undefined)) {
|
|
619
|
+
usageErr(
|
|
620
|
+
`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`
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const ctx = await prepare(options);
|
|
625
|
+
if (typeof ctx === "number") {
|
|
626
|
+
return ctx;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const stdin = options.stdin ?? process.stdin;
|
|
630
|
+
const readValue = createSecretValueReader(stdin, ctx.err);
|
|
631
|
+
const secrets: Array<{ name: string; value: string }> = [];
|
|
632
|
+
for (const entry of parsed) {
|
|
633
|
+
secrets.push({
|
|
634
|
+
name: entry.name,
|
|
635
|
+
value: entry.value ?? (await readValue(entry.name)),
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
let secretNames: string[];
|
|
640
|
+
try {
|
|
641
|
+
secretNames = await ctx.client.setSecrets({
|
|
642
|
+
teamId: ctx.teamId,
|
|
643
|
+
slug,
|
|
644
|
+
secrets,
|
|
645
|
+
});
|
|
646
|
+
} catch (error) {
|
|
647
|
+
return renderApiError(error, ctx.err);
|
|
648
|
+
}
|
|
649
|
+
if (ctx.json) {
|
|
650
|
+
ctx.out(`${JSON.stringify({ secretNames }, null, 2)}\n`);
|
|
651
|
+
return 0;
|
|
652
|
+
}
|
|
653
|
+
ctx.out(
|
|
654
|
+
`Set ${secrets.length} secret${secrets.length === 1 ? "" : "s"} on ${slug}.\n` +
|
|
655
|
+
`Secrets now set: ${secretNames.join(", ")}\n` +
|
|
656
|
+
`The engine picks them up on its next deploy.\n`
|
|
657
|
+
);
|
|
658
|
+
return 0;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export async function cmdSecretsList(
|
|
662
|
+
slug: string | undefined,
|
|
663
|
+
options: DeployCliOptions
|
|
664
|
+
): Promise<number> {
|
|
665
|
+
if (slug === undefined || slug === "") {
|
|
666
|
+
(options.err ?? ((t: string) => void process.stderr.write(t)))(
|
|
667
|
+
`Usage: ${CLI} secrets list <slug> [--team <id>] [--json]\n`
|
|
668
|
+
);
|
|
669
|
+
return 1;
|
|
670
|
+
}
|
|
671
|
+
const ctx = await prepare(options);
|
|
672
|
+
if (typeof ctx === "number") {
|
|
673
|
+
return ctx;
|
|
674
|
+
}
|
|
675
|
+
let secrets: SecretInfo[];
|
|
676
|
+
try {
|
|
677
|
+
secrets = await ctx.client.listSecrets({ teamId: ctx.teamId, slug });
|
|
678
|
+
} catch (error) {
|
|
679
|
+
return renderApiError(error, ctx.err);
|
|
680
|
+
}
|
|
681
|
+
if (ctx.json) {
|
|
682
|
+
ctx.out(`${JSON.stringify({ secrets }, null, 2)}\n`);
|
|
683
|
+
return 0;
|
|
684
|
+
}
|
|
685
|
+
if (secrets.length === 0) {
|
|
686
|
+
ctx.out(
|
|
687
|
+
`No secrets set on ${slug}. Add one with \`${CLI} secrets set ${slug} NAME\`.\n`
|
|
688
|
+
);
|
|
689
|
+
return 0;
|
|
690
|
+
}
|
|
691
|
+
ctx.out(
|
|
692
|
+
`${renderTable(
|
|
693
|
+
["NAME", "CREATED"],
|
|
694
|
+
secrets.map((secret) => [secret.name, secret.createdAt])
|
|
695
|
+
)}\n`
|
|
696
|
+
);
|
|
697
|
+
return 0;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export async function cmdSecretsUnset(
|
|
701
|
+
slug: string | undefined,
|
|
702
|
+
name: string | undefined,
|
|
703
|
+
options: DeployCliOptions
|
|
704
|
+
): Promise<number> {
|
|
705
|
+
if (slug === undefined || slug === "" || name === undefined || name === "") {
|
|
706
|
+
(options.err ?? ((t: string) => void process.stderr.write(t)))(
|
|
707
|
+
`Usage: ${CLI} secrets unset <slug> NAME [--team <id>]\n`
|
|
708
|
+
);
|
|
709
|
+
return 1;
|
|
710
|
+
}
|
|
711
|
+
const ctx = await prepare(options);
|
|
712
|
+
if (typeof ctx === "number") {
|
|
713
|
+
return ctx;
|
|
714
|
+
}
|
|
715
|
+
try {
|
|
716
|
+
await ctx.client.deleteSecret({ teamId: ctx.teamId, slug, name });
|
|
717
|
+
} catch (error) {
|
|
718
|
+
if (error instanceof DeployApiError && error.status === 404) {
|
|
719
|
+
ctx.err(
|
|
720
|
+
`No secret named ${name} on deployment ${slug} — check \`${CLI} secrets list ${slug}\` (and the slug / --team).\n`
|
|
721
|
+
);
|
|
722
|
+
return 1;
|
|
723
|
+
}
|
|
724
|
+
return renderApiError(error, ctx.err);
|
|
725
|
+
}
|
|
726
|
+
if (ctx.json) {
|
|
727
|
+
ctx.out(`${JSON.stringify({ removed: name }, null, 2)}\n`);
|
|
728
|
+
} else {
|
|
729
|
+
ctx.out(`Removed secret ${name} from ${slug}.\n`);
|
|
730
|
+
}
|
|
731
|
+
return 0;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Returns a reader for interactive secret values: hidden (no-echo) prompt
|
|
736
|
+
* when `stdin` is a TTY, otherwise one line per secret from the piped
|
|
737
|
+
* stream. One reader per command so piped lines are consumed in order.
|
|
738
|
+
*/
|
|
739
|
+
function createSecretValueReader(
|
|
740
|
+
stdin: SecretInputStream,
|
|
741
|
+
err: (text: string) => void
|
|
742
|
+
): (name: string) => Promise<string> {
|
|
743
|
+
if (stdin.isTTY === true) {
|
|
744
|
+
return (name) => promptHidden(stdin, err, `Value for ${name}: `);
|
|
745
|
+
}
|
|
746
|
+
const readLine = createLineReader(stdin);
|
|
747
|
+
return async (name) => {
|
|
748
|
+
const line = await readLine();
|
|
749
|
+
if (line === undefined) {
|
|
750
|
+
throw new DeployApiError(
|
|
751
|
+
`stdin ended before a value for ${name} was provided (pipe one line per secret).`,
|
|
752
|
+
0
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
return line;
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/** Raw-mode hidden prompt: echoes nothing, supports backspace and Ctrl-C. */
|
|
760
|
+
function promptHidden(
|
|
761
|
+
stdin: SecretInputStream,
|
|
762
|
+
err: (text: string) => void,
|
|
763
|
+
prompt: string
|
|
764
|
+
): Promise<string> {
|
|
765
|
+
return new Promise((resolvePromise, reject) => {
|
|
766
|
+
err(prompt);
|
|
767
|
+
stdin.setRawMode?.(true);
|
|
768
|
+
stdin.resume();
|
|
769
|
+
let value = "";
|
|
770
|
+
const cleanup = (): void => {
|
|
771
|
+
stdin.off("data", onData);
|
|
772
|
+
stdin.setRawMode?.(false);
|
|
773
|
+
stdin.pause();
|
|
774
|
+
err("\n");
|
|
775
|
+
};
|
|
776
|
+
const onData = (chunk: Buffer | string): void => {
|
|
777
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
778
|
+
for (const ch of text) {
|
|
779
|
+
if (ch === "\r" || ch === "\n") {
|
|
780
|
+
cleanup();
|
|
781
|
+
resolvePromise(value);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (ch === "\u0003") {
|
|
785
|
+
cleanup();
|
|
786
|
+
reject(new DeployApiError("Aborted.", 0));
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (ch === "\u007f" || ch === "\b") {
|
|
790
|
+
value = value.slice(0, -1);
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
value += ch;
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
stdin.on("data", onData);
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** Buffered line reader over a piped stream; undefined once it ends. */
|
|
801
|
+
function createLineReader(
|
|
802
|
+
stream: NodeJS.ReadableStream
|
|
803
|
+
): () => Promise<string | undefined> {
|
|
804
|
+
let buffer = "";
|
|
805
|
+
let ended = false;
|
|
806
|
+
const waiters: Array<(line: string | undefined) => void> = [];
|
|
807
|
+
|
|
808
|
+
const drain = (): void => {
|
|
809
|
+
while (waiters.length > 0) {
|
|
810
|
+
const newline = buffer.indexOf("\n");
|
|
811
|
+
if (newline !== -1) {
|
|
812
|
+
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
813
|
+
buffer = buffer.slice(newline + 1);
|
|
814
|
+
waiters.shift()?.(line);
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (!ended) {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
// Stream ended: a trailing unterminated line still counts once.
|
|
821
|
+
const rest = buffer;
|
|
822
|
+
buffer = "";
|
|
823
|
+
waiters.shift()?.(rest === "" ? undefined : rest.replace(/\r$/, ""));
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
stream.on("data", (chunk: Buffer | string) => {
|
|
828
|
+
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
829
|
+
drain();
|
|
830
|
+
});
|
|
831
|
+
stream.on("end", () => {
|
|
832
|
+
ended = true;
|
|
833
|
+
drain();
|
|
834
|
+
});
|
|
835
|
+
stream.resume();
|
|
836
|
+
|
|
837
|
+
return () =>
|
|
838
|
+
new Promise((resolvePromise) => {
|
|
839
|
+
waiters.push(resolvePromise);
|
|
840
|
+
drain();
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// ============================================================================
|
|
845
|
+
// Polling + rendering helpers
|
|
846
|
+
// ============================================================================
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Poll deployment status every `pollIntervalMs` until it lands in
|
|
850
|
+
* `terminal` (single-line progress on a TTY, one line per transition
|
|
851
|
+
* otherwise). Throws DeployApiError on request failures or timeout.
|
|
852
|
+
*/
|
|
853
|
+
async function pollUntil(
|
|
854
|
+
ctx: DeployCliContext,
|
|
855
|
+
slug: string,
|
|
856
|
+
terminal: ReadonlySet<DeploymentStatus>
|
|
857
|
+
): Promise<DeploymentDetail> {
|
|
858
|
+
const startedAt = Date.now();
|
|
859
|
+
const tty = process.stderr.isTTY === true;
|
|
860
|
+
let lastStatus: DeploymentStatus | undefined;
|
|
861
|
+
let sawProgress = false;
|
|
862
|
+
|
|
863
|
+
const report = (status: DeploymentStatus): void => {
|
|
864
|
+
if (ctx.json) {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const elapsed = Math.round((Date.now() - startedAt) / 1000);
|
|
868
|
+
if (tty) {
|
|
869
|
+
ctx.err(`\r\x1b[2K${slug}: ${status} (${elapsed}s)`);
|
|
870
|
+
sawProgress = true;
|
|
871
|
+
} else if (status !== lastStatus) {
|
|
872
|
+
ctx.err(`${slug}: ${status}\n`);
|
|
873
|
+
}
|
|
874
|
+
lastStatus = status;
|
|
875
|
+
};
|
|
876
|
+
const finishProgressLine = (): void => {
|
|
877
|
+
if (tty && sawProgress) {
|
|
878
|
+
ctx.err("\n");
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
for (;;) {
|
|
883
|
+
const detail = await ctx.client
|
|
884
|
+
.getDeployment({ teamId: ctx.teamId, slug })
|
|
885
|
+
.catch((error: unknown) => {
|
|
886
|
+
finishProgressLine();
|
|
887
|
+
throw error;
|
|
888
|
+
});
|
|
889
|
+
report(detail.status);
|
|
890
|
+
if (terminal.has(detail.status)) {
|
|
891
|
+
finishProgressLine();
|
|
892
|
+
return detail;
|
|
893
|
+
}
|
|
894
|
+
if (Date.now() - startedAt >= ctx.timeoutMs) {
|
|
895
|
+
finishProgressLine();
|
|
896
|
+
throw new DeployApiError(
|
|
897
|
+
`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}\`.`,
|
|
898
|
+
0
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
await sleep(ctx.pollIntervalMs);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function aliasTokenBlock(aliasToken: string): string {
|
|
906
|
+
return (
|
|
907
|
+
`\n Alias token (shown ONCE — save it now, it cannot be retrieved again):\n\n` +
|
|
908
|
+
` ${aliasToken}\n\n` +
|
|
909
|
+
` Send it as the X-Agent-Alias-Token header on requests to the alias URL.\n` +
|
|
910
|
+
` Lost tokens: \`${CLI} rotate-token <slug>\` mints a replacement.\n\n`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function renderApiError(error: unknown, err: (text: string) => void): number {
|
|
915
|
+
if (error instanceof DeployApiError) {
|
|
916
|
+
err(`${error.message}\n`);
|
|
917
|
+
return 1;
|
|
918
|
+
}
|
|
919
|
+
throw error;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function renderTable(header: string[], rows: string[][]): string {
|
|
923
|
+
const widths = header.map((h, i) =>
|
|
924
|
+
Math.max(h.length, ...rows.map((row) => (row[i] ?? "").length))
|
|
925
|
+
);
|
|
926
|
+
const renderRow = (row: string[]): string =>
|
|
927
|
+
row
|
|
928
|
+
.map((cell, i) => cell.padEnd(widths[i] ?? 0))
|
|
929
|
+
.join(" ")
|
|
930
|
+
.trimEnd();
|
|
931
|
+
return [renderRow(header), ...rows.map(renderRow)].join("\n");
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function describeError(error: unknown): string {
|
|
935
|
+
return error instanceof Error ? error.message : String(error);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function sleep(ms: number): Promise<void> {
|
|
939
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
940
|
+
}
|