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.
- package/README.md +69 -0
- package/index.ts +526 -0
- package/package.json +33 -0
- package/template/.env.example +49 -0
- package/template/Dockerfile +31 -0
- package/template/bin/atlas.ts +1092 -0
- package/template/bin/enrich.ts +551 -0
- package/template/data/.gitkeep +0 -0
- package/template/data/demo-sqlite.sql +372 -0
- package/template/data/demo.sql +371 -0
- package/template/docker-compose.yml +23 -0
- package/template/docs/deploy.md +341 -0
- package/template/eslint.config.mjs +18 -0
- package/template/fly.toml +46 -0
- package/template/gitignore +5 -0
- package/template/next.config.ts +8 -0
- package/template/package.json +55 -0
- package/template/postcss.config.mjs +8 -0
- package/template/public/.gitkeep +0 -0
- package/template/railway.json +13 -0
- package/template/render.yaml +19 -0
- package/template/semantic/catalog.yml +5 -0
- package/template/semantic/entities/.gitkeep +0 -0
- package/template/semantic/glossary.yml +6 -0
- package/template/semantic/metrics/.gitkeep +0 -0
- package/template/src/app/api/chat/route.ts +107 -0
- package/template/src/app/api/health/route.ts +97 -0
- package/template/src/app/error.tsx +24 -0
- package/template/src/app/globals.css +1 -0
- package/template/src/app/layout.tsx +19 -0
- package/template/src/app/page.tsx +650 -0
- package/template/src/global.d.ts +1 -0
- package/template/src/lib/agent.ts +112 -0
- package/template/src/lib/db/connection.ts +150 -0
- package/template/src/lib/providers.ts +63 -0
- package/template/src/lib/semantic.ts +53 -0
- package/template/src/lib/startup.ts +211 -0
- package/template/src/lib/tools/__tests__/sql.test.ts +538 -0
- package/template/src/lib/tools/explore-sandbox.ts +189 -0
- package/template/src/lib/tools/explore.ts +164 -0
- package/template/src/lib/tools/report.ts +33 -0
- package/template/src/lib/tools/sql.ts +202 -0
- package/template/src/types/vercel-sandbox.d.ts +54 -0
- package/template/tsconfig.json +41 -0
- 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)
|