@cursor/july 0.1.15 → 0.1.17
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/README.md +9 -4
- package/dist/bin/agent-serve.js +60 -2
- package/dist/channels/slack/api.d.ts.map +1 -1
- package/dist/channels/slack/api.js +13 -3
- package/dist/channels/slack/auth.d.ts +7 -1
- package/dist/channels/slack/auth.d.ts.map +1 -1
- package/dist/channels/slack/auth.js +14 -0
- package/dist/channels/slack/blocks.d.ts +61 -0
- package/dist/channels/slack/blocks.d.ts.map +1 -0
- package/dist/channels/slack/blocks.js +90 -0
- package/dist/channels/slack/defaults.d.ts +2 -0
- package/dist/channels/slack/defaults.d.ts.map +1 -1
- package/dist/channels/slack/defaults.js +29 -3
- package/dist/channels/slack/dispatch.d.ts +3 -17
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/index.d.ts +1 -0
- package/dist/channels/slack/index.d.ts.map +1 -1
- package/dist/channels/slack/index.js +1 -0
- package/dist/channels/slack/interactive.d.ts +39 -3
- package/dist/channels/slack/interactive.d.ts.map +1 -1
- package/dist/channels/slack/interactive.js +152 -7
- package/dist/channels/slack/manifest.d.ts +3 -0
- package/dist/channels/slack/manifest.d.ts.map +1 -1
- package/dist/channels/slack/manifest.js +3 -0
- package/dist/channels/slack/setup.d.ts +4 -1
- package/dist/channels/slack/setup.d.ts.map +1 -1
- package/dist/channels/slack/setup.js +16 -2
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +24 -3
- package/dist/channels/slack/types.d.ts +77 -0
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/connections.d.ts +4 -1
- package/dist/connections.d.ts.map +1 -1
- package/dist/connections.js +4 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.DpdGZTLe.js → app.BbEXr2KO.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DCBsrfwP.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.IB-0QqIT.js → VPLocalSearchBox.DBEi4HDp.js} +1 -1
- package/dist/docs/assets/chunks/{theme.-fYBoNtR.js → theme.DGfptcHw.js} +2 -2
- package/dist/docs/assets/{example-agents_index.md.BPXjUtRB.js → example-agents_index.md.D2PEVSXl.js} +2 -2
- package/dist/docs/assets/{example-agents_index.md.BPXjUtRB.lean.js → example-agents_index.md.D2PEVSXl.lean.js} +1 -1
- package/dist/docs/assets/example-agents_security-reviewer.md.BZSNycMf.js +19 -0
- package/dist/docs/assets/{example-agents_security-reviewer.md.CE9v-aKm.lean.js → example-agents_security-reviewer.md.BZSNycMf.lean.js} +1 -1
- package/dist/docs/assets/{guides_mcp-oauth.md.DqmEOQY1.js → guides_mcp-oauth.md.Dd8EgSem.js} +3 -2
- package/dist/docs/assets/{guides_mcp-oauth.md.DqmEOQY1.lean.js → guides_mcp-oauth.md.Dd8EgSem.lean.js} +1 -1
- package/dist/docs/assets/{guides_slack.md.FWale6Ip.js → guides_slack.md.D_xFKlnF.js} +1 -1
- package/dist/docs/assets/{reference_connections.md.CsSr3rDz.js → reference_connections.md.B9Q3TOve.js} +12 -5
- package/dist/docs/assets/{reference_connections.md.CsSr3rDz.lean.js → reference_connections.md.B9Q3TOve.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +5 -5
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +14 -14
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +6 -5
- package/dist/docs/guides/slack.html +5 -5
- package/dist/docs/guides/webhooks.html +3 -3
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +3 -3
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/channels.html +3 -3
- package/dist/docs/reference/cli.html +3 -3
- package/dist/docs/reference/connections.html +14 -7
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +3 -3
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/evals/assertions.d.ts +190 -0
- package/dist/evals/assertions.d.ts.map +1 -0
- package/dist/evals/assertions.js +461 -0
- package/dist/evals/expect.d.ts +95 -0
- package/dist/evals/expect.d.ts.map +1 -0
- package/dist/evals/expect.js +194 -0
- package/dist/evals/judge.d.ts +133 -0
- package/dist/evals/judge.d.ts.map +1 -0
- package/dist/evals/judge.js +228 -0
- package/dist/evals/loaders.d.ts +51 -0
- package/dist/evals/loaders.d.ts.map +1 -0
- package/dist/evals/loaders.js +124 -0
- package/dist/evals/matchers.d.ts +95 -0
- package/dist/evals/matchers.d.ts.map +1 -0
- package/dist/evals/matchers.js +145 -0
- package/dist/evals/reporters.d.ts +70 -0
- package/dist/evals/reporters.d.ts.map +1 -0
- package/dist/evals/reporters.js +221 -0
- package/dist/evals/results.d.ts +103 -0
- package/dist/evals/results.d.ts.map +1 -0
- package/dist/evals/results.js +28 -0
- package/dist/evals/run-facts.d.ts +77 -0
- package/dist/evals/run-facts.d.ts.map +1 -0
- package/dist/evals/run-facts.js +187 -0
- package/dist/evals.d.ts +136 -31
- package/dist/evals.d.ts.map +1 -1
- package/dist/evals.js +76 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/cli-ax.d.ts +27 -0
- package/dist/internal/cli-ax.d.ts.map +1 -1
- package/dist/internal/cli-ax.js +90 -12
- package/dist/internal/cli-mcp.d.ts +8 -0
- package/dist/internal/cli-mcp.d.ts.map +1 -1
- package/dist/internal/cli-mcp.js +98 -0
- package/dist/internal/cli-slack.d.ts +29 -1
- package/dist/internal/cli-slack.d.ts.map +1 -1
- package/dist/internal/cli-slack.js +777 -12
- package/dist/internal/deploy-client.d.ts +7 -0
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +9 -0
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +29 -9
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +2 -0
- package/dist/internal/eval-judge-model.d.ts +59 -0
- package/dist/internal/eval-judge-model.d.ts.map +1 -0
- package/dist/internal/eval-judge-model.js +131 -0
- package/dist/internal/eval-run-store.d.ts.map +1 -1
- package/dist/internal/eval-run-store.js +12 -20
- package/dist/internal/eval-runner.d.ts +31 -25
- package/dist/internal/eval-runner.d.ts.map +1 -1
- package/dist/internal/eval-runner.js +332 -128
- package/dist/internal/evals-client.d.ts.map +1 -1
- package/dist/internal/evals-client.js +7 -3
- package/dist/internal/local-env.d.ts +8 -0
- package/dist/internal/local-env.d.ts.map +1 -0
- package/dist/internal/local-env.js +47 -0
- package/dist/internal/playground/static.d.ts +3 -0
- package/dist/internal/playground/static.d.ts.map +1 -1
- package/dist/internal/playground/static.js +3 -0
- package/dist/internal/run-client.d.ts +15 -0
- package/dist/internal/run-client.d.ts.map +1 -1
- package/dist/internal/run-client.js +3 -2
- package/dist/internal/sdk-runner.d.ts +36 -1
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +65 -25
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +10 -1
- package/dist/internal/session-engine.d.ts +18 -5
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +72 -19
- package/dist/internal/slack-provision-client.d.ts +130 -0
- package/dist/internal/slack-provision-client.d.ts.map +1 -0
- package/dist/internal/slack-provision-client.js +284 -0
- package/dist/playground/assets/cursor-icons-16-f_W_ogc-.woff2 +0 -0
- package/dist/playground/assets/index-CidizGZv.css +1 -0
- package/dist/playground/assets/index-DNqirsrK.js +85 -0
- package/dist/playground/index.html +2 -2
- package/dist/types.d.ts +70 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +26 -0
- package/docs/example-agents/index.md +1 -1
- package/docs/example-agents/security-reviewer.md +23 -21
- package/docs/guides/mcp-oauth.md +10 -5
- package/docs/guides/slack.md +3 -2
- package/docs/reference/connections.md +28 -18
- package/package.json +20 -2
- package/skills/create-agent/SKILL.md +15 -7
- package/skills/debug/SKILL.md +3 -1
- package/skills/mcp-auth/SKILL.md +1 -1
- package/skills/setup-slack/SKILL.md +173 -11
- package/src/bin/agent-serve.ts +63 -2
- package/src/channels/slack/api.ts +16 -3
- package/src/channels/slack/auth.ts +16 -1
- package/src/channels/slack/blocks.ts +139 -0
- package/src/channels/slack/defaults.ts +32 -4
- package/src/channels/slack/dispatch.ts +3 -18
- package/src/channels/slack/index.ts +1 -0
- package/src/channels/slack/interactive.ts +183 -19
- package/src/channels/slack/manifest.ts +3 -0
- package/src/channels/slack/setup.ts +16 -2
- package/src/channels/slack/slack-channel.ts +37 -6
- package/src/channels/slack/types.ts +89 -0
- package/src/connections.ts +4 -1
- package/src/evals/assertions.ts +704 -0
- package/src/evals/expect.ts +272 -0
- package/src/evals/judge.ts +346 -0
- package/src/evals/loaders.ts +113 -0
- package/src/evals/matchers.ts +246 -0
- package/src/evals/reporters.ts +274 -0
- package/src/evals/results.ts +132 -0
- package/src/evals/run-facts.ts +274 -0
- package/src/evals.ts +249 -42
- package/src/index.ts +2 -0
- package/src/internal/cli-ax.ts +134 -15
- package/src/internal/cli-mcp.ts +118 -5
- package/src/internal/cli-slack.ts +993 -15
- package/src/internal/deploy-client.ts +10 -0
- package/src/internal/discovery.ts +31 -5
- package/src/internal/distribution.ts +2 -0
- package/src/internal/eval-judge-model.ts +148 -0
- package/src/internal/eval-run-store.ts +10 -14
- package/src/internal/eval-runner.ts +466 -215
- package/src/internal/evals-client.ts +14 -2
- package/src/internal/local-env.ts +53 -0
- package/src/internal/playground/static.ts +3 -0
- package/src/internal/run-client.ts +17 -1
- package/src/internal/sdk-runner.ts +103 -22
- package/src/internal/server.ts +10 -0
- package/src/internal/session-engine.ts +89 -21
- package/src/internal/slack-provision-client.ts +438 -0
- package/src/types.ts +92 -4
- package/dist/channels/slack/post-update-delivery.d.ts +0 -85
- package/dist/channels/slack/post-update-delivery.d.ts.map +0 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.D2d8CfFS.js +0 -1
- package/dist/docs/assets/example-agents_security-reviewer.md.CE9v-aKm.js +0 -19
- package/dist/internal/json-dir-store.d.ts +0 -32
- package/dist/internal/json-dir-store.d.ts.map +0 -1
- package/dist/internal/persistence-coordinator.d.ts +0 -127
- package/dist/internal/persistence-coordinator.d.ts.map +0 -1
- package/dist/persistence.d.ts +0 -184
- package/dist/persistence.d.ts.map +0 -1
- package/dist/playground/assets/cursor-icons-16-CQ50JpfO.woff2 +0 -0
- package/dist/playground/assets/index-BVMOoiek.js +0 -86
- package/dist/playground/assets/index-DYmP4b--.css +0 -1
- /package/dist/docs/assets/{guides_slack.md.FWale6Ip.lean.js → guides_slack.md.D_xFKlnF.lean.js} +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `agent-serve slack …` CLI — manifests, setup, doctor (Socket Mode)
|
|
2
|
+
* `agent-serve slack …` CLI — manifests, setup, doctor (Socket Mode), and
|
|
3
|
+
* managed provisioning (`create` / `destroy`) through Cursor's
|
|
4
|
+
* "Add to Slack" API (`slack-provision-client.ts`).
|
|
3
5
|
*/
|
|
4
6
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
5
7
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
@@ -10,23 +12,56 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
12
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
11
13
|
});
|
|
12
14
|
};
|
|
13
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
14
|
-
import { basename, join, resolve as resolvePath } from "node:path";
|
|
15
|
-
import { buildSlackManifestPair, formatSlackSetupGuide, initSlackChannel, runSlackDoctor, } from "../channels/slack/index.js";
|
|
15
|
+
import { access, chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
16
|
+
import { basename, extname, join, resolve as resolvePath } from "node:path";
|
|
17
|
+
import { buildSlackManifest, buildSlackManifestPair, envPrefixFromName, formatSlackSetupGuide, initSlackChannel, runSlackDoctor, slackEnvKeys, } from "../channels/slack/index.js";
|
|
18
|
+
import { resolveDeployAuth } from "./cli-deploy.js";
|
|
19
|
+
import { cursorExternalApiUrl } from "./cursor/credentials.js";
|
|
20
|
+
import { slugFromDirectoryName } from "./deploy-client.js";
|
|
21
|
+
import { describeError } from "./describe-error.js";
|
|
22
|
+
import { CLI_COMMAND_NAME as CLI } from "./distribution.js";
|
|
23
|
+
import { loadAgentLocalEnv } from "./local-env.js";
|
|
24
|
+
import { openBrowser } from "./open-browser.js";
|
|
25
|
+
import { AgentServeSlackClient, SlackProvisionApiError, } from "./slack-provision-client.js";
|
|
16
26
|
const SLACK_HELP = `agent-serve slack — Slack channel pack helpers (Socket Mode)
|
|
17
27
|
|
|
18
28
|
Usage:
|
|
19
29
|
agent-serve slack setup
|
|
30
|
+
agent-serve slack create [--dir .] [--name NAME] [--slack-team T0123ABCD]
|
|
31
|
+
[--prod] [--team <id>] [--icon URL-or-FILE]
|
|
32
|
+
[--force] [--prefix PREFIX | --no-prefix] [--json]
|
|
33
|
+
agent-serve slack destroy [--dir .] [--slack-team T0123ABCD] [--prod]
|
|
34
|
+
[--team <id>] [--json]
|
|
35
|
+
agent-serve slack icon <URL-or-FILE> [--dir .] [--slack-team T0123ABCD]
|
|
36
|
+
[--prod] [--team <id>] [--json]
|
|
20
37
|
agent-serve slack init [--dir .] [--name NAME] [--prefix PREFIX | --no-prefix]
|
|
21
38
|
[--channel-posts]
|
|
22
39
|
agent-serve slack manifest --env dev|prod|both [--dir .] [--name NAME]
|
|
23
40
|
[--print]
|
|
24
|
-
agent-serve slack doctor [--prefix PREFIX] [--json]
|
|
41
|
+
agent-serve slack doctor [--dir .] [--prefix PREFIX | --no-prefix] [--json]
|
|
42
|
+
|
|
43
|
+
create/destroy/icon — Cursor "Add to Slack" managed provisioning:
|
|
44
|
+
create One-command Path B setup: connects the Slack workspace (browser
|
|
45
|
+
consent) when needed, creates + installs this agent's dedicated
|
|
46
|
+
Socket Mode app from the generated manifest, writes the token
|
|
47
|
+
pair into <dir>/.env.local, then runs doctor (prose mode; --json
|
|
48
|
+
reports without the doctor pass). Requires
|
|
49
|
+
\`agent-serve login\` (or CURSOR_API_KEY) and the feature gate.
|
|
50
|
+
--prod provisions the prod app (default dev); --slack-team picks
|
|
51
|
+
the workspace when several are connected. --icon sets the app's
|
|
52
|
+
icon (best-effort: a bad icon warns, never fails the create).
|
|
53
|
+
A re-run refuses to overwrite manifest edits made by hand at
|
|
54
|
+
api.slack.com (manifest_drift); --force overwrites them.
|
|
55
|
+
destroy Delete the provisioned Slack app for this agent (+env). Tokens
|
|
56
|
+
already in .env.local are left in place (they just stop working).
|
|
57
|
+
icon Set the provisioned app's icon: an https image URL or a local
|
|
58
|
+
png/jpg/gif file (at most 512KB).
|
|
25
59
|
|
|
26
60
|
Env tokens:
|
|
27
61
|
single agent: SLACK_BOT_TOKEN + SLACK_APP_TOKEN
|
|
28
62
|
multi-agent: <PREFIX>_SLACK_BOT_TOKEN + <PREFIX>_SLACK_APP_TOKEN
|
|
29
|
-
(init
|
|
63
|
+
(init/create default PREFIX from the directory name;
|
|
64
|
+
doctor defaults to it too when those keys are set)
|
|
30
65
|
|
|
31
66
|
See also: skills/setup-slack/SKILL.md
|
|
32
67
|
`;
|
|
@@ -42,6 +77,12 @@ export function cmdSlack(subcommand, options) {
|
|
|
42
77
|
case "setup":
|
|
43
78
|
process.stdout.write(formatSlackSetupGuide());
|
|
44
79
|
return 0;
|
|
80
|
+
case "create":
|
|
81
|
+
return cmdSlackCreate(options);
|
|
82
|
+
case "destroy":
|
|
83
|
+
return cmdSlackDestroy(options);
|
|
84
|
+
case "icon":
|
|
85
|
+
return cmdSlackIcon(options);
|
|
45
86
|
case "init":
|
|
46
87
|
return cmdSlackInit(options);
|
|
47
88
|
case "manifest":
|
|
@@ -128,17 +169,741 @@ function runManifest(options) {
|
|
|
128
169
|
}
|
|
129
170
|
function runDoctor(options) {
|
|
130
171
|
return __awaiter(this, void 0, void 0, function* () {
|
|
131
|
-
|
|
172
|
+
var _a;
|
|
173
|
+
const dir = resolvePath(options.dir);
|
|
174
|
+
// Tokens usually live in the agent's .env.local; real env still wins.
|
|
175
|
+
loadAgentLocalEnv(dir);
|
|
176
|
+
const envPrefix = resolveDoctorEnvPrefix(options, dir);
|
|
177
|
+
const doctor = (_a = options.doctorImpl) !== null && _a !== void 0 ? _a : runSlackDoctor;
|
|
178
|
+
const result = yield doctor(envPrefix === undefined ? {} : { envPrefix });
|
|
132
179
|
if (options.json) {
|
|
133
180
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
134
181
|
}
|
|
135
182
|
else {
|
|
136
|
-
process.stdout.write(
|
|
137
|
-
for (const check of result.checks) {
|
|
138
|
-
process.stdout.write(`${check.ok ? "ok" : "!!"} ${check.id}: ${check.message}\n`);
|
|
139
|
-
}
|
|
140
|
-
process.stdout.write(result.ok ? "doctor: pass\n" : "doctor: fail\n");
|
|
183
|
+
process.stdout.write(renderDoctorReport(result));
|
|
141
184
|
}
|
|
142
185
|
return result.ok ? 0 : 1;
|
|
143
186
|
});
|
|
144
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* --prefix / --no-prefix win; otherwise the directory-derived prefix
|
|
190
|
+
* create/init use, but only when one of its keys is set — unprefixed
|
|
191
|
+
* setups keep the bare SLACK_* default.
|
|
192
|
+
*/
|
|
193
|
+
function resolveDoctorEnvPrefix(options, dir) {
|
|
194
|
+
if (options.noPrefix === true) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
if (options.prefix !== undefined) {
|
|
198
|
+
return options.prefix;
|
|
199
|
+
}
|
|
200
|
+
const derived = envPrefixFromName(basename(dir));
|
|
201
|
+
const keys = slackEnvKeys(derived);
|
|
202
|
+
return process.env[keys.bot] !== undefined ||
|
|
203
|
+
process.env[keys.app] !== undefined
|
|
204
|
+
? derived
|
|
205
|
+
: undefined;
|
|
206
|
+
}
|
|
207
|
+
// ============================================================================
|
|
208
|
+
// create / destroy — Cursor "Add to Slack" managed provisioning
|
|
209
|
+
// ============================================================================
|
|
210
|
+
const DEFAULT_CONNECT_POLL_INTERVAL_MS = 3000;
|
|
211
|
+
const DEFAULT_CONNECT_POLL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
212
|
+
function resolveSlackAppTarget(options, err) {
|
|
213
|
+
var _a, _b;
|
|
214
|
+
if (options.env === "both" && options.prod !== true) {
|
|
215
|
+
err("Provisioning targets one env at a time; pass --prod for the prod app (default dev).\n");
|
|
216
|
+
return 1;
|
|
217
|
+
}
|
|
218
|
+
const env = options.prod === true || options.env === "prod" ? "prod" : "dev";
|
|
219
|
+
const dir = resolvePath(options.dir);
|
|
220
|
+
// Backend slug rule, mirrored so the error names the real problem
|
|
221
|
+
// instead of a generic 400.
|
|
222
|
+
const slug = slugFromDirectoryName(basename(dir));
|
|
223
|
+
if (slug === undefined || !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(slug)) {
|
|
224
|
+
err(`Could not derive a valid app slug from the directory name ${JSON.stringify(basename(dir))} (slugs start with a letter or digit; lowercase letters, digits, '-', '_'); rename the directory or pass --dir <agent>.\n`);
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
const name = (_a = options.name) !== null && _a !== void 0 ? _a : basename(dir);
|
|
228
|
+
const envPrefix = options.noPrefix === true
|
|
229
|
+
? undefined
|
|
230
|
+
: ((_b = options.prefix) !== null && _b !== void 0 ? _b : envPrefixFromName(basename(dir)));
|
|
231
|
+
return { dir, name, slug, env, envPrefix, envKeys: slackEnvKeys(envPrefix) };
|
|
232
|
+
}
|
|
233
|
+
function errStream(options) {
|
|
234
|
+
var _a;
|
|
235
|
+
return (_a = options.err) !== null && _a !== void 0 ? _a : ((text) => void process.stderr.write(text));
|
|
236
|
+
}
|
|
237
|
+
const SLACK_ICON_MAX_BYTES = 524288;
|
|
238
|
+
const SLACK_ICON_URL_MAX_LENGTH = 3000;
|
|
239
|
+
const SLACK_ICON_CONTENT_TYPES = {
|
|
240
|
+
".png": "image/png",
|
|
241
|
+
".jpg": "image/jpeg",
|
|
242
|
+
".jpeg": "image/jpeg",
|
|
243
|
+
".gif": "image/gif",
|
|
244
|
+
};
|
|
245
|
+
function resolveSlackIconSource(raw, err) {
|
|
246
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
247
|
+
if (raw.startsWith("https://")) {
|
|
248
|
+
if (raw.length > SLACK_ICON_URL_MAX_LENGTH) {
|
|
249
|
+
err(`Icon URLs must be at most ${SLACK_ICON_URL_MAX_LENGTH} characters (got ${raw.length}).\n`);
|
|
250
|
+
return 1;
|
|
251
|
+
}
|
|
252
|
+
return { kind: "url", url: raw };
|
|
253
|
+
}
|
|
254
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) {
|
|
255
|
+
err(`Icon URLs must be https (got ${JSON.stringify(raw)}).\n`);
|
|
256
|
+
return 1;
|
|
257
|
+
}
|
|
258
|
+
const path = resolvePath(raw);
|
|
259
|
+
const contentType = SLACK_ICON_CONTENT_TYPES[extname(path).toLowerCase()];
|
|
260
|
+
if (contentType === undefined) {
|
|
261
|
+
err(`Icon files must be .png, .jpg/.jpeg, or .gif (got ${JSON.stringify(basename(path))}).\n`);
|
|
262
|
+
return 1;
|
|
263
|
+
}
|
|
264
|
+
let data;
|
|
265
|
+
try {
|
|
266
|
+
data = yield readFile(path);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
err(`Could not read the icon file ${path}: ${describeError(error)}\n`);
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
if (data.length > SLACK_ICON_MAX_BYTES) {
|
|
273
|
+
err(`Icon files must be at most ${SLACK_ICON_MAX_BYTES} bytes (${path} is ${data.length}).\n`);
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
return { kind: "file", base64: data.toString("base64"), contentType, path };
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function toSlackAppIconInput(source) {
|
|
280
|
+
return source.kind === "url"
|
|
281
|
+
? { url: source.url }
|
|
282
|
+
: { base64: source.base64, contentType: source.contentType };
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* URL icons ride the provision request; file icons post to the icon
|
|
286
|
+
* endpoint afterwards. Best-effort: failures warn, never fail the create.
|
|
287
|
+
*/
|
|
288
|
+
function applySlackCreateIcon(ctx, source, result) {
|
|
289
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
290
|
+
if (source === undefined) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
let failure;
|
|
294
|
+
if (source.kind === "url") {
|
|
295
|
+
if (result.iconStatus !== "set") {
|
|
296
|
+
failure = "Slack did not accept the icon URL";
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
try {
|
|
301
|
+
yield ctx.client.setAppIcon({
|
|
302
|
+
teamId: ctx.teamId,
|
|
303
|
+
slackAppId: result.appId,
|
|
304
|
+
icon: toSlackAppIconInput(source),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
failure = describeError(error);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (failure === undefined) {
|
|
312
|
+
return "set";
|
|
313
|
+
}
|
|
314
|
+
const retryWith = source.kind === "url" ? source.url : source.path;
|
|
315
|
+
ctx.err(`warning: setting the Slack app icon failed: ${failure}\nRetry with \`${CLI} slack icon ${retryWith}\`.\n`);
|
|
316
|
+
return "failed";
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Credentials → provisioning client → team id, sharing the deploy CLI's
|
|
321
|
+
* resolution (stored login / CURSOR_API_KEY, `--team` override, GetMe
|
|
322
|
+
* default-team fallback). Returns an exit code (error already rendered)
|
|
323
|
+
* on failure.
|
|
324
|
+
*/
|
|
325
|
+
function prepareProvision(options) {
|
|
326
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
327
|
+
var _a, _b, _c, _d;
|
|
328
|
+
const out = (_a = options.out) !== null && _a !== void 0 ? _a : ((text) => void process.stdout.write(text));
|
|
329
|
+
const err = errStream(options);
|
|
330
|
+
const auth = yield resolveDeployAuth({
|
|
331
|
+
team: options.team,
|
|
332
|
+
apiKey: options.apiKey,
|
|
333
|
+
fetchImpl: options.fetchImpl,
|
|
334
|
+
err,
|
|
335
|
+
});
|
|
336
|
+
if (typeof auth === "number") {
|
|
337
|
+
return auth;
|
|
338
|
+
}
|
|
339
|
+
// Provisioning routes live on the external API host beside deploys.
|
|
340
|
+
return {
|
|
341
|
+
client: new AgentServeSlackClient({
|
|
342
|
+
backendUrl: cursorExternalApiUrl(),
|
|
343
|
+
apiKey: auth.apiKey,
|
|
344
|
+
fetchImpl: options.fetchImpl,
|
|
345
|
+
}),
|
|
346
|
+
teamId: auth.teamId,
|
|
347
|
+
json: options.json === true,
|
|
348
|
+
out,
|
|
349
|
+
err,
|
|
350
|
+
openBrowserImpl: (_b = options.openBrowserImpl) !== null && _b !== void 0 ? _b : openBrowser,
|
|
351
|
+
pollIntervalMs: (_c = options.connectPollIntervalMs) !== null && _c !== void 0 ? _c : DEFAULT_CONNECT_POLL_INTERVAL_MS,
|
|
352
|
+
pollTimeoutMs: (_d = options.connectPollTimeoutMs) !== null && _d !== void 0 ? _d : DEFAULT_CONNECT_POLL_TIMEOUT_MS,
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function matchConnectedWorkspace(workspaces, wait) {
|
|
357
|
+
const candidates = wait.slackTeamId === undefined
|
|
358
|
+
? workspaces
|
|
359
|
+
: workspaces.filter((workspace) => workspace.slackTeamId === wait.slackTeamId);
|
|
360
|
+
if (wait.notConnectedAt === undefined) {
|
|
361
|
+
return candidates[0];
|
|
362
|
+
}
|
|
363
|
+
return candidates.find((workspace) => workspace.connectedAt !== wait.notConnectedAt);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Mint the browser consent URL, open it, and poll `GET /status` until the
|
|
367
|
+
* awaited workspace shows up (or the poll times out). Returns the
|
|
368
|
+
* connected workspace, or an exit code with the error already rendered.
|
|
369
|
+
*/
|
|
370
|
+
function runConnectFlow(ctx, wait) {
|
|
371
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
372
|
+
let url;
|
|
373
|
+
try {
|
|
374
|
+
url = yield ctx.client.connect(ctx.teamId);
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
return renderSlackApiError(error, ctx.err);
|
|
378
|
+
}
|
|
379
|
+
ctx.err(`\nConnect your Slack workspace to Cursor — approve in the browser:\n\n ${url}\n\n(waiting for the Slack consent to complete, up to ${Math.round(ctx.pollTimeoutMs / 60000)} minutes)\n`);
|
|
380
|
+
ctx.openBrowserImpl(url);
|
|
381
|
+
const deadline = Date.now() + ctx.pollTimeoutMs;
|
|
382
|
+
let lastPollFailure;
|
|
383
|
+
for (;;) {
|
|
384
|
+
let workspace;
|
|
385
|
+
let sleepMs = ctx.pollIntervalMs;
|
|
386
|
+
try {
|
|
387
|
+
const status = yield ctx.client.status(ctx.teamId);
|
|
388
|
+
workspace = matchConnectedWorkspace(status.workspaces, wait);
|
|
389
|
+
lastPollFailure = undefined;
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
// Transient poll failures must not abort a minted consent flow; only
|
|
393
|
+
// a still-failing poll at the deadline surfaces the error.
|
|
394
|
+
if (Date.now() >= deadline) {
|
|
395
|
+
return renderSlackApiError(error, ctx.err);
|
|
396
|
+
}
|
|
397
|
+
const failure = describeError(error);
|
|
398
|
+
if (failure !== lastPollFailure) {
|
|
399
|
+
lastPollFailure = failure;
|
|
400
|
+
ctx.err(`(status poll failed, retrying: ${failure})\n`);
|
|
401
|
+
}
|
|
402
|
+
if (error instanceof SlackProvisionApiError &&
|
|
403
|
+
error.retryAfterSeconds !== undefined) {
|
|
404
|
+
// Respect backend backpressure instead of hammering at the poll rate.
|
|
405
|
+
sleepMs = Math.max(sleepMs, error.retryAfterSeconds * 1000);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (workspace !== undefined) {
|
|
409
|
+
ctx.err(`Connected Slack workspace ${workspace.slackTeamId}.\n`);
|
|
410
|
+
return workspace;
|
|
411
|
+
}
|
|
412
|
+
if (Date.now() >= deadline) {
|
|
413
|
+
ctx.err(`Timed out waiting for the Slack workspace connection. The consent link stays valid for ~10 minutes — approve it and re-run \`${CLI} slack create\`.\n`);
|
|
414
|
+
return 1;
|
|
415
|
+
}
|
|
416
|
+
yield sleep(sleepMs);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
export function cmdSlackCreate(options) {
|
|
421
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
422
|
+
var _a, _b;
|
|
423
|
+
const target = resolveSlackAppTarget(options, errStream(options));
|
|
424
|
+
if (typeof target === "number") {
|
|
425
|
+
return target;
|
|
426
|
+
}
|
|
427
|
+
// Resolved before any network I/O so a bad path/type/size fails fast.
|
|
428
|
+
let iconSource;
|
|
429
|
+
if (options.icon !== undefined) {
|
|
430
|
+
const resolved = yield resolveSlackIconSource(options.icon, errStream(options));
|
|
431
|
+
if (typeof resolved === "number") {
|
|
432
|
+
return resolved;
|
|
433
|
+
}
|
|
434
|
+
iconSource = resolved;
|
|
435
|
+
}
|
|
436
|
+
const ctx = yield prepareProvision(options);
|
|
437
|
+
if (typeof ctx === "number") {
|
|
438
|
+
return ctx;
|
|
439
|
+
}
|
|
440
|
+
// Workspace: --slack-team wins; one → use it; none → connect; several → ask.
|
|
441
|
+
let status;
|
|
442
|
+
try {
|
|
443
|
+
status = yield ctx.client.status(ctx.teamId);
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
return renderSlackApiError(error, ctx.err);
|
|
447
|
+
}
|
|
448
|
+
let workspace;
|
|
449
|
+
if (options.slackTeam !== undefined) {
|
|
450
|
+
workspace = status.workspaces.find((candidate) => candidate.slackTeamId === options.slackTeam);
|
|
451
|
+
}
|
|
452
|
+
else if (status.workspaces.length === 1) {
|
|
453
|
+
workspace = status.workspaces[0];
|
|
454
|
+
}
|
|
455
|
+
else if (status.workspaces.length > 1) {
|
|
456
|
+
ctx.err(`Multiple Slack workspaces are connected:\n` +
|
|
457
|
+
status.workspaces
|
|
458
|
+
.map((candidate) => ` - ${candidate.slackTeamId} (Slack user ${candidate.slackUserId}, connected ${candidate.connectedAt})\n`)
|
|
459
|
+
.join("") +
|
|
460
|
+
`Pass --slack-team <id> to pick one.\n`);
|
|
461
|
+
return 1;
|
|
462
|
+
}
|
|
463
|
+
if (workspace === undefined) {
|
|
464
|
+
const connected = yield runConnectFlow(ctx, {
|
|
465
|
+
slackTeamId: options.slackTeam,
|
|
466
|
+
});
|
|
467
|
+
if (typeof connected === "number") {
|
|
468
|
+
return connected;
|
|
469
|
+
}
|
|
470
|
+
workspace = connected;
|
|
471
|
+
}
|
|
472
|
+
let wrote;
|
|
473
|
+
try {
|
|
474
|
+
wrote = yield scaffoldSlackChannelFiles(options, target);
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
ctx.err(`${describeError(error)}\n`);
|
|
478
|
+
return 1;
|
|
479
|
+
}
|
|
480
|
+
if (wrote.length > 0 && !ctx.json) {
|
|
481
|
+
ctx.err(`Scaffolded the Slack channel: ${wrote.join(", ")}\n`);
|
|
482
|
+
}
|
|
483
|
+
// A re-run rebuilds this manifest from flags, so backend-side
|
|
484
|
+
// customizations reset to it (documented in the skill).
|
|
485
|
+
const manifest = buildSlackManifest({
|
|
486
|
+
name: target.name,
|
|
487
|
+
env: target.env,
|
|
488
|
+
channelPosts: options.channelPosts === true,
|
|
489
|
+
});
|
|
490
|
+
const manifestJson = JSON.stringify(manifest);
|
|
491
|
+
// One connect-and-retry for not_connected / insufficient_scopes (both
|
|
492
|
+
// are fixed by a fresh consent grant).
|
|
493
|
+
let result;
|
|
494
|
+
let connectRetried = false;
|
|
495
|
+
for (;;) {
|
|
496
|
+
try {
|
|
497
|
+
result = yield ctx.client.provisionApp({
|
|
498
|
+
teamId: ctx.teamId,
|
|
499
|
+
slackTeamId: workspace.slackTeamId,
|
|
500
|
+
slug: target.slug,
|
|
501
|
+
env: target.env,
|
|
502
|
+
manifest: manifestJson,
|
|
503
|
+
icon: (iconSource === null || iconSource === void 0 ? void 0 : iconSource.kind) === "url" ? { url: iconSource.url } : undefined,
|
|
504
|
+
force: options.force === true ? true : undefined,
|
|
505
|
+
});
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
if (error instanceof SlackProvisionApiError &&
|
|
510
|
+
(error.code === "not_connected" ||
|
|
511
|
+
error.code === "insufficient_scopes") &&
|
|
512
|
+
!connectRetried) {
|
|
513
|
+
connectRetried = true;
|
|
514
|
+
// Both codes arrive holding a row that GET /status already lists, so
|
|
515
|
+
// the poll waits for a fresh grant instead of re-matching that row.
|
|
516
|
+
const wait = {
|
|
517
|
+
slackTeamId: workspace.slackTeamId,
|
|
518
|
+
notConnectedAt: workspace.connectedAt,
|
|
519
|
+
};
|
|
520
|
+
if (error.code === "insufficient_scopes") {
|
|
521
|
+
ctx.err(`The Slack connection is missing scopes provisioning needs; approving the consent link again grants them.\n`);
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
ctx.err(`The Slack workspace is not connected to Cursor; starting the connect flow.\n`);
|
|
525
|
+
}
|
|
526
|
+
const connected = yield runConnectFlow(ctx, wait);
|
|
527
|
+
if (typeof connected === "number") {
|
|
528
|
+
return connected;
|
|
529
|
+
}
|
|
530
|
+
workspace = connected;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
return renderCreateProvisionError(error, ctx.err);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const iconStatus = yield applySlackCreateIcon(ctx, iconSource, result);
|
|
537
|
+
if (result.status === "pending_admin_approval") {
|
|
538
|
+
if (ctx.json) {
|
|
539
|
+
ctx.out(`${JSON.stringify({
|
|
540
|
+
ok: false,
|
|
541
|
+
appId: result.appId,
|
|
542
|
+
status: result.status,
|
|
543
|
+
slackTeamId: workspace.slackTeamId,
|
|
544
|
+
envKeys: target.envKeys,
|
|
545
|
+
wrote,
|
|
546
|
+
oauthAuthorizeUrl: (_a = result.oauthAuthorizeUrl) !== null && _a !== void 0 ? _a : null,
|
|
547
|
+
iconStatus,
|
|
548
|
+
}, null, 2)}\n`);
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
ctx.err(`The Slack app was created (app ${result.appId}), but installing it needs a Slack workspace-admin approval.\n` +
|
|
552
|
+
(result.oauthAuthorizeUrl === undefined
|
|
553
|
+
? ""
|
|
554
|
+
: `\n Approve it (or send this to a workspace admin):\n\n ${result.oauthAuthorizeUrl}\n\n`) +
|
|
555
|
+
`Once an admin approves the app, re-run \`${CLI} slack create\` to finish the install and mint the tokens.\n`);
|
|
556
|
+
}
|
|
557
|
+
return 1;
|
|
558
|
+
}
|
|
559
|
+
// Tokens come back exactly once and are never printed.
|
|
560
|
+
const envEntries = [{ key: target.envKeys.bot, value: result.botToken }];
|
|
561
|
+
if (result.appToken !== undefined) {
|
|
562
|
+
envEntries.push({ key: target.envKeys.app, value: result.appToken });
|
|
563
|
+
}
|
|
564
|
+
const envFile = join(target.dir, ".env.local");
|
|
565
|
+
try {
|
|
566
|
+
yield upsertEnvFile(envFile, envEntries);
|
|
567
|
+
if (yield ensureEnvLocalGitignored(target.dir)) {
|
|
568
|
+
wrote.push(".gitignore (+.env.local)");
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
ctx.err(`Could not write ${envFile}: ${describeError(error)}\n`);
|
|
573
|
+
return 1;
|
|
574
|
+
}
|
|
575
|
+
wrote.push(".env.local");
|
|
576
|
+
for (const { key, value } of envEntries) {
|
|
577
|
+
process.env[key] = value;
|
|
578
|
+
}
|
|
579
|
+
if (result.appToken === undefined) {
|
|
580
|
+
ctx.err(`warning: Slack did not return an app-level token, so ${target.envKeys.app} was not written (an existing value was left as-is and may belong to an older app); re-run \`${CLI} slack create\` to recover it.\n`);
|
|
581
|
+
}
|
|
582
|
+
if (ctx.json) {
|
|
583
|
+
ctx.out(`${JSON.stringify({
|
|
584
|
+
ok: true,
|
|
585
|
+
appId: result.appId,
|
|
586
|
+
status: result.status,
|
|
587
|
+
slackTeamId: workspace.slackTeamId,
|
|
588
|
+
envKeys: target.envKeys,
|
|
589
|
+
wrote,
|
|
590
|
+
iconStatus,
|
|
591
|
+
}, null, 2)}\n`);
|
|
592
|
+
return 0;
|
|
593
|
+
}
|
|
594
|
+
ctx.out(`Provisioned the ${target.env} Slack app for ${target.slug} (app ${result.appId}) in workspace ${workspace.slackTeamId}.\n` +
|
|
595
|
+
` wrote ${envEntries.map((entry) => entry.key).join(" + ")} to ${envFile}\n\n`);
|
|
596
|
+
const doctor = (_b = options.doctorImpl) !== null && _b !== void 0 ? _b : runSlackDoctor;
|
|
597
|
+
const doctorResult = yield doctor(target.envPrefix === undefined ? {} : { envPrefix: target.envPrefix });
|
|
598
|
+
ctx.out(renderDoctorReport(doctorResult));
|
|
599
|
+
ctx.out(`\nNext steps:\n` +
|
|
600
|
+
` 1. ${CLI} dev ${options.dir}\n` +
|
|
601
|
+
` 2. In Slack, invite @${manifest.features.bot_user.display_name} to a channel and @mention it (or DM it).\n`);
|
|
602
|
+
return 0;
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Resolve the one provisioned app for (slug, env[, --slack-team]) from
|
|
607
|
+
* `GET /status`. Returns an exit code with the error already rendered
|
|
608
|
+
* when zero or several apps match.
|
|
609
|
+
*/
|
|
610
|
+
function resolveProvisionedApp(ctx, target, options) {
|
|
611
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
612
|
+
let status;
|
|
613
|
+
try {
|
|
614
|
+
status = yield ctx.client.status(ctx.teamId);
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
return renderSlackApiError(error, ctx.err);
|
|
618
|
+
}
|
|
619
|
+
const sameSlug = status.apps.filter((app) => app.slug === target.slug);
|
|
620
|
+
let candidates = sameSlug.filter((app) => app.env === target.env);
|
|
621
|
+
if (options.slackTeam !== undefined) {
|
|
622
|
+
candidates = candidates.filter((app) => app.slackTeamId === options.slackTeam);
|
|
623
|
+
}
|
|
624
|
+
if (candidates.length === 0) {
|
|
625
|
+
ctx.err(`No provisioned ${target.env} Slack app for "${target.slug}"${options.slackTeam === undefined
|
|
626
|
+
? ""
|
|
627
|
+
: ` in workspace ${options.slackTeam}`} (team ${ctx.teamId}).\n` +
|
|
628
|
+
(sameSlug.length === 0
|
|
629
|
+
? ""
|
|
630
|
+
: `Provisioned apps for this agent:\n${formatAppList(sameSlug)}`));
|
|
631
|
+
return 1;
|
|
632
|
+
}
|
|
633
|
+
if (candidates.length > 1) {
|
|
634
|
+
ctx.err(`Multiple provisioned Slack apps match "${target.slug}" (${target.env}):\n` +
|
|
635
|
+
formatAppList(candidates) +
|
|
636
|
+
`Pass --slack-team <id> to pick one.\n`);
|
|
637
|
+
return 1;
|
|
638
|
+
}
|
|
639
|
+
return candidates[0];
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
export function cmdSlackDestroy(options) {
|
|
643
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
644
|
+
const target = resolveSlackAppTarget(options, errStream(options));
|
|
645
|
+
if (typeof target === "number") {
|
|
646
|
+
return target;
|
|
647
|
+
}
|
|
648
|
+
const ctx = yield prepareProvision(options);
|
|
649
|
+
if (typeof ctx === "number") {
|
|
650
|
+
return ctx;
|
|
651
|
+
}
|
|
652
|
+
const app = yield resolveProvisionedApp(ctx, target, options);
|
|
653
|
+
if (typeof app === "number") {
|
|
654
|
+
return app;
|
|
655
|
+
}
|
|
656
|
+
try {
|
|
657
|
+
yield ctx.client.deleteApp({
|
|
658
|
+
teamId: ctx.teamId,
|
|
659
|
+
slackAppId: app.slackAppId,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
return renderSlackApiError(error, ctx.err);
|
|
664
|
+
}
|
|
665
|
+
if (ctx.json) {
|
|
666
|
+
ctx.out(`${JSON.stringify({
|
|
667
|
+
ok: true,
|
|
668
|
+
deleted: true,
|
|
669
|
+
appId: app.slackAppId,
|
|
670
|
+
slackTeamId: app.slackTeamId,
|
|
671
|
+
slug: target.slug,
|
|
672
|
+
env: target.env,
|
|
673
|
+
}, null, 2)}\n`);
|
|
674
|
+
return 0;
|
|
675
|
+
}
|
|
676
|
+
ctx.out(`Deleted the ${target.env} Slack app for ${target.slug} (app ${app.slackAppId}) from workspace ${app.slackTeamId}.\n` +
|
|
677
|
+
`Tokens in .env.local were left in place — ${target.envKeys.bot} / ${target.envKeys.app} are now inert; remove them whenever you like.\n`);
|
|
678
|
+
return 0;
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
export function cmdSlackIcon(options) {
|
|
682
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
683
|
+
const err = errStream(options);
|
|
684
|
+
if (options.iconSource === undefined) {
|
|
685
|
+
err(`Usage: ${CLI} slack icon <https-url-or-file> [--dir .] [--slack-team T0123ABCD] [--prod] [--team <id>] [--json]\n`);
|
|
686
|
+
return 1;
|
|
687
|
+
}
|
|
688
|
+
const source = yield resolveSlackIconSource(options.iconSource, err);
|
|
689
|
+
if (typeof source === "number") {
|
|
690
|
+
return source;
|
|
691
|
+
}
|
|
692
|
+
const target = resolveSlackAppTarget(options, err);
|
|
693
|
+
if (typeof target === "number") {
|
|
694
|
+
return target;
|
|
695
|
+
}
|
|
696
|
+
const ctx = yield prepareProvision(options);
|
|
697
|
+
if (typeof ctx === "number") {
|
|
698
|
+
return ctx;
|
|
699
|
+
}
|
|
700
|
+
const app = yield resolveProvisionedApp(ctx, target, options);
|
|
701
|
+
if (typeof app === "number") {
|
|
702
|
+
return app;
|
|
703
|
+
}
|
|
704
|
+
try {
|
|
705
|
+
yield ctx.client.setAppIcon({
|
|
706
|
+
teamId: ctx.teamId,
|
|
707
|
+
slackAppId: app.slackAppId,
|
|
708
|
+
icon: toSlackAppIconInput(source),
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
return renderSlackApiError(error, ctx.err);
|
|
713
|
+
}
|
|
714
|
+
if (ctx.json) {
|
|
715
|
+
ctx.out(`${JSON.stringify({
|
|
716
|
+
ok: true,
|
|
717
|
+
appId: app.slackAppId,
|
|
718
|
+
slackTeamId: app.slackTeamId,
|
|
719
|
+
slug: target.slug,
|
|
720
|
+
env: target.env,
|
|
721
|
+
iconStatus: "set",
|
|
722
|
+
}, null, 2)}\n`);
|
|
723
|
+
return 0;
|
|
724
|
+
}
|
|
725
|
+
ctx.out(`Set the icon for the ${target.env} Slack app for ${target.slug} (app ${app.slackAppId}).\n`);
|
|
726
|
+
return 0;
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
function formatAppList(apps) {
|
|
730
|
+
return apps
|
|
731
|
+
.map((app) => ` - ${app.slackAppId} (${app.env}) in workspace ${app.slackTeamId} — ${app.installStatus}\n`)
|
|
732
|
+
.join("");
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Scaffold the channel files when the channel file is absent. Existing
|
|
736
|
+
* files are never overwritten: initSlackChannel writes with `wx`, and
|
|
737
|
+
* EEXIST means "already scaffolded" (provisioning builds its manifest in
|
|
738
|
+
* memory and does not need the on-disk copies).
|
|
739
|
+
*/
|
|
740
|
+
function scaffoldSlackChannelFiles(options, target) {
|
|
741
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
742
|
+
var _a;
|
|
743
|
+
const channelFile = "agent/channels/slack.ts";
|
|
744
|
+
if (yield fileExists(join(target.dir, channelFile))) {
|
|
745
|
+
return [];
|
|
746
|
+
}
|
|
747
|
+
try {
|
|
748
|
+
// Pass the resolved name/prefix so the channel file and the env keys
|
|
749
|
+
// share one derivation.
|
|
750
|
+
const result = yield initSlackChannel({
|
|
751
|
+
dir: target.dir,
|
|
752
|
+
name: target.name,
|
|
753
|
+
envPrefix: (_a = target.envPrefix) !== null && _a !== void 0 ? _a : "",
|
|
754
|
+
channelPosts: options.channelPosts,
|
|
755
|
+
});
|
|
756
|
+
return result.files;
|
|
757
|
+
}
|
|
758
|
+
catch (error) {
|
|
759
|
+
if (isErrnoException(error) && error.code === "EEXIST") {
|
|
760
|
+
// The channel file is written first, so it exists by the time a
|
|
761
|
+
// later pre-existing file aborts the scaffold.
|
|
762
|
+
return (yield fileExists(join(target.dir, channelFile)))
|
|
763
|
+
? [channelFile]
|
|
764
|
+
: [];
|
|
765
|
+
}
|
|
766
|
+
throw error;
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Append `KEY=value` lines to an env file, replacing every existing line
|
|
772
|
+
* for the same key instead of appending duplicates. Creates the file when
|
|
773
|
+
* missing.
|
|
774
|
+
*/
|
|
775
|
+
function upsertEnvFile(path, entries) {
|
|
776
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
777
|
+
var _a;
|
|
778
|
+
let existing = "";
|
|
779
|
+
try {
|
|
780
|
+
existing = yield readFile(path, "utf8");
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
if (!isErrnoException(error) || error.code !== "ENOENT") {
|
|
784
|
+
throw error;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
const lines = existing === "" ? [] : existing.replace(/\r?\n$/, "").split(/\r?\n/);
|
|
788
|
+
// A stale `export KEY=…` line would shadow or duplicate the bare line
|
|
789
|
+
// under some loaders; treat both spellings as the same entry.
|
|
790
|
+
const matchesKey = (line, key) => line.startsWith(`${key}=`) || line.startsWith(`export ${key}=`);
|
|
791
|
+
for (const { key, value } of entries) {
|
|
792
|
+
const next = `${key}=${value}`;
|
|
793
|
+
let replaced = false;
|
|
794
|
+
for (let i = 0; i < lines.length;) {
|
|
795
|
+
if (!matchesKey((_a = lines[i]) !== null && _a !== void 0 ? _a : "", key)) {
|
|
796
|
+
i += 1;
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
if (replaced) {
|
|
800
|
+
lines.splice(i, 1);
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
lines[i] = next;
|
|
804
|
+
replaced = true;
|
|
805
|
+
i += 1;
|
|
806
|
+
}
|
|
807
|
+
if (!replaced) {
|
|
808
|
+
lines.push(next);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
// writeFile's mode only applies on create; clamp pre-existing files too.
|
|
812
|
+
yield writeFile(path, `${lines.join("\n")}\n`, { mode: 0o600 });
|
|
813
|
+
yield chmod(path, 0o600);
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Ensure the agent directory's .gitignore covers .env.local (create or
|
|
818
|
+
* append). A bare `.env` line does not count — that pattern matches only
|
|
819
|
+
* the file literally named .env.
|
|
820
|
+
*/
|
|
821
|
+
function ensureEnvLocalGitignored(dir) {
|
|
822
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
823
|
+
const path = join(dir, ".gitignore");
|
|
824
|
+
let existing = "";
|
|
825
|
+
try {
|
|
826
|
+
existing = yield readFile(path, "utf8");
|
|
827
|
+
}
|
|
828
|
+
catch (error) {
|
|
829
|
+
if (!isErrnoException(error) || error.code !== "ENOENT") {
|
|
830
|
+
throw error;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
const covered = existing
|
|
834
|
+
.split(/\r?\n/)
|
|
835
|
+
.some((line) => /^\.env(\.local\*?|\.\*|\*)$/.test(line.trim()));
|
|
836
|
+
if (covered) {
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
const next = existing === "" || existing.endsWith("\n")
|
|
840
|
+
? `${existing}.env.local\n`
|
|
841
|
+
: `${existing}\n.env.local\n`;
|
|
842
|
+
yield writeFile(path, next);
|
|
843
|
+
return true;
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
function renderDoctorReport(result) {
|
|
847
|
+
let text = `env: ${result.envKeys.bot} / ${result.envKeys.app}\n`;
|
|
848
|
+
for (const check of result.checks) {
|
|
849
|
+
text += `${check.ok ? "ok" : "!!"} ${check.id}: ${check.message}\n`;
|
|
850
|
+
}
|
|
851
|
+
text += result.ok ? "doctor: pass\n" : "doctor: fail\n";
|
|
852
|
+
return text;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Actionable rendering for `POST /apps` failures the create flow does not
|
|
856
|
+
* retry itself (the connect-and-retry codes are handled by the caller).
|
|
857
|
+
*/
|
|
858
|
+
function renderCreateProvisionError(error, err) {
|
|
859
|
+
var _a;
|
|
860
|
+
if (!(error instanceof SlackProvisionApiError)) {
|
|
861
|
+
throw error;
|
|
862
|
+
}
|
|
863
|
+
switch (error.code) {
|
|
864
|
+
case "app_conflict":
|
|
865
|
+
err(`${error.message}\nA concurrent \`${CLI} slack create\` for this agent won the race; re-run create to pick up that app.\n`);
|
|
866
|
+
return 1;
|
|
867
|
+
case "app_cap_reached":
|
|
868
|
+
err(`${error.message}\nDelete unused apps with \`${CLI} slack destroy\`, then re-run create.\n`);
|
|
869
|
+
return 1;
|
|
870
|
+
case "ratelimited":
|
|
871
|
+
err(`${error.message}\nRe-run \`${CLI} slack create\` after the wait.\n`);
|
|
872
|
+
return 1;
|
|
873
|
+
case "invalid_app_id":
|
|
874
|
+
err(`${error.message}\nThe provisioned Slack app seems to have been deleted outside Cursor. Run \`${CLI} slack destroy\` to drop the stale record, then re-run \`${CLI} slack create\`.\n`);
|
|
875
|
+
return 1;
|
|
876
|
+
case "manifest_drift":
|
|
877
|
+
err(`${error.message}\n` +
|
|
878
|
+
((_a = error.changedPaths) !== null && _a !== void 0 ? _a : []).map((path) => ` - ${path}\n`).join("") +
|
|
879
|
+
`Re-run \`${CLI} slack create --force\` to overwrite those edits.\n`);
|
|
880
|
+
return 1;
|
|
881
|
+
default:
|
|
882
|
+
err(`${error.message}\n`);
|
|
883
|
+
return 1;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
function renderSlackApiError(error, err) {
|
|
887
|
+
if (error instanceof SlackProvisionApiError) {
|
|
888
|
+
err(`${error.message}\n`);
|
|
889
|
+
return 1;
|
|
890
|
+
}
|
|
891
|
+
throw error;
|
|
892
|
+
}
|
|
893
|
+
function fileExists(path) {
|
|
894
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
895
|
+
try {
|
|
896
|
+
yield access(path);
|
|
897
|
+
return true;
|
|
898
|
+
}
|
|
899
|
+
catch (_a) {
|
|
900
|
+
return false;
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
function isErrnoException(error) {
|
|
905
|
+
return error instanceof Error && "code" in error;
|
|
906
|
+
}
|
|
907
|
+
function sleep(ms) {
|
|
908
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
909
|
+
}
|