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 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)
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import { main } from "../lib/setup.mjs";
3
3
 
4
4
  main().catch((err) => {
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: `${typeImport}const config${typeAnnotation} = {
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
- if (await writeIfNotExists(settingsPath, generateClaudeSettings(options))) {
799
- results.push(".claude/settings.local.json");
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
- // ─── Preflight checks ──────────────────────────────────────
30
- heading("Preflight");
31
-
32
- if (!hasCommand("gh")) {
33
- fail("GitHub CLI (gh) is required. Install it: https://cli.github.com");
34
- process.exit(1);
35
- }
36
- ok("GitHub CLI installed");
37
-
38
- if (!hasCommand("vercel")) {
39
- fail("Vercel CLI is required. Install it: npm i -g vercel");
40
- process.exit(1);
41
- }
42
- ok("Vercel CLI installed");
43
-
44
- // ─── Detect project ─────────────────────────────────────────
45
- const project = await detectProject(cwd);
46
-
47
- if (!project.isNextJs) {
48
- fail("No Next.js project found. Run this from your Next.js project root.");
49
- process.exit(1);
50
- }
51
- ok(`Next.js ${project.nextVersion ?? ""} ${project.isAppRouter ? "(App Router)" : "(Pages Router)"}`);
52
-
53
- if (!project.hasGit || !project.gitRemote) {
54
- fail("No GitHub remote found. Run: git remote add origin <url>");
55
- process.exit(1);
56
- }
57
- ok(`Repo: ${project.gitRemote}`);
58
- ok(`Branch: ${project.defaultBranch || "main"}`);
59
- ok(`Package manager: ${project.packageManager}`);
60
- blank();
61
-
62
- // ─── Step 1: Anthropic API key ──────────────────────────────
63
- heading("Step 1 of 4 \u2014 AI Provider");
64
- info(`Get a key at ${cyan("https://console.anthropic.com/keys")}`);
65
- blank();
66
-
67
- let anthropicKey;
68
- while (true) {
69
- anthropicKey = await ask("Anthropic API key:", { mask: true });
70
- if (!anthropicKey) { warn("Required."); continue; }
71
- const valid = await spinner("Validating", () => validateAnthropicKey(anthropicKey));
72
- if (valid) break;
73
- fail("Invalid key. Check that it starts with sk-ant- and try again.");
74
- }
75
- blank();
76
-
77
- // ─── Step 2: GitHub access ──────────────────────────────────
78
- heading("Step 2 of 4 \u2014 GitHub Access");
79
-
80
- let githubToken;
81
- try {
82
- githubToken = execSync("gh auth token", { encoding: "utf-8" }).trim();
83
- } catch {
84
- fail("GitHub CLI not authenticated. Run: gh auth login");
85
- process.exit(1);
86
- }
87
-
88
- // OAuth tokens (gho_*) expire in ~8 hours \u2014 not suitable for Vercel env
89
- if (githubToken.startsWith("gho_")) {
90
- warn("Your GitHub CLI token is a short-lived OAuth token (expires in ~8 hours).");
91
- info("Anteater needs a long-lived Personal Access Token (PAT) for the deployed API route.");
92
- blank();
93
- info(`${bold("Create a Fine-grained token:")} ${cyan("https://github.com/settings/tokens?type=beta")}`);
94
- info(` 1. Click ${bold("Generate new token")}`);
95
- info(` 2. Select ${bold("Only select repositories")} pick your repo`);
96
- info(` 3. Set permissions: ${bold("Contents")}, ${bold("Pull requests")}, ${bold("Actions")} Read and write`);
97
- info(` 4. Generate and copy the token`);
98
- blank();
99
- githubToken = await ask("Paste your GitHub PAT (ghp_... or github_pat_...):");
100
- if (!githubToken) {
101
- fail("A GitHub PAT is required.");
102
- process.exit(1);
103
- }
104
- } else {
105
- ok("Using token from GitHub CLI");
106
- }
107
-
108
- const check = await spinner("Checking permissions", () =>
109
- validateGitHubToken(githubToken, project.gitRemote)
110
- );
111
-
112
- if (!check.ok && check.missing.length > 0 && !check.missing.includes("unknown")) {
113
- if (githubToken.startsWith("ghp_") || githubToken.startsWith("github_pat_")) {
114
- fail("Token is missing required scopes: " + check.missing.join(", "));
115
- info(`Create a new Fine-grained PAT at ${cyan("https://github.com/settings/tokens?type=beta")}`);
116
- info(`Set permissions: ${bold("Contents")}, ${bold("Pull requests")}, ${bold("Actions")} → Read and write`);
117
- process.exit(1);
118
- }
119
- info("Upgrading token scopes...");
120
- try {
121
- execSync(`gh auth refresh --scopes ${check.missing.join(",")}`, { stdio: "inherit" });
122
- githubToken = execSync("gh auth token", { encoding: "utf-8" }).trim();
123
- ok("Token scopes updated");
124
- } catch {
125
- fail("Could not upgrade token. Run: gh auth refresh --scopes repo,workflow");
126
- process.exit(1);
127
- }
128
- } else if (check.ok) {
129
- ok("Token has required permissions");
130
- }
131
- blank();
132
-
133
- // ─── Step 3: Configure paths ────────────────────────────────
134
- heading("Step 3 of 4 \u2014 Editable Paths");
135
-
136
- const defaultAllowed = [];
137
- const defaultBlocked = ["lib/auth/**", "lib/billing/**", ".env*"];
138
-
139
- if (project.isAppRouter) {
140
- defaultAllowed.push("app/**", "components/**", "styles/**");
141
- defaultBlocked.push("app/api/**");
142
- } else {
143
- defaultAllowed.push("pages/**", "components/**", "styles/**");
144
- defaultBlocked.push("pages/api/**");
145
- }
146
-
147
- console.log(` ${green("Allowed:")} ${defaultAllowed.join(", ")}`);
148
- console.log(` ${red("Blocked:")} ${defaultBlocked.join(", ")}`);
149
- blank();
150
-
151
- const useDefaults = await confirm("Use these defaults?");
152
- let allowedGlobs = defaultAllowed;
153
- let blockedGlobs = defaultBlocked;
154
-
155
- if (!useDefaults) {
156
- const customAllowed = await ask("Allowed globs (comma-separated):");
157
- const customBlocked = await ask("Blocked globs (comma-separated):");
158
- if (customAllowed) allowedGlobs = customAllowed.split(",").map((s) => s.trim());
159
- if (customBlocked) blockedGlobs = customBlocked.split(",").map((s) => s.trim());
160
- }
161
- blank();
162
-
163
- // ─── Step 4: Agent configuration ─────────────────────────────
164
- heading("Step 4 of 4 \u2014 Agent Configuration");
165
-
166
- const model = await select("Select AI model:", [
167
- { label: "Sonnet (recommended)", hint: "fast, cost-effective, great for most changes", value: "sonnet" },
168
- { label: "Opus", hint: "most capable, higher cost", value: "opus" },
169
- { label: "Opus 1M", hint: "Opus with extended context (1M tokens)", value: "opus[1m]" },
170
- { label: "Haiku", hint: "fastest, lowest cost, best for simple changes", value: "haiku" },
171
- ]);
172
- ok(`Model: ${model}`);
173
- blank();
174
-
175
- let permissionsMode = await select("Select agent permissions mode:", [
176
- { label: "Sandboxed (recommended)", hint: "full local access, no internet or external services", value: "sandboxed" },
177
- { label: "Unrestricted", hint: "full access including web, GitHub CLI, Vercel, and all MCP tools", value: "unrestricted" },
178
- ]);
179
-
180
- if (permissionsMode === "unrestricted") {
181
- blank();
182
- warn("Unrestricted mode grants the AI agent full access to:");
183
- info(" - Internet (web fetches, searches, curl)");
184
- info(" - GitHub CLI (push, PR creation, issue management)");
185
- info(" - Vercel CLI (deployments, env vars)");
186
- info(" - All MCP tools (browser automation, etc.)");
187
- info(" - File deletion and system commands");
188
- blank();
189
- warn("The agent will run with bypassPermissions \u2014 no confirmation prompts.");
190
- warn("Only use this if you trust the prompts your users will submit.");
191
- blank();
192
- const confirmed = await confirm("Confirm unrestricted mode?", false);
193
- if (!confirmed) {
194
- permissionsMode = "sandboxed";
195
- ok("Falling back to Sandboxed mode");
196
- } else {
197
- ok("Unrestricted mode confirmed");
198
- }
199
- } else {
200
- ok("Sandboxed mode \u2014 agent cannot access internet or external services");
201
- }
202
- blank();
203
-
204
- // ─── Install & scaffold ─────────────────────────────────────
205
- heading("Installing");
206
-
207
- const installCmd = {
208
- pnpm: "pnpm add next-anteater",
209
- yarn: "yarn add next-anteater",
210
- npm: "npm install next-anteater",
211
- }[project.packageManager];
212
-
213
- await spinner("Installing next-anteater", () => {
214
- execSync(installCmd, { cwd, stdio: "ignore" });
215
- });
216
-
217
- const productionBranch = project.defaultBranch || "main";
218
- const scaffolded = await spinner("Creating files", () =>
219
- scaffoldFiles(cwd, {
220
- repo: project.gitRemote,
221
- allowedGlobs,
222
- blockedGlobs,
223
- autoMerge: true,
224
- productionBranch,
225
- isTypeScript: project.isTypeScript,
226
- isAppRouter: project.isAppRouter,
227
- layoutFile: project.layoutFile,
228
- model,
229
- permissionsMode,
230
- packageManager: project.packageManager,
231
- })
232
- );
233
-
234
- for (const f of scaffolded) ok(`Created ${f}`);
235
-
236
- // ─── Set secrets ────────────────────────────────────────────
237
- heading("Configuring secrets");
238
-
239
- // GitHub Actions secret
240
- try {
241
- await spinner("Setting ANTHROPIC_API_KEY in GitHub secrets", () => {
242
- setGitHubSecret(project.gitRemote, "ANTHROPIC_API_KEY", anthropicKey);
243
- });
244
- } catch (err) {
245
- warn(`Could not set secret: ${err.message}`);
246
- info("Set manually: gh secret set ANTHROPIC_API_KEY --repo " + project.gitRemote);
247
- }
248
-
249
- // .env.local for local dev (only GITHUB_TOKEN needed)
250
- await spinner("Writing .env.local", () =>
251
- writeEnvLocal(cwd, { GITHUB_TOKEN: githubToken })
252
- );
253
-
254
- // Vercel: only GITHUB_TOKEN needed (repo auto-detected, deploy detection automatic)
255
- await spinner("Setting GITHUB_TOKEN in Vercel", () => {
256
- setVercelEnv("GITHUB_TOKEN", githubToken);
257
- });
258
-
259
- // ─── Push workflow ──────────────────────────────────────────
260
- if (scaffolded.some((f) => f.includes("anteater.yml"))) {
261
- await spinner("Pushing workflow to GitHub", () => {
262
- execSync(`git add .github/workflows/anteater.yml`, { cwd, stdio: "ignore" });
263
- execSync(`git commit -m "chore: add Anteater workflow"`, { cwd, stdio: "ignore" });
264
- execSync(`git push origin ${productionBranch}`, { cwd, stdio: "ignore" });
265
- });
266
-
267
- // Verify
268
- const activated = await spinner("Verifying workflow", async () => {
269
- await new Promise((r) => setTimeout(r, 2000));
270
- try {
271
- const res = await fetch(
272
- `https://api.github.com/repos/${project.gitRemote}/actions/workflows`,
273
- { headers: { Authorization: `Bearer ${githubToken}`, Accept: "application/vnd.github+json" } }
274
- );
275
- const data = await res.json();
276
- return data.workflows?.some((w) => w.path === ".github/workflows/anteater.yml" && w.state === "active");
277
- } catch { return false; }
278
- });
279
-
280
- if (activated) ok("Workflow active");
281
- else warn(`Check: ${cyan(`https://github.com/${project.gitRemote}/actions`)}`);
282
- }
283
-
284
- // ─── Test ───────────────────────────────────────────────────
285
- const dispatchOk = await spinner("Running test dispatch", async () => {
286
- try {
287
- const res = await fetch(
288
- `https://api.github.com/repos/${project.gitRemote}/actions/workflows/anteater.yml/dispatches`,
289
- {
290
- method: "POST",
291
- headers: {
292
- Authorization: `Bearer ${githubToken}`,
293
- Accept: "application/vnd.github+json",
294
- "X-GitHub-Api-Version": "2022-11-28",
295
- },
296
- body: JSON.stringify({
297
- ref: productionBranch,
298
- inputs: {
299
- requestId: "setup-test",
300
- prompt: "setup verification \u2014 no changes expected",
301
- mode: "prod",
302
- branch: "anteater/setup-test",
303
- baseBranch: productionBranch,
304
- autoMerge: "false",
305
- },
306
- }),
307
- }
308
- );
309
- return res.status === 204;
310
- } catch { return false; }
311
- });
312
-
313
- if (dispatchOk) ok("Pipeline is working");
314
- else warn("Test dispatch failed \u2014 check GitHub Actions");
315
-
316
- // ─── Done! ──────────────────────────────────────────────────
317
- closeRL();
318
- blank();
319
- console.log(` ${bold(green("\u{1F41C} Anteater is ready."))}`);
320
- blank();
321
- info(`Deploy your app and look for the "${green("Edit this page")}" button.`);
322
- info("Users can modify your app by typing changes in the Anteater bar.");
323
- blank();
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 { bold, green, red, dim, ok, fail, heading, blank } from "./ui.mjs";
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
- // Remove component usage
39
- content = content.replace(/^\s*<AnteaterBar[^]*?\/>\s*\n?/gm, "");
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
- heading("Removing scaffolded files");
164
+ let totalRemoved = 0;
79
165
 
80
- let removed = 0;
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")) removed++;
84
- if (await removeFile(join(cwd, "anteater.config.js"), "anteater.config.js")) removed++;
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/")) removed++;
88
- if (await removeFile(join(cwd, "pages/api/anteater"), "pages/api/anteater/")) removed++;
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")) removed++;
92
- if (await removeFile(join(cwd, "components/anteater-bar-wrapper.js"), "components/anteater-bar-wrapper.js")) removed++;
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")) removed++;
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
- // Unpatch layout
187
+ // ── Layout cleanup ────────────────────────────────────
98
188
  heading("Cleaning up layout");
99
- await unpatchLayout();
189
+ if (await unpatchLayout()) totalRemoved++;
100
190
 
101
- // Remove dependency
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 (removed > 0) {
107
- console.log(` ${green("\u2713")} Anteater uninstalled. Run ${dim("npx next-anteater setup")} to reinstall.`);
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 nothing to remove.")}`);
219
+ console.log(` ${dim("No Anteater files found \u2014 nothing to remove.")}`);
110
220
  }
111
221
  console.log();
112
222
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-anteater",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "AI-powered live editing for your Next.js app",
5
5
  "bin": {
6
6
  "anteater": "bin/setup-anteater.mjs",