create-atlas-agent 0.2.5

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.
Files changed (45) hide show
  1. package/README.md +69 -0
  2. package/index.ts +526 -0
  3. package/package.json +33 -0
  4. package/template/.env.example +49 -0
  5. package/template/Dockerfile +31 -0
  6. package/template/bin/atlas.ts +1092 -0
  7. package/template/bin/enrich.ts +551 -0
  8. package/template/data/.gitkeep +0 -0
  9. package/template/data/demo-sqlite.sql +372 -0
  10. package/template/data/demo.sql +371 -0
  11. package/template/docker-compose.yml +23 -0
  12. package/template/docs/deploy.md +341 -0
  13. package/template/eslint.config.mjs +18 -0
  14. package/template/fly.toml +46 -0
  15. package/template/gitignore +5 -0
  16. package/template/next.config.ts +8 -0
  17. package/template/package.json +55 -0
  18. package/template/postcss.config.mjs +8 -0
  19. package/template/public/.gitkeep +0 -0
  20. package/template/railway.json +13 -0
  21. package/template/render.yaml +19 -0
  22. package/template/semantic/catalog.yml +5 -0
  23. package/template/semantic/entities/.gitkeep +0 -0
  24. package/template/semantic/glossary.yml +6 -0
  25. package/template/semantic/metrics/.gitkeep +0 -0
  26. package/template/src/app/api/chat/route.ts +107 -0
  27. package/template/src/app/api/health/route.ts +97 -0
  28. package/template/src/app/error.tsx +24 -0
  29. package/template/src/app/globals.css +1 -0
  30. package/template/src/app/layout.tsx +19 -0
  31. package/template/src/app/page.tsx +650 -0
  32. package/template/src/global.d.ts +1 -0
  33. package/template/src/lib/agent.ts +112 -0
  34. package/template/src/lib/db/connection.ts +150 -0
  35. package/template/src/lib/providers.ts +63 -0
  36. package/template/src/lib/semantic.ts +53 -0
  37. package/template/src/lib/startup.ts +211 -0
  38. package/template/src/lib/tools/__tests__/sql.test.ts +538 -0
  39. package/template/src/lib/tools/explore-sandbox.ts +189 -0
  40. package/template/src/lib/tools/explore.ts +164 -0
  41. package/template/src/lib/tools/report.ts +33 -0
  42. package/template/src/lib/tools/sql.ts +202 -0
  43. package/template/src/types/vercel-sandbox.d.ts +54 -0
  44. package/template/tsconfig.json +41 -0
  45. package/template/vercel.json +3 -0
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # create-atlas-agent
2
+
3
+ Scaffold a new [Atlas](https://github.com/msywu/data-agent) text-to-SQL agent project.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ bun create atlas-agent my-app
9
+ cd my-app
10
+ bun run dev
11
+ ```
12
+
13
+ The interactive setup asks for your database (SQLite or PostgreSQL), LLM provider, and API key. SQLite is the default — zero setup, no Docker required.
14
+
15
+ ### Non-interactive mode
16
+
17
+ Skip all prompts with sensible defaults (SQLite + Anthropic + demo data):
18
+
19
+ ```bash
20
+ bun create atlas-agent my-app --defaults
21
+ ```
22
+
23
+ ## Requirements
24
+
25
+ - [Bun](https://bun.sh/) v1.3+
26
+ - An LLM API key (Anthropic, OpenAI, or another supported provider)
27
+
28
+ ## What you get
29
+
30
+ A self-contained Next.js 16 project with:
31
+
32
+ - Text-to-SQL agent with multi-layer SQL validation
33
+ - Auto-generated semantic layer (YAML) from your database schema
34
+ - Chat UI with streaming responses
35
+ - Docker, Railway, Fly.io, Render, and Vercel deployment configs
36
+ - SQLite (default) or PostgreSQL support
37
+
38
+ ## Local development
39
+
40
+ To test changes to the scaffolding CLI from the repo root:
41
+
42
+ ```bash
43
+ # Refresh template files from the repo
44
+ cd create-atlas && bun run prepublishOnly && cd ..
45
+
46
+ # Test interactive mode
47
+ bun create-atlas/index.ts test-app
48
+
49
+ # Test non-interactive mode
50
+ bun create-atlas/index.ts test-app --defaults
51
+ ```
52
+
53
+ ## Publishing
54
+
55
+ ```bash
56
+ cd create-atlas
57
+ bun run prepublishOnly # Copies src/, bin/, data/, docs/deploy.md into template/
58
+ bun publish --access public
59
+ ```
60
+
61
+ After publishing, verify from the registry:
62
+
63
+ ```bash
64
+ bun create atlas-agent verify-test --defaults
65
+ ```
66
+
67
+ ## License
68
+
69
+ MIT
package/index.ts ADDED
@@ -0,0 +1,526 @@
1
+ #!/usr/bin/env bun
2
+ import * as p from "@clack/prompts";
3
+ import pc from "picocolors";
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ import { execSync } from "child_process";
7
+
8
+ // Read version from package.json to stay in sync
9
+ const pkg = JSON.parse(
10
+ fs.readFileSync(path.join(import.meta.dir, "package.json"), "utf-8")
11
+ );
12
+ const ATLAS_VERSION: string = pkg.version;
13
+
14
+ // Provider → API key env var mapping
15
+ const PROVIDER_KEY_MAP: Record<string, { envVar: string; placeholder: string }> = {
16
+ anthropic: { envVar: "ANTHROPIC_API_KEY", placeholder: "sk-ant-..." },
17
+ openai: { envVar: "OPENAI_API_KEY", placeholder: "sk-..." },
18
+ bedrock: { envVar: "AWS_ACCESS_KEY_ID", placeholder: "AKIA..." },
19
+ ollama: { envVar: "OLLAMA_BASE_URL", placeholder: "http://localhost:11434" },
20
+ gateway: { envVar: "AI_GATEWAY_API_KEY", placeholder: "vcel_gw_..." },
21
+ };
22
+
23
+ // Default models per provider
24
+ const PROVIDER_DEFAULT_MODEL: Record<string, string> = {
25
+ anthropic: "claude-sonnet-4-6",
26
+ openai: "gpt-4o",
27
+ bedrock: "anthropic.claude-sonnet-4-6-v1",
28
+ ollama: "llama3.1",
29
+ gateway: "anthropic/claude-sonnet-4.6",
30
+ };
31
+
32
+ function copyDirRecursive(src: string, dest: string): void {
33
+ if (!fs.existsSync(dest)) {
34
+ fs.mkdirSync(dest, { recursive: true });
35
+ }
36
+
37
+ const entries = fs.readdirSync(src, { withFileTypes: true });
38
+ for (const entry of entries) {
39
+ const srcPath = path.join(src, entry.name);
40
+ const destPath = path.join(dest, entry.name);
41
+
42
+ if (entry.isDirectory()) {
43
+ copyDirRecursive(srcPath, destPath);
44
+ } else {
45
+ fs.copyFileSync(srcPath, destPath);
46
+ }
47
+ }
48
+ }
49
+
50
+ function bail(message?: string): never {
51
+ p.cancel(message ?? "Setup cancelled.");
52
+ process.exit(1);
53
+ }
54
+
55
+ // Parse --defaults / -y flag for non-interactive mode
56
+ const args = process.argv.slice(2);
57
+ const useDefaults = args.includes("--defaults") || args.includes("-y");
58
+ const positionalArgs = args.filter((a) => !a.startsWith("-"));
59
+
60
+ // Handle --help / -h
61
+ if (args.includes("--help") || args.includes("-h")) {
62
+ console.log(`
63
+ Usage: bun create atlas-agent [project-name] [options]
64
+
65
+ Options:
66
+ --defaults, -y Use all default values (non-interactive)
67
+ --help, -h Show this help message
68
+
69
+ Examples:
70
+ bun create atlas-agent my-app
71
+ bun create atlas-agent my-app --defaults
72
+ `);
73
+ process.exit(0);
74
+ }
75
+
76
+ // Reject unknown flags
77
+ const knownFlags = new Set(["--defaults", "-y", "--help", "-h"]);
78
+ const unknownFlags = args.filter((a) => a.startsWith("-") && !knownFlags.has(a));
79
+ if (unknownFlags.length > 0) {
80
+ console.error(`Unknown flag(s): ${unknownFlags.join(", ")}`);
81
+ console.error("Run with --help for usage information.");
82
+ process.exit(1);
83
+ }
84
+
85
+ // Helpers to deduplicate useDefaults branches
86
+ async function selectOrDefault<T extends string>(opts: {
87
+ label: string;
88
+ message: string;
89
+ options: { value: T; label: string; hint?: string }[];
90
+ initialValue: T;
91
+ defaultDisplay: string;
92
+ }): Promise<T> {
93
+ if (useDefaults) {
94
+ p.log.info(`${opts.label}: ${pc.cyan(opts.defaultDisplay)} ${pc.dim("(default)")}`);
95
+ return opts.initialValue;
96
+ }
97
+ const result = await p.select({
98
+ message: opts.message,
99
+ options: opts.options,
100
+ initialValue: opts.initialValue,
101
+ });
102
+ if (p.isCancel(result)) bail();
103
+ return result as T;
104
+ }
105
+
106
+ async function confirmOrDefault(opts: {
107
+ label: string;
108
+ message: string;
109
+ initialValue: boolean;
110
+ defaultDisplay: string;
111
+ }): Promise<boolean> {
112
+ if (useDefaults) {
113
+ p.log.info(`${opts.label}: ${pc.cyan(opts.defaultDisplay)} ${pc.dim("(default)")}`);
114
+ return opts.initialValue;
115
+ }
116
+ const result = await p.confirm({
117
+ message: opts.message,
118
+ initialValue: opts.initialValue,
119
+ });
120
+ if (p.isCancel(result)) bail();
121
+ return result as boolean;
122
+ }
123
+
124
+ async function main() {
125
+ console.log("");
126
+ p.intro(
127
+ `${pc.bgCyan(pc.black(" create-atlas-agent "))} ${pc.dim(`v${ATLAS_VERSION}`)}`
128
+ );
129
+
130
+ // ── 1. Project name ──────────────────────────────────────────────
131
+ let projectName: string;
132
+
133
+ if (positionalArgs[0]) {
134
+ projectName = positionalArgs[0];
135
+ p.log.info(`Project name: ${pc.cyan(projectName)}`);
136
+ } else if (useDefaults) {
137
+ projectName = "my-atlas-app";
138
+ p.log.info(`Project name: ${pc.cyan(projectName)} ${pc.dim("(default)")}`);
139
+ } else {
140
+ const result = await p.text({
141
+ message: "What is your project name?",
142
+ placeholder: "my-atlas-app",
143
+ defaultValue: "my-atlas-app",
144
+ validate(value) {
145
+ if (!value.trim()) return "Project name is required.";
146
+ if (!/^[a-z0-9._-]+$/i.test(value))
147
+ return "Project name can only contain letters, numbers, dots, hyphens, and underscores.";
148
+ },
149
+ });
150
+ if (p.isCancel(result)) bail();
151
+ projectName = result as string;
152
+ }
153
+
154
+ const targetDir = path.resolve(process.cwd(), projectName);
155
+
156
+ if (fs.existsSync(targetDir)) {
157
+ if (useDefaults) {
158
+ bail(`Directory ${projectName} already exists.`);
159
+ }
160
+ const overwrite = await p.confirm({
161
+ message: `Directory ${pc.yellow(projectName)} already exists. Overwrite?`,
162
+ initialValue: false,
163
+ });
164
+ if (p.isCancel(overwrite) || !overwrite) bail("Directory already exists.");
165
+ }
166
+
167
+ // ── 2. Database choice ────────────────────────────────────────────
168
+ const dbChoice = await selectOrDefault({
169
+ label: "Database",
170
+ message: "Which database?",
171
+ options: [
172
+ { value: "sqlite", label: "SQLite", hint: "Instant start, no setup (default)" },
173
+ { value: "postgres", label: "PostgreSQL", hint: "Bring your connection string" },
174
+ ],
175
+ initialValue: "sqlite",
176
+ defaultDisplay: "SQLite",
177
+ });
178
+
179
+ // ── 3. PostgreSQL connection string (if postgres) ─────────────────
180
+ let databaseUrl: string;
181
+ if (dbChoice === "postgres") {
182
+ if (useDefaults) {
183
+ databaseUrl = "postgresql://atlas:atlas@localhost:5432/atlas";
184
+ p.log.info(`Database URL: ${pc.cyan(databaseUrl)} ${pc.dim("(default)")}`);
185
+ } else {
186
+ const connResult = await p.text({
187
+ message: "PostgreSQL connection string:",
188
+ placeholder: "postgresql://atlas:atlas@localhost:5432/atlas",
189
+ defaultValue: "postgresql://atlas:atlas@localhost:5432/atlas",
190
+ validate(value) {
191
+ if (!value.trim()) return "Database URL is required.";
192
+ if (!value.startsWith("postgresql://") && !value.startsWith("postgres://"))
193
+ return "Must be a PostgreSQL connection string (postgresql://...).";
194
+ },
195
+ });
196
+ if (p.isCancel(connResult)) bail();
197
+ databaseUrl = connResult as string;
198
+ }
199
+ } else {
200
+ databaseUrl = "file:./data/atlas.db";
201
+ }
202
+
203
+ // ── 4. LLM Provider ──────────────────────────────────────────────
204
+ const provider = await selectOrDefault({
205
+ label: "LLM provider",
206
+ message: "Which LLM provider?",
207
+ options: [
208
+ { value: "anthropic", label: "Anthropic", hint: "Claude (default)" },
209
+ { value: "openai", label: "OpenAI", hint: "GPT-4o" },
210
+ { value: "bedrock", label: "AWS Bedrock", hint: "Region-specific" },
211
+ { value: "ollama", label: "Ollama", hint: "Local models" },
212
+ { value: "gateway", label: "Vercel AI Gateway", hint: "One key, hundreds of models" },
213
+ ],
214
+ initialValue: "anthropic",
215
+ defaultDisplay: "Anthropic",
216
+ });
217
+
218
+ // ── 5. API Key ────────────────────────────────────────────────────
219
+ const keyInfo = PROVIDER_KEY_MAP[provider];
220
+ let apiKey = "";
221
+
222
+ if (useDefaults) {
223
+ apiKey = keyInfo.placeholder;
224
+ p.log.warn(
225
+ `${keyInfo.envVar} set to placeholder value. Edit .env and set a real API key before running.`
226
+ );
227
+ } else if (provider === "bedrock") {
228
+ // Bedrock needs multiple AWS credentials
229
+ const accessKeyId = await p.text({
230
+ message: `Enter your ${pc.cyan("AWS_ACCESS_KEY_ID")}:`,
231
+ placeholder: "AKIA...",
232
+ validate(value) {
233
+ if (!value.trim()) return "AWS Access Key ID is required.";
234
+ },
235
+ });
236
+ if (p.isCancel(accessKeyId)) bail();
237
+
238
+ const secretAccessKey = await p.text({
239
+ message: `Enter your ${pc.cyan("AWS_SECRET_ACCESS_KEY")}:`,
240
+ placeholder: "wJalr...",
241
+ validate(value) {
242
+ if (!value.trim()) return "AWS Secret Access Key is required.";
243
+ },
244
+ });
245
+ if (p.isCancel(secretAccessKey)) bail();
246
+
247
+ const awsRegion = await p.text({
248
+ message: `Enter your ${pc.cyan("AWS_REGION")}:`,
249
+ placeholder: "us-east-1",
250
+ defaultValue: "us-east-1",
251
+ });
252
+ if (p.isCancel(awsRegion)) bail();
253
+
254
+ // Store all three as a composite — we'll unpack when writing .env
255
+ apiKey = `AWS_ACCESS_KEY_ID=${accessKeyId}\nAWS_SECRET_ACCESS_KEY=${secretAccessKey}\nAWS_REGION=${awsRegion}`;
256
+ } else {
257
+ const keyPrompt = await p.text({
258
+ message: `Enter your ${pc.cyan(keyInfo.envVar)}:`,
259
+ placeholder: keyInfo.placeholder,
260
+ validate(value) {
261
+ if (provider !== "ollama" && !value.trim())
262
+ return `${keyInfo.envVar} is required.`;
263
+ },
264
+ });
265
+ if (p.isCancel(keyPrompt)) bail();
266
+ apiKey = (keyPrompt as string) || keyInfo.placeholder;
267
+ }
268
+
269
+ // ── 6. Model override ────────────────────────────────────────────
270
+ const defaultModel = PROVIDER_DEFAULT_MODEL[provider];
271
+ let modelOverride = "";
272
+
273
+ if (!useDefaults) {
274
+ const result = await p.text({
275
+ message: `Model override? ${pc.dim(`(default: ${defaultModel})`)}`,
276
+ placeholder: defaultModel,
277
+ defaultValue: "",
278
+ });
279
+ if (p.isCancel(result)) bail();
280
+ modelOverride = result as string;
281
+ }
282
+
283
+ // ── 7. Semantic layer / demo data ─────────────────────────────────
284
+ let loadDemo = false;
285
+ let generateSemantic = false;
286
+
287
+ if (dbChoice === "sqlite") {
288
+ loadDemo = await confirmOrDefault({
289
+ label: "Demo data",
290
+ message: "Load demo dataset? (50 companies, ~200 people, 80 accounts)",
291
+ initialValue: true,
292
+ defaultDisplay: "yes",
293
+ });
294
+ } else {
295
+ generateSemantic = await confirmOrDefault({
296
+ label: "Generate semantic layer",
297
+ message: "Generate semantic layer now? (requires database access)",
298
+ initialValue: false,
299
+ defaultDisplay: "no",
300
+ });
301
+ }
302
+
303
+ // ── Pre-flight checks ───────────────────────────────────────────
304
+ try {
305
+ const bunVersion = execSync("bun --version", { encoding: "utf-8", stdio: "pipe" }).trim();
306
+ const major = parseInt(bunVersion.split(".")[0], 10);
307
+ if (isNaN(major) || major < 1) {
308
+ p.log.warn(`Bun ${bunVersion} detected. Atlas requires Bun 1.0+.`);
309
+ }
310
+ } catch (err) {
311
+ p.log.warn(`Could not detect bun version: ${err instanceof Error ? err.message : String(err)}`);
312
+ }
313
+
314
+ // ── DB connectivity check (Postgres only) ────────────────────────
315
+ if (generateSemantic && dbChoice === "postgres") {
316
+ const connSpinner = p.spinner();
317
+ connSpinner.start("Checking database connectivity...");
318
+ try {
319
+ execSync(
320
+ `bun -e "const{Pool}=require('pg');const p=new Pool({connectionString:process.env.DATABASE_URL,connectionTimeoutMillis:5000});const c=await p.connect();c.release();await p.end()"`,
321
+ { stdio: "pipe", timeout: 15_000, env: { ...process.env, DATABASE_URL: databaseUrl } }
322
+ );
323
+ connSpinner.stop("Database is reachable.");
324
+ } catch (err) {
325
+ connSpinner.stop("Could not connect to database.");
326
+ if (err && typeof err === "object" && "stderr" in err) {
327
+ const stderr = String((err as { stderr: unknown }).stderr).trim();
328
+ if (stderr) p.log.warn(stderr);
329
+ }
330
+ const proceed = await p.confirm({
331
+ message: "Database is not reachable. Try generating semantic layer anyway?",
332
+ initialValue: false,
333
+ });
334
+ if (p.isCancel(proceed) || !proceed) {
335
+ generateSemantic = false;
336
+ p.log.info("Skipping. Run 'bun run atlas -- init' later when the DB is available.");
337
+ }
338
+ }
339
+ }
340
+
341
+ // ── Scaffold ──────────────────────────────────────────────────────
342
+ const s = p.spinner();
343
+
344
+ // Step 1: Copy template (self-contained — includes src/, bin/, data/)
345
+ s.start("Copying project files...");
346
+ const templateDir = path.join(import.meta.dir, "template");
347
+
348
+ if (!fs.existsSync(templateDir)) {
349
+ s.stop("Template directory not found.");
350
+ bail("Could not find template/ directory. Is the package installed correctly?");
351
+ }
352
+
353
+ try {
354
+ copyDirRecursive(templateDir, targetDir);
355
+ } catch (err) {
356
+ s.stop("Failed to copy project files.");
357
+ p.log.error(`Copy failed: ${err instanceof Error ? err.message : String(err)}`);
358
+ if (fs.existsSync(targetDir)) {
359
+ p.log.warn(
360
+ `Partial directory may remain at ${pc.yellow(targetDir)}. Remove it manually before retrying.`
361
+ );
362
+ }
363
+ process.exit(1);
364
+ }
365
+
366
+ // Rename gitignore → .gitignore (npm/bun strips .gitignore from published tarballs)
367
+ const gitignoreSrc = path.join(targetDir, "gitignore");
368
+ const gitignoreDest = path.join(targetDir, ".gitignore");
369
+ if (fs.existsSync(gitignoreSrc)) {
370
+ try {
371
+ fs.renameSync(gitignoreSrc, gitignoreDest);
372
+ } catch (err) {
373
+ p.log.warn(
374
+ `Failed to rename gitignore to .gitignore: ${err instanceof Error ? err.message : String(err)}`
375
+ );
376
+ }
377
+ } else if (!fs.existsSync(gitignoreDest)) {
378
+ p.log.warn(
379
+ "No .gitignore found in template. Your project may accidentally commit secrets (.env). Add one manually."
380
+ );
381
+ }
382
+
383
+ // Replace %PROJECT_NAME% in templated files
384
+ const filesToReplace = ["package.json", "fly.toml", "render.yaml"];
385
+ for (const file of filesToReplace) {
386
+ const filePath = path.join(targetDir, file);
387
+ if (!fs.existsSync(filePath)) {
388
+ s.stop(`Template file missing: ${file}`);
389
+ bail(`${file} was not found after copying the template. Is the package installed correctly?`);
390
+ }
391
+ const content = fs.readFileSync(filePath, "utf-8");
392
+ const replaced = content.replace(/%PROJECT_NAME%/g, projectName);
393
+ if (content === replaced && content.includes("PROJECT_NAME")) {
394
+ p.log.warn(`${file} may contain unreplaced template variables.`);
395
+ }
396
+ fs.writeFileSync(filePath, replaced);
397
+ }
398
+
399
+ s.stop("Project files copied.");
400
+
401
+ // Step 2: Write .env
402
+ s.start("Writing environment configuration...");
403
+
404
+ let envContent = `# Generated by create-atlas-agent v${ATLAS_VERSION}\n\n`;
405
+
406
+ envContent += `# Database\n`;
407
+ if (dbChoice === "sqlite") {
408
+ envContent += `# SQLite — zero setup, data stored locally\n`;
409
+ envContent += `DATABASE_URL=${databaseUrl}\n`;
410
+ envContent += `# To switch to PostgreSQL later:\n`;
411
+ envContent += `# DATABASE_URL=postgresql://user:pass@host:5432/dbname\n`;
412
+ } else {
413
+ envContent += `DATABASE_URL=${databaseUrl}\n`;
414
+ }
415
+
416
+ envContent += `\n# LLM Provider\n`;
417
+ envContent += `ATLAS_PROVIDER=${provider}\n`;
418
+
419
+ if (provider === "bedrock") {
420
+ envContent += `${apiKey}\n`;
421
+ } else {
422
+ envContent += `${keyInfo.envVar}=${apiKey}\n`;
423
+ }
424
+
425
+ if (modelOverride) {
426
+ envContent += `\n# Model override\n`;
427
+ envContent += `ATLAS_MODEL=${modelOverride}\n`;
428
+ }
429
+
430
+ envContent += `\n# Security (defaults)\n`;
431
+ envContent += `ATLAS_TABLE_WHITELIST=true\n`;
432
+ envContent += `ATLAS_ROW_LIMIT=1000\n`;
433
+ envContent += `ATLAS_QUERY_TIMEOUT=30000\n`;
434
+
435
+ fs.writeFileSync(path.join(targetDir, ".env"), envContent);
436
+ s.stop("Environment file written.");
437
+
438
+ // Step 3: Install dependencies
439
+ s.start("Installing dependencies with bun...");
440
+ try {
441
+ execSync("bun install", {
442
+ cwd: targetDir,
443
+ stdio: "pipe",
444
+ timeout: 120_000,
445
+ });
446
+ s.stop("Dependencies installed.");
447
+ } catch (err) {
448
+ s.stop("Failed to install dependencies.");
449
+ p.log.warn(
450
+ `Could not run ${pc.cyan("bun install")}: ${err instanceof Error ? err.message : String(err)}`
451
+ );
452
+ p.log.warn(`Run it manually in ${pc.yellow(projectName)}/`);
453
+ }
454
+
455
+ // Step 4: Load demo data + generate semantic layer (SQLite)
456
+ if (loadDemo && dbChoice === "sqlite") {
457
+ s.start("Loading demo data and generating semantic layer...");
458
+ try {
459
+ execSync("bun run atlas -- init --demo", {
460
+ cwd: targetDir,
461
+ stdio: "pipe",
462
+ timeout: 60_000,
463
+ env: { ...process.env, DATABASE_URL: databaseUrl },
464
+ });
465
+ s.stop("Demo data loaded and semantic layer generated.");
466
+ } catch (err) {
467
+ s.stop("Failed to load demo data.");
468
+ let detail = err instanceof Error ? err.message : String(err);
469
+ if (err && typeof err === "object" && "stderr" in err) {
470
+ const stderr = String((err as { stderr: unknown }).stderr).trim();
471
+ if (stderr) detail = stderr;
472
+ }
473
+ p.log.warn(`Demo seeding failed: ${detail}`);
474
+ p.log.warn(
475
+ `Run ${pc.cyan("bun run atlas -- init --demo")} manually after resolving the issue.`
476
+ );
477
+ }
478
+ }
479
+
480
+ // Step 4b: Generate semantic layer (Postgres)
481
+ if (generateSemantic && dbChoice === "postgres") {
482
+ s.start("Generating semantic layer from database...");
483
+ try {
484
+ execSync("bun run atlas -- init --enrich", {
485
+ cwd: targetDir,
486
+ stdio: "pipe",
487
+ timeout: 300_000,
488
+ env: { ...process.env, DATABASE_URL: databaseUrl },
489
+ });
490
+ s.stop("Semantic layer generated.");
491
+ } catch (err) {
492
+ s.stop("Failed to generate semantic layer.");
493
+ p.log.warn(
494
+ `Semantic layer generation failed: ${err instanceof Error ? err.message : String(err)}`
495
+ );
496
+ p.log.warn(
497
+ `Run ${pc.cyan("bun run atlas -- init --enrich")} manually after resolving the issue.`
498
+ );
499
+ }
500
+ }
501
+
502
+ // ── Success ───────────────────────────────────────────────────────
503
+ const nextSteps = [`cd ${projectName}`, "bun run dev"];
504
+
505
+ let noteBody =
506
+ nextSteps.map((step) => pc.cyan(step)).join("\n") +
507
+ "\n\n" +
508
+ pc.dim("See docs/deploy.md for deployment options (Railway, Fly.io, Docker, Vercel).");
509
+ if (useDefaults) {
510
+ noteBody += "\n" + pc.yellow("Note: .env contains a placeholder API key. Edit it before running.");
511
+ }
512
+ if (dbChoice === "sqlite") {
513
+ noteBody += "\n" + pc.dim("Note: SQLite data is ephemeral in containers. Use PostgreSQL for production.");
514
+ }
515
+
516
+ p.note(noteBody, "Next steps");
517
+
518
+ p.outro(
519
+ `${pc.green("Done!")} Your Atlas project is ready at ${pc.cyan(`./${projectName}`)}`
520
+ );
521
+ }
522
+
523
+ main().catch((err) => {
524
+ p.log.error(err instanceof Error ? err.message : String(err));
525
+ process.exit(1);
526
+ });
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "create-atlas-agent",
3
+ "version": "0.2.5",
4
+ "description": "Create a new Atlas text-to-SQL agent project",
5
+ "bin": {
6
+ "create-atlas-agent": "./index.ts"
7
+ },
8
+ "scripts": {
9
+ "prepublishOnly": "test -f ../docs/deploy.md || (echo 'ERROR: docs/deploy.md not found.' && exit 1) && test -f ../data/demo-sqlite.sql || (echo 'ERROR: data/demo-sqlite.sql not found.' && exit 1) && test -f ./template/gitignore || (echo 'ERROR: template/gitignore not found. .gitignore will be missing from scaffolded projects.' && exit 1) && rm -rf ./template/src ./template/bin ./template/data ./template/docs && cp -r ../src ./template/src && cp -r ../bin ./template/bin && cp -r ../data ./template/data && mkdir -p ./template/docs && cp ../docs/deploy.md ./template/docs/deploy.md && mkdir -p ./template/public && touch ./template/public/.gitkeep"
10
+ },
11
+ "keywords": [
12
+ "atlas",
13
+ "text-to-sql",
14
+ "data-analyst",
15
+ "agent",
16
+ "semantic-layer",
17
+ "bun",
18
+ "nextjs"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/msywu/data-agent"
23
+ },
24
+ "engines": {
25
+ "bun": ">=1.3"
26
+ },
27
+ "dependencies": {
28
+ "@clack/prompts": "^0.10.0",
29
+ "picocolors": "^1.1.0"
30
+ },
31
+ "files": ["index.ts", "template/", "README.md"],
32
+ "license": "MIT"
33
+ }
@@ -0,0 +1,49 @@
1
+ # === Database ===
2
+ # SQLite (default — zero setup, no Docker):
3
+ # DATABASE_URL=file:./data/atlas.db
4
+
5
+ # PostgreSQL (local dev with Docker — bun run db:up):
6
+ # DATABASE_URL=postgresql://atlas:atlas@localhost:5432/atlas
7
+
8
+ # Production PostgreSQL:
9
+ # DATABASE_URL=postgresql://user:pass@your-host:5432/yourdb
10
+
11
+ # === LLM Provider (pick one) ===
12
+ # ATLAS_PROVIDER=anthropic
13
+ # ANTHROPIC_API_KEY=sk-ant-...
14
+
15
+ # ATLAS_PROVIDER=openai
16
+ # OPENAI_API_KEY=sk-...
17
+
18
+ # ATLAS_PROVIDER=bedrock
19
+ # AWS_REGION=us-east-1
20
+ # AWS_ACCESS_KEY_ID=...
21
+ # AWS_SECRET_ACCESS_KEY=...
22
+
23
+ # ATLAS_PROVIDER=ollama
24
+ # OLLAMA_BASE_URL=http://localhost:11434
25
+
26
+ # ATLAS_PROVIDER=gateway
27
+ # AI_GATEWAY_API_KEY=...
28
+
29
+ # === Model (optional, provider-specific default used if omitted) ===
30
+ # ATLAS_MODEL=claude-sonnet-4-6
31
+
32
+ # === Security ===
33
+ # Non-SELECT SQL (INSERT, UPDATE, DELETE, DROP, etc.) is always rejected — no toggle.
34
+ # ATLAS_TABLE_WHITELIST=true # Default: true — only allow tables in semantic layer
35
+ # ATLAS_ROW_LIMIT=1000 # Default: 1000
36
+ # ATLAS_QUERY_TIMEOUT=30000 # Default: 30s in milliseconds (PostgreSQL only; ignored for SQLite)
37
+
38
+ # === Production Deployment ===
39
+ # Required for production (Railway, Fly.io, etc.):
40
+ # ATLAS_PROVIDER + its API key (e.g. ANTHROPIC_API_KEY)
41
+ # DATABASE_URL=postgresql://user:pass@host:5432/dbname
42
+ # (SQLite is also supported for single-server deployments: DATABASE_URL=file:/data/atlas.db)
43
+ #
44
+ # Optional (defaults are fine for most deployments):
45
+ # ATLAS_MODEL, ATLAS_ROW_LIMIT, ATLAS_QUERY_TIMEOUT
46
+ # PORT (set automatically by most platforms)
47
+
48
+ # === Runtime ===
49
+ # ATLAS_RUNTIME=vercel # Force Vercel Sandbox for explore tool (auto-detected on Vercel)