next-anteater 0.2.4 → 0.2.6
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 +47 -0
- package/bin/setup-anteater.mjs +1 -1
- package/lib/scaffold.mjs +12 -5
- package/lib/setup.mjs +330 -324
- package/lib/uninstall.mjs +130 -20
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# next-anteater
|
|
2
|
+
|
|
3
|
+
Let users make your app.
|
|
4
|
+
|
|
5
|
+
Anteater adds a prompt bar to your Next.js app. When a user describes a change, Claude edits the code, opens a PR, and redeploys automatically via GitHub Actions.
|
|
6
|
+
|
|
7
|
+
## Security Warning
|
|
8
|
+
|
|
9
|
+
**Anteater gives users the ability to modify your application's code via AI. Only expose it to trusted users in a sandboxed environment.**
|
|
10
|
+
|
|
11
|
+
- Users with access to the prompt bar can make **arbitrary code changes**, including destructive ones.
|
|
12
|
+
- The AI agent runs in GitHub Actions with access to your **repository secrets and deployment pipeline**.
|
|
13
|
+
- A malicious or careless prompt could **access sensitive data, delete files, or break your app**.
|
|
14
|
+
- Anteater does **not** provide authentication or authorization. You must protect the prompt bar behind your own auth layer.
|
|
15
|
+
|
|
16
|
+
> Treat Anteater like giving someone commit access to your repo. Never expose it to the public internet with real credentials or production data.
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
npx next-anteater setup
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Three steps: Anthropic key, GitHub PAT, editable paths. Everything else is automatic.
|
|
25
|
+
|
|
26
|
+
## How it works
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
User types change → GitHub Action runs AI agent → PR auto-merges → Vercel redeploys
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
After setup, edit `anteater.config.ts` to control:
|
|
35
|
+
|
|
36
|
+
- `allowedGlobs` / `blockedGlobs` — which files the agent can modify
|
|
37
|
+
- `autoMerge` — whether PRs merge automatically
|
|
38
|
+
- `requireReviewFor` — keywords that block auto-merge (e.g., "auth", "billing")
|
|
39
|
+
- `maxFilesChanged` / `maxDiffBytes` — safety limits on change size
|
|
40
|
+
|
|
41
|
+
## Security Disclaimer ⚠️
|
|
42
|
+
|
|
43
|
+
This software is provided "as is", without warranty of any kind. Use it at your own risk. The authors and contributors are not responsible for any damage, data loss, security breaches, or other harm resulting from the use of this software. By using Anteater, you accept full responsibility for how it is deployed, configured, and who is granted access. See [LICENSE](https://github.com/scottgriffinm/anteater/blob/master/LICENSE) for the full terms.
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
[MIT](https://github.com/scottgriffinm/anteater/blob/master/LICENSE)
|
package/bin/setup-anteater.mjs
CHANGED
package/lib/scaffold.mjs
CHANGED
|
@@ -27,7 +27,14 @@ export function generateConfig({ repo, allowedGlobs, blockedGlobs, autoMerge, is
|
|
|
27
27
|
|
|
28
28
|
return {
|
|
29
29
|
filename: `anteater.config.${ext}`,
|
|
30
|
-
content:
|
|
30
|
+
content: `/**
|
|
31
|
+
* SECURITY: Anteater lets users modify your app's code via AI.
|
|
32
|
+
* Only expose the prompt bar to trusted users behind your own auth layer.
|
|
33
|
+
* Users can make destructive changes and potentially access sensitive data.
|
|
34
|
+
* Never use this in a production environment with real credentials.
|
|
35
|
+
* See: https://github.com/scottgriffinm/anteater#security-warning
|
|
36
|
+
*/
|
|
37
|
+
${typeImport}const config${typeAnnotation} = {
|
|
31
38
|
repo: "${repo}",
|
|
32
39
|
productionBranch: "${productionBranch}",
|
|
33
40
|
modes: ["prod", "copy"],
|
|
@@ -792,12 +799,12 @@ export async function scaffoldFiles(cwd, options) {
|
|
|
792
799
|
results.push(".github/workflows/anteater.yml");
|
|
793
800
|
}
|
|
794
801
|
|
|
795
|
-
// Claude Code agent settings
|
|
802
|
+
// Claude Code agent settings (always overwrite — reflects current choices)
|
|
796
803
|
if (options.model && options.permissionsMode) {
|
|
797
804
|
const settingsPath = join(cwd, ".claude/settings.local.json");
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
805
|
+
await mkdir(dirname(settingsPath), { recursive: true });
|
|
806
|
+
await writeFile(settingsPath, generateClaudeSettings(options), "utf-8");
|
|
807
|
+
results.push(".claude/settings.local.json");
|
|
801
808
|
}
|
|
802
809
|
|
|
803
810
|
// Patch layout
|
package/lib/setup.mjs
CHANGED
|
@@ -1,324 +1,330 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* anteater setup — Interactive CLI to install and configure Anteater.
|
|
3
|
-
*
|
|
4
|
-
* Core logic extracted from bin/setup-anteater.mjs so it can be
|
|
5
|
-
* imported by tests without hitting the shebang line.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { execSync } from "node:child_process";
|
|
9
|
-
import {
|
|
10
|
-
bold, dim, green, red, yellow, cyan,
|
|
11
|
-
ok, fail, warn, info, heading, blank,
|
|
12
|
-
ask, confirm, select, spinner, closeRL,
|
|
13
|
-
} from "./ui.mjs";
|
|
14
|
-
import { detectProject } from "./detect.mjs";
|
|
15
|
-
import { scaffoldFiles } from "./scaffold.mjs";
|
|
16
|
-
import {
|
|
17
|
-
validateAnthropicKey, validateGitHubToken, setGitHubSecret, setVercelEnv,
|
|
18
|
-
writeEnvLocal, hasCommand,
|
|
19
|
-
} from "./secrets.mjs";
|
|
20
|
-
|
|
21
|
-
const cwd = process.cwd();
|
|
22
|
-
|
|
23
|
-
export async function main() {
|
|
24
|
-
console.log();
|
|
25
|
-
console.log(` ${bold("\u{1F41C} Anteater Setup")}`);
|
|
26
|
-
console.log(` ${"\u2500".repeat(17)}`);
|
|
27
|
-
blank();
|
|
28
|
-
|
|
29
|
-
// ───
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
console.log(` ${
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
let
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
{ label: "
|
|
169
|
-
{ label: "Opus
|
|
170
|
-
{ label: "
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
{ label: "
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
info(" -
|
|
185
|
-
info(" -
|
|
186
|
-
info(" -
|
|
187
|
-
info(" -
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
warn("
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
execSync(`git
|
|
264
|
-
execSync(`git
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
{
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
info(
|
|
323
|
-
|
|
324
|
-
|
|
1
|
+
/**
|
|
2
|
+
* anteater setup — Interactive CLI to install and configure Anteater.
|
|
3
|
+
*
|
|
4
|
+
* Core logic extracted from bin/setup-anteater.mjs so it can be
|
|
5
|
+
* imported by tests without hitting the shebang line.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
import {
|
|
10
|
+
bold, dim, green, red, yellow, cyan,
|
|
11
|
+
ok, fail, warn, info, heading, blank,
|
|
12
|
+
ask, confirm, select, spinner, closeRL,
|
|
13
|
+
} from "./ui.mjs";
|
|
14
|
+
import { detectProject } from "./detect.mjs";
|
|
15
|
+
import { scaffoldFiles } from "./scaffold.mjs";
|
|
16
|
+
import {
|
|
17
|
+
validateAnthropicKey, validateGitHubToken, setGitHubSecret, setVercelEnv,
|
|
18
|
+
writeEnvLocal, hasCommand,
|
|
19
|
+
} from "./secrets.mjs";
|
|
20
|
+
|
|
21
|
+
const cwd = process.cwd();
|
|
22
|
+
|
|
23
|
+
export async function main() {
|
|
24
|
+
console.log();
|
|
25
|
+
console.log(` ${bold("\u{1F41C} Anteater Setup")}`);
|
|
26
|
+
console.log(` ${"\u2500".repeat(17)}`);
|
|
27
|
+
blank();
|
|
28
|
+
|
|
29
|
+
// ─── Agent piping guide ────────────────────────────────────
|
|
30
|
+
if (!process.stdin.isTTY) {
|
|
31
|
+
console.log(` ${bold("Piped input detected — Agent Guide")}`);
|
|
32
|
+
console.log(` ${"─".repeat(40)}`);
|
|
33
|
+
console.log(` Prompts (one per line, in order):`);
|
|
34
|
+
console.log(` 1. Anthropic API key`);
|
|
35
|
+
console.log(` 2. GitHub PAT (ghp_... or github_pat_...)`);
|
|
36
|
+
console.log(` 3. Use default paths? (Y/n)`);
|
|
37
|
+
console.log(` 4. Model (1=Sonnet 2=Opus 3=Opus1M 4=Haiku)`);
|
|
38
|
+
console.log(` 5. Permissions (1=Sandboxed 2=Unrestricted)`);
|
|
39
|
+
console.log(` 6. Confirm unrestricted? (y/N) — only if 5=2`);
|
|
40
|
+
console.log();
|
|
41
|
+
console.log(` If paths=n, 2 extra prompts after #3:`);
|
|
42
|
+
console.log(` 3a. Allowed globs (comma-separated)`);
|
|
43
|
+
console.log(` 3b. Blocked globs (comma-separated)`);
|
|
44
|
+
console.log(` ${"─".repeat(40)}`);
|
|
45
|
+
blank();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Security notice ───────────────────────────────────────
|
|
49
|
+
warn("SECURITY: Anteater lets users modify your app's code via AI.");
|
|
50
|
+
info("Only expose it to trusted users in a sandboxed environment.");
|
|
51
|
+
info("Users can make destructive changes and potentially access sensitive data.");
|
|
52
|
+
info(`Learn more: ${cyan("https://github.com/scottgriffinm/anteater#security-warning")}`);
|
|
53
|
+
blank();
|
|
54
|
+
|
|
55
|
+
// ─── Preflight checks ──────────────────────────────────────
|
|
56
|
+
heading("Preflight");
|
|
57
|
+
|
|
58
|
+
if (!hasCommand("gh")) {
|
|
59
|
+
fail("GitHub CLI (gh) is required. Install it: https://cli.github.com");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
ok("GitHub CLI installed");
|
|
63
|
+
|
|
64
|
+
if (!hasCommand("vercel")) {
|
|
65
|
+
fail("Vercel CLI is required. Install it: npm i -g vercel");
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
ok("Vercel CLI installed");
|
|
69
|
+
|
|
70
|
+
// ─── Detect project ─────────────────────────────────────────
|
|
71
|
+
const project = await detectProject(cwd);
|
|
72
|
+
|
|
73
|
+
if (!project.isNextJs) {
|
|
74
|
+
fail("No Next.js project found. Run this from your Next.js project root.");
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
ok(`Next.js ${project.nextVersion ?? ""} ${project.isAppRouter ? "(App Router)" : "(Pages Router)"}`);
|
|
78
|
+
|
|
79
|
+
if (!project.hasGit || !project.gitRemote) {
|
|
80
|
+
fail("No GitHub remote found. Run: git remote add origin <url>");
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
ok(`Repo: ${project.gitRemote}`);
|
|
84
|
+
ok(`Branch: ${project.defaultBranch || "main"}`);
|
|
85
|
+
ok(`Package manager: ${project.packageManager}`);
|
|
86
|
+
blank();
|
|
87
|
+
|
|
88
|
+
// ─── Step 1: Anthropic API key ──────────────────────────────
|
|
89
|
+
heading("Step 1 of 4 \u2014 AI Provider");
|
|
90
|
+
info(`Get a key at ${cyan("https://console.anthropic.com/keys")}`);
|
|
91
|
+
blank();
|
|
92
|
+
|
|
93
|
+
let anthropicKey;
|
|
94
|
+
while (true) {
|
|
95
|
+
anthropicKey = await ask("Anthropic API key:", { mask: true });
|
|
96
|
+
if (!anthropicKey) { warn("Required."); continue; }
|
|
97
|
+
const valid = await spinner("Validating", () => validateAnthropicKey(anthropicKey));
|
|
98
|
+
if (valid) break;
|
|
99
|
+
fail("Invalid key. Check that it starts with sk-ant- and try again.");
|
|
100
|
+
}
|
|
101
|
+
blank();
|
|
102
|
+
|
|
103
|
+
// ─── Step 2: GitHub PAT ─────────────────────────────────────
|
|
104
|
+
heading("Step 2 of 4 \u2014 GitHub Access");
|
|
105
|
+
|
|
106
|
+
info("Anteater needs a long-lived Personal Access Token (PAT) for the deployed API route.");
|
|
107
|
+
blank();
|
|
108
|
+
info(`${bold("Create a Fine-grained token:")} ${cyan("https://github.com/settings/tokens?type=beta")}`);
|
|
109
|
+
info(` 1. Click ${bold("Generate new token")}`);
|
|
110
|
+
info(` 2. Select ${bold("Only select repositories")} \u2192 pick your repo`);
|
|
111
|
+
info(` 3. Set permissions: ${bold("Contents")}, ${bold("Pull requests")}, ${bold("Actions")} \u2192 Read and write`);
|
|
112
|
+
info(` 4. Generate and copy the token`);
|
|
113
|
+
blank();
|
|
114
|
+
const githubToken = await ask("Paste your GitHub PAT (ghp_... or github_pat_...):");
|
|
115
|
+
if (!githubToken) {
|
|
116
|
+
fail("A GitHub PAT is required.");
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const check = await spinner("Checking permissions", () =>
|
|
121
|
+
validateGitHubToken(githubToken, project.gitRemote)
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (!check.ok && check.missing.length > 0 && !check.missing.includes("unknown")) {
|
|
125
|
+
fail("Token is missing required scopes: " + check.missing.join(", "));
|
|
126
|
+
info(`Create a new Fine-grained PAT at ${cyan("https://github.com/settings/tokens?type=beta")}`);
|
|
127
|
+
info(`Set permissions: ${bold("Contents")}, ${bold("Pull requests")}, ${bold("Actions")} \u2192 Read and write`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
} else if (check.ok) {
|
|
130
|
+
ok("Token has required permissions");
|
|
131
|
+
}
|
|
132
|
+
blank();
|
|
133
|
+
|
|
134
|
+
// ─── Step 3: Configure paths ────────────────────────────────
|
|
135
|
+
heading("Step 3 of 4 \u2014 Editable Paths");
|
|
136
|
+
|
|
137
|
+
const defaultAllowed = [];
|
|
138
|
+
const defaultBlocked = ["lib/auth/**", "lib/billing/**", ".env*"];
|
|
139
|
+
|
|
140
|
+
if (project.isAppRouter) {
|
|
141
|
+
defaultAllowed.push("app/**", "components/**", "styles/**");
|
|
142
|
+
defaultBlocked.push("app/api/**");
|
|
143
|
+
} else {
|
|
144
|
+
defaultAllowed.push("pages/**", "components/**", "styles/**");
|
|
145
|
+
defaultBlocked.push("pages/api/**");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log(` ${green("Allowed:")} ${defaultAllowed.join(", ")}`);
|
|
149
|
+
console.log(` ${red("Blocked:")} ${defaultBlocked.join(", ")}`);
|
|
150
|
+
blank();
|
|
151
|
+
|
|
152
|
+
const useDefaults = await confirm("Use these defaults?");
|
|
153
|
+
let allowedGlobs = defaultAllowed;
|
|
154
|
+
let blockedGlobs = defaultBlocked;
|
|
155
|
+
|
|
156
|
+
if (!useDefaults) {
|
|
157
|
+
const customAllowed = await ask("Allowed globs (comma-separated):");
|
|
158
|
+
const customBlocked = await ask("Blocked globs (comma-separated):");
|
|
159
|
+
if (customAllowed) allowedGlobs = customAllowed.split(",").map((s) => s.trim());
|
|
160
|
+
if (customBlocked) blockedGlobs = customBlocked.split(",").map((s) => s.trim());
|
|
161
|
+
}
|
|
162
|
+
blank();
|
|
163
|
+
|
|
164
|
+
// ─── Step 4: Agent configuration ─────────────────────────────
|
|
165
|
+
heading("Step 4 of 4 \u2014 Agent Configuration");
|
|
166
|
+
|
|
167
|
+
const model = await select("Select AI model:", [
|
|
168
|
+
{ label: "Sonnet (recommended)", hint: "fast, cost-effective, great for most changes", value: "sonnet" },
|
|
169
|
+
{ label: "Opus", hint: "most capable, higher cost", value: "opus" },
|
|
170
|
+
{ label: "Opus 1M", hint: "Opus with extended context (1M tokens)", value: "opus[1m]" },
|
|
171
|
+
{ label: "Haiku", hint: "fastest, lowest cost, best for simple changes", value: "haiku" },
|
|
172
|
+
]);
|
|
173
|
+
ok(`Model: ${model}`);
|
|
174
|
+
blank();
|
|
175
|
+
|
|
176
|
+
let permissionsMode = await select("Select agent permissions mode:", [
|
|
177
|
+
{ label: "Sandboxed (recommended)", hint: "full local access, no internet or external services", value: "sandboxed" },
|
|
178
|
+
{ label: "Unrestricted", hint: "full access including web, GitHub CLI, Vercel, and all MCP tools", value: "unrestricted" },
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
if (permissionsMode === "unrestricted") {
|
|
182
|
+
blank();
|
|
183
|
+
warn("Unrestricted mode grants the AI agent full access to:");
|
|
184
|
+
info(" - Internet (web fetches, searches, curl)");
|
|
185
|
+
info(" - GitHub CLI (push, PR creation, issue management)");
|
|
186
|
+
info(" - Vercel CLI (deployments, env vars)");
|
|
187
|
+
info(" - All MCP tools (browser automation, etc.)");
|
|
188
|
+
info(" - File deletion and system commands");
|
|
189
|
+
blank();
|
|
190
|
+
warn("The agent will run with bypassPermissions \u2014 no confirmation prompts.");
|
|
191
|
+
warn("Only use this if you trust the prompts your users will submit.");
|
|
192
|
+
blank();
|
|
193
|
+
const confirmed = await confirm("Confirm unrestricted mode?", false);
|
|
194
|
+
if (!confirmed) {
|
|
195
|
+
permissionsMode = "sandboxed";
|
|
196
|
+
ok("Falling back to Sandboxed mode");
|
|
197
|
+
} else {
|
|
198
|
+
ok("Unrestricted mode confirmed");
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
ok("Sandboxed mode \u2014 agent cannot access internet or external services");
|
|
202
|
+
}
|
|
203
|
+
blank();
|
|
204
|
+
|
|
205
|
+
// ─── Install & scaffold ─────────────────────────────────────
|
|
206
|
+
heading("Installing");
|
|
207
|
+
|
|
208
|
+
const installCmd = {
|
|
209
|
+
pnpm: "pnpm add next-anteater",
|
|
210
|
+
yarn: "yarn add next-anteater",
|
|
211
|
+
npm: "npm install next-anteater",
|
|
212
|
+
}[project.packageManager];
|
|
213
|
+
|
|
214
|
+
await spinner("Installing next-anteater", () => {
|
|
215
|
+
execSync(installCmd, { cwd, stdio: "ignore" });
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const productionBranch = project.defaultBranch || "main";
|
|
219
|
+
const scaffolded = await spinner("Creating files", () =>
|
|
220
|
+
scaffoldFiles(cwd, {
|
|
221
|
+
repo: project.gitRemote,
|
|
222
|
+
allowedGlobs,
|
|
223
|
+
blockedGlobs,
|
|
224
|
+
autoMerge: true,
|
|
225
|
+
productionBranch,
|
|
226
|
+
isTypeScript: project.isTypeScript,
|
|
227
|
+
isAppRouter: project.isAppRouter,
|
|
228
|
+
layoutFile: project.layoutFile,
|
|
229
|
+
model,
|
|
230
|
+
permissionsMode,
|
|
231
|
+
packageManager: project.packageManager,
|
|
232
|
+
})
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
for (const f of scaffolded) ok(`Created ${f}`);
|
|
236
|
+
|
|
237
|
+
// ─── Set secrets ────────────────────────────────────────────
|
|
238
|
+
heading("Configuring secrets");
|
|
239
|
+
|
|
240
|
+
// GitHub Actions secret
|
|
241
|
+
try {
|
|
242
|
+
await spinner("Setting ANTHROPIC_API_KEY in GitHub secrets", () => {
|
|
243
|
+
setGitHubSecret(project.gitRemote, "ANTHROPIC_API_KEY", anthropicKey);
|
|
244
|
+
});
|
|
245
|
+
} catch (err) {
|
|
246
|
+
warn(`Could not set secret: ${err.message}`);
|
|
247
|
+
info("Set manually: gh secret set ANTHROPIC_API_KEY --repo " + project.gitRemote);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// .env.local for local dev (only GITHUB_TOKEN needed)
|
|
251
|
+
await spinner("Writing .env.local", () =>
|
|
252
|
+
writeEnvLocal(cwd, { GITHUB_TOKEN: githubToken })
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// Vercel: only GITHUB_TOKEN needed (repo auto-detected, deploy detection automatic)
|
|
256
|
+
await spinner("Setting GITHUB_TOKEN in Vercel", () => {
|
|
257
|
+
setVercelEnv("GITHUB_TOKEN", githubToken);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// ─── Push workflow ──────────────────────────────────────────
|
|
261
|
+
if (scaffolded.some((f) => f.includes("anteater.yml"))) {
|
|
262
|
+
await spinner("Pushing workflow to GitHub", () => {
|
|
263
|
+
execSync(`git add .github/workflows/anteater.yml`, { cwd, stdio: "ignore" });
|
|
264
|
+
execSync(`git commit -m "chore: add Anteater workflow"`, { cwd, stdio: "ignore" });
|
|
265
|
+
execSync(`git push origin ${productionBranch}`, { cwd, stdio: "ignore" });
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Verify
|
|
269
|
+
const activated = await spinner("Verifying workflow", async () => {
|
|
270
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
271
|
+
try {
|
|
272
|
+
const res = await fetch(
|
|
273
|
+
`https://api.github.com/repos/${project.gitRemote}/actions/workflows`,
|
|
274
|
+
{ headers: { Authorization: `Bearer ${githubToken}`, Accept: "application/vnd.github+json" } }
|
|
275
|
+
);
|
|
276
|
+
const data = await res.json();
|
|
277
|
+
return data.workflows?.some((w) => w.path === ".github/workflows/anteater.yml" && w.state === "active");
|
|
278
|
+
} catch { return false; }
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (activated) ok("Workflow active");
|
|
282
|
+
else warn(`Check: ${cyan(`https://github.com/${project.gitRemote}/actions`)}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ─── Test ───────────────────────────────────────────────────
|
|
286
|
+
const dispatchOk = await spinner("Running test dispatch", async () => {
|
|
287
|
+
try {
|
|
288
|
+
const res = await fetch(
|
|
289
|
+
`https://api.github.com/repos/${project.gitRemote}/actions/workflows/anteater.yml/dispatches`,
|
|
290
|
+
{
|
|
291
|
+
method: "POST",
|
|
292
|
+
headers: {
|
|
293
|
+
Authorization: `Bearer ${githubToken}`,
|
|
294
|
+
Accept: "application/vnd.github+json",
|
|
295
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
296
|
+
},
|
|
297
|
+
body: JSON.stringify({
|
|
298
|
+
ref: productionBranch,
|
|
299
|
+
inputs: {
|
|
300
|
+
requestId: "setup-test",
|
|
301
|
+
prompt: "setup verification \u2014 no changes expected",
|
|
302
|
+
mode: "prod",
|
|
303
|
+
branch: "anteater/setup-test",
|
|
304
|
+
baseBranch: productionBranch,
|
|
305
|
+
autoMerge: "false",
|
|
306
|
+
},
|
|
307
|
+
}),
|
|
308
|
+
}
|
|
309
|
+
);
|
|
310
|
+
return res.status === 204;
|
|
311
|
+
} catch { return false; }
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
if (dispatchOk) ok("Pipeline is working");
|
|
315
|
+
else warn("Test dispatch failed \u2014 check GitHub Actions");
|
|
316
|
+
|
|
317
|
+
// ─── Done! ──────────────────────────────────────────────────
|
|
318
|
+
closeRL();
|
|
319
|
+
blank();
|
|
320
|
+
console.log(` ${bold(green("\u{1F41C} Anteater is ready."))}`);
|
|
321
|
+
blank();
|
|
322
|
+
info(`Deploy your app and look for the "${green("Edit this page")}" button.`);
|
|
323
|
+
info("Users can modify your app by typing changes in the Anteater bar.");
|
|
324
|
+
blank();
|
|
325
|
+
warn("Reminder: only expose Anteater to trusted users.");
|
|
326
|
+
info("Users with access to the prompt bar can make arbitrary code changes.");
|
|
327
|
+
info("Use a sandboxed environment without real credentials or production data.");
|
|
328
|
+
info(`Protect the prompt bar behind your own auth layer \u2014 Anteater does ${bold("not")} provide auth.`);
|
|
329
|
+
blank();
|
|
330
|
+
}
|
package/lib/uninstall.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { readFile, writeFile, rm, access } from "node:fs/promises";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
import { bold, green, red, dim, ok, fail, info, heading, blank } from "./ui.mjs";
|
|
7
8
|
|
|
8
9
|
const cwd = process.cwd();
|
|
9
10
|
|
|
@@ -25,6 +26,9 @@ async function removeFile(path, label) {
|
|
|
25
26
|
return false;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Remove AnteaterBar import and component usage from the layout file.
|
|
31
|
+
*/
|
|
28
32
|
async function unpatchLayout() {
|
|
29
33
|
for (const layoutFile of ["app/layout.tsx", "app/layout.js"]) {
|
|
30
34
|
const fullPath = join(cwd, layoutFile);
|
|
@@ -33,11 +37,17 @@ async function unpatchLayout() {
|
|
|
33
37
|
let content = await readFile(fullPath, "utf-8");
|
|
34
38
|
if (!content.includes("AnteaterBar")) return false;
|
|
35
39
|
|
|
36
|
-
// Remove import lines
|
|
40
|
+
// Remove import lines that contain AnteaterBar
|
|
37
41
|
content = content.replace(/^.*AnteaterBar.*\n/gm, "");
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
|
|
43
|
+
// Remove <AnteaterBar /> or <AnteaterBar ... /> on its own line
|
|
44
|
+
content = content.replace(/^\s*<AnteaterBar\b[^>]*\/>\s*\n?/gm, "");
|
|
45
|
+
// Remove inline <AnteaterBar /> (e.g., "{children} <AnteaterBar />")
|
|
46
|
+
content = content.replace(/\s*<AnteaterBar\b[^>]*\/>/g, "");
|
|
47
|
+
|
|
48
|
+
// Remove <AnteaterBarWrapper /> on its own line or inline
|
|
40
49
|
content = content.replace(/^\s*<AnteaterBarWrapper\s*\/>\s*\n?/gm, "");
|
|
50
|
+
content = content.replace(/\s*<AnteaterBarWrapper\s*\/>/g, "");
|
|
41
51
|
|
|
42
52
|
await writeFile(fullPath, content, "utf-8");
|
|
43
53
|
ok(`Unpatched ${layoutFile}`);
|
|
@@ -46,6 +56,9 @@ async function unpatchLayout() {
|
|
|
46
56
|
return false;
|
|
47
57
|
}
|
|
48
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Remove next-anteater from package.json dependencies.
|
|
61
|
+
*/
|
|
49
62
|
async function removeDependency() {
|
|
50
63
|
const pkgPath = join(cwd, "package.json");
|
|
51
64
|
if (!(await fileExists(pkgPath))) return false;
|
|
@@ -69,44 +82,141 @@ async function removeDependency() {
|
|
|
69
82
|
return changed;
|
|
70
83
|
}
|
|
71
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Remove GITHUB_TOKEN line from .env.local if present.
|
|
87
|
+
*/
|
|
88
|
+
async function cleanEnvLocal() {
|
|
89
|
+
const envPath = join(cwd, ".env.local");
|
|
90
|
+
if (!(await fileExists(envPath))) return false;
|
|
91
|
+
|
|
92
|
+
const content = await readFile(envPath, "utf-8");
|
|
93
|
+
if (!content.includes("GITHUB_TOKEN=")) return false;
|
|
94
|
+
|
|
95
|
+
const cleaned = content
|
|
96
|
+
.split("\n")
|
|
97
|
+
.filter((line) => !line.startsWith("GITHUB_TOKEN="))
|
|
98
|
+
.join("\n");
|
|
99
|
+
|
|
100
|
+
await writeFile(envPath, cleaned, "utf-8");
|
|
101
|
+
ok("Removed GITHUB_TOKEN from .env.local");
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Detect the git remote (owner/repo) for secret cleanup.
|
|
107
|
+
*/
|
|
108
|
+
function detectRepo() {
|
|
109
|
+
try {
|
|
110
|
+
const url = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
|
|
111
|
+
const match = url.match(/github\.com[/:](.+?)(?:\.git)?$/);
|
|
112
|
+
return match ? match[1] : null;
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Remove ANTHROPIC_API_KEY from GitHub Actions secrets.
|
|
120
|
+
*/
|
|
121
|
+
async function removeGitHubSecret(repo) {
|
|
122
|
+
try {
|
|
123
|
+
execSync(`gh secret delete ANTHROPIC_API_KEY --repo ${repo}`, {
|
|
124
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
125
|
+
});
|
|
126
|
+
ok("Removed ANTHROPIC_API_KEY from GitHub secrets");
|
|
127
|
+
return true;
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Remove GITHUB_TOKEN from Vercel environment variables.
|
|
135
|
+
*/
|
|
136
|
+
async function removeVercelEnv() {
|
|
137
|
+
try {
|
|
138
|
+
execSync("vercel --version", { stdio: "ignore" });
|
|
139
|
+
} catch {
|
|
140
|
+
return false; // Vercel CLI not installed
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let removed = false;
|
|
144
|
+
for (const env of ["production", "preview", "development"]) {
|
|
145
|
+
try {
|
|
146
|
+
execSync(`vercel env rm GITHUB_TOKEN ${env} --yes`, {
|
|
147
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
148
|
+
});
|
|
149
|
+
removed = true;
|
|
150
|
+
} catch {
|
|
151
|
+
// May not exist in this environment
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (removed) ok("Removed GITHUB_TOKEN from Vercel environment variables");
|
|
155
|
+
return removed;
|
|
156
|
+
}
|
|
157
|
+
|
|
72
158
|
export async function main() {
|
|
73
159
|
console.log();
|
|
74
160
|
console.log(` ${bold("\u{1F41C} Anteater Uninstall")}`);
|
|
75
161
|
console.log(` ${"\u2500".repeat(20)}`);
|
|
76
162
|
blank();
|
|
77
163
|
|
|
78
|
-
|
|
164
|
+
let totalRemoved = 0;
|
|
79
165
|
|
|
80
|
-
|
|
166
|
+
// ── Scaffolded files ──────────────────────────────────
|
|
167
|
+
heading("Removing scaffolded files");
|
|
81
168
|
|
|
82
169
|
// Config file
|
|
83
|
-
if (await removeFile(join(cwd, "anteater.config.ts"), "anteater.config.ts"))
|
|
84
|
-
if (await removeFile(join(cwd, "anteater.config.js"), "anteater.config.js"))
|
|
170
|
+
if (await removeFile(join(cwd, "anteater.config.ts"), "anteater.config.ts")) totalRemoved++;
|
|
171
|
+
if (await removeFile(join(cwd, "anteater.config.js"), "anteater.config.js")) totalRemoved++;
|
|
85
172
|
|
|
86
173
|
// API routes
|
|
87
|
-
if (await removeFile(join(cwd, "app/api/anteater"), "app/api/anteater/"))
|
|
88
|
-
if (await removeFile(join(cwd, "pages/api/anteater"), "pages/api/anteater/"))
|
|
174
|
+
if (await removeFile(join(cwd, "app/api/anteater"), "app/api/anteater/")) totalRemoved++;
|
|
175
|
+
if (await removeFile(join(cwd, "pages/api/anteater"), "pages/api/anteater/")) totalRemoved++;
|
|
89
176
|
|
|
90
177
|
// Wrapper component
|
|
91
|
-
if (await removeFile(join(cwd, "components/anteater-bar-wrapper.tsx"), "components/anteater-bar-wrapper.tsx"))
|
|
92
|
-
if (await removeFile(join(cwd, "components/anteater-bar-wrapper.js"), "components/anteater-bar-wrapper.js"))
|
|
178
|
+
if (await removeFile(join(cwd, "components/anteater-bar-wrapper.tsx"), "components/anteater-bar-wrapper.tsx")) totalRemoved++;
|
|
179
|
+
if (await removeFile(join(cwd, "components/anteater-bar-wrapper.js"), "components/anteater-bar-wrapper.js")) totalRemoved++;
|
|
93
180
|
|
|
94
181
|
// GitHub workflow
|
|
95
|
-
if (await removeFile(join(cwd, ".github/workflows/anteater.yml"), ".github/workflows/anteater.yml"))
|
|
182
|
+
if (await removeFile(join(cwd, ".github/workflows/anteater.yml"), ".github/workflows/anteater.yml")) totalRemoved++;
|
|
183
|
+
|
|
184
|
+
// Claude Code agent settings
|
|
185
|
+
if (await removeFile(join(cwd, ".claude/settings.local.json"), ".claude/settings.local.json")) totalRemoved++;
|
|
96
186
|
|
|
97
|
-
//
|
|
187
|
+
// ── Layout cleanup ────────────────────────────────────
|
|
98
188
|
heading("Cleaning up layout");
|
|
99
|
-
await unpatchLayout()
|
|
189
|
+
if (await unpatchLayout()) totalRemoved++;
|
|
100
190
|
|
|
101
|
-
//
|
|
191
|
+
// ── Package dependency ────────────────────────────────
|
|
102
192
|
heading("Removing dependency");
|
|
103
|
-
await removeDependency()
|
|
193
|
+
if (await removeDependency()) totalRemoved++;
|
|
194
|
+
|
|
195
|
+
// ── Environment & secrets ─────────────────────────────
|
|
196
|
+
heading("Cleaning up secrets & environment");
|
|
197
|
+
|
|
198
|
+
// .env.local
|
|
199
|
+
if (await cleanEnvLocal()) totalRemoved++;
|
|
200
|
+
|
|
201
|
+
// GitHub secret
|
|
202
|
+
const repo = detectRepo();
|
|
203
|
+
if (repo) {
|
|
204
|
+
if (await removeGitHubSecret(repo)) totalRemoved++;
|
|
205
|
+
} else {
|
|
206
|
+
info("Could not detect repo — skip GitHub secret cleanup");
|
|
207
|
+
info("Run manually: gh secret delete ANTHROPIC_API_KEY --repo <owner>/<repo>");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Vercel env var
|
|
211
|
+
if (await removeVercelEnv()) totalRemoved++;
|
|
104
212
|
|
|
213
|
+
// ── Summary ───────────────────────────────────────────
|
|
105
214
|
blank();
|
|
106
|
-
if (
|
|
107
|
-
console.log(` ${green("\u2713")} Anteater uninstalled
|
|
215
|
+
if (totalRemoved > 0) {
|
|
216
|
+
console.log(` ${green("\u2713")} Anteater fully uninstalled (${totalRemoved} items removed).`);
|
|
217
|
+
console.log(` ${dim("Run npx next-anteater setup to reinstall.")}`);
|
|
108
218
|
} else {
|
|
109
|
-
console.log(` ${dim("No Anteater files found
|
|
219
|
+
console.log(` ${dim("No Anteater files found \u2014 nothing to remove.")}`);
|
|
110
220
|
}
|
|
111
221
|
console.log();
|
|
112
222
|
}
|