@tellet/create 0.9.0 → 0.12.0

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.
@@ -0,0 +1,402 @@
1
+ import * as p from "@clack/prompts";
2
+ import chalk from "chalk";
3
+ import { generateAgents } from "./ai/generate.js";
4
+ import { scaffoldProject } from "./scaffold/project.js";
5
+ import { setupSupabase } from "./setup/supabase.js";
6
+ import { installDependencies } from "./setup/deps.js";
7
+ export async function main() {
8
+ console.clear();
9
+ console.log();
10
+ console.log(chalk.bold(` ${chalk.white("tel")}${chalk.yellow("let")} ${chalk.dim("— Build Your AI Company")}`));
11
+ console.log();
12
+ p.intro(chalk.bgHex("#8b5cf6").white(" @tellet/create "));
13
+ // Step 0: New or Connect
14
+ const modeChoice = await p.select({
15
+ message: "What would you like to do?",
16
+ options: [
17
+ {
18
+ value: "new",
19
+ label: "New",
20
+ hint: "Build a new AI company from scratch",
21
+ },
22
+ {
23
+ value: "connect",
24
+ label: "Connect",
25
+ hint: "Add AI agents to your existing business",
26
+ },
27
+ ],
28
+ });
29
+ if (p.isCancel(modeChoice)) {
30
+ p.cancel("Setup cancelled.");
31
+ process.exit(0);
32
+ }
33
+ const mode = modeChoice;
34
+ // Step 1: Company info
35
+ const company = await p.group({
36
+ name: () => p.text({
37
+ message: "What's your company name?",
38
+ placeholder: "Sunny Coffee",
39
+ validate: (v) => (!v ? "Company name is required" : undefined),
40
+ }),
41
+ description: () => p.text({
42
+ message: "Describe your business (what you do, who your customers are, what help you need):",
43
+ placeholder: "We sell specialty coffee subscriptions. Customers are coffee enthusiasts aged 25-45...",
44
+ validate: (v) => !v || v.length < 20
45
+ ? "Please provide at least a few sentences"
46
+ : undefined,
47
+ }),
48
+ }, {
49
+ onCancel: () => {
50
+ p.cancel("Setup cancelled.");
51
+ process.exit(0);
52
+ },
53
+ });
54
+ // Step 2: Deployment tier
55
+ const tierChoice = await p.select({
56
+ message: "Deployment mode:",
57
+ options: [
58
+ {
59
+ value: "quickstart",
60
+ label: "Quick Start",
61
+ hint: "Vercel + Supabase — free, instant deploy",
62
+ },
63
+ {
64
+ value: "cloud",
65
+ label: "Cloud",
66
+ hint: "Railway / Render / Fly.io — Docker, $5-20/mo",
67
+ },
68
+ {
69
+ value: "enterprise",
70
+ label: "Enterprise",
71
+ hint: "AWS CDK — auto-provision Lambda + RDS + CloudFront",
72
+ },
73
+ ],
74
+ });
75
+ if (p.isCancel(tierChoice)) {
76
+ p.cancel("Setup cancelled.");
77
+ process.exit(0);
78
+ }
79
+ const tier = tierChoice;
80
+ // Step 3: Choose AI provider
81
+ const providerChoice = await p.select({
82
+ message: "Choose your AI provider:",
83
+ options: [
84
+ {
85
+ value: "anthropic",
86
+ label: "Anthropic (Claude)",
87
+ hint: "recommended — also powers the Orchestrator",
88
+ },
89
+ {
90
+ value: "openai",
91
+ label: "OpenAI (GPT)",
92
+ hint: "uses gpt-4.1 for generation",
93
+ },
94
+ ],
95
+ });
96
+ if (p.isCancel(providerChoice)) {
97
+ p.cancel("Setup cancelled.");
98
+ process.exit(0);
99
+ }
100
+ const provider = providerChoice;
101
+ // Step 4: Get API key
102
+ const envKey = provider === "anthropic"
103
+ ? process.env.ANTHROPIC_API_KEY
104
+ : process.env.OPENAI_API_KEY;
105
+ const envName = provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
106
+ const keyPrefix = provider === "anthropic" ? "sk-ant-" : "sk-";
107
+ const keyPlaceholder = provider === "anthropic" ? "sk-ant-..." : "sk-...";
108
+ let apiKey = envKey || "";
109
+ if (!apiKey) {
110
+ const keyInput = await p.text({
111
+ message: `Your ${provider === "anthropic" ? "Anthropic" : "OpenAI"} API key:`,
112
+ placeholder: keyPlaceholder,
113
+ validate: (v) => !v || !v.startsWith(keyPrefix)
114
+ ? `Please enter a valid key (starts with ${keyPrefix})`
115
+ : undefined,
116
+ });
117
+ if (p.isCancel(keyInput)) {
118
+ p.cancel("Setup cancelled.");
119
+ process.exit(0);
120
+ }
121
+ apiKey = keyInput;
122
+ process.env[envName] = apiKey;
123
+ }
124
+ else {
125
+ p.log.info(chalk.dim(`Using ${envName} from environment.`));
126
+ }
127
+ // Orchestrator always needs Anthropic
128
+ let anthropicKey = "";
129
+ if (provider === "anthropic") {
130
+ anthropicKey = apiKey;
131
+ }
132
+ else {
133
+ const existingKey = process.env.ANTHROPIC_API_KEY || "";
134
+ if (existingKey) {
135
+ anthropicKey = existingKey;
136
+ p.log.info(chalk.dim("Using ANTHROPIC_API_KEY for Orchestrator."));
137
+ }
138
+ else {
139
+ p.log.info(chalk.dim("The Orchestrator requires an Anthropic API key (Claude tool use)."));
140
+ const orchKeyInput = await p.text({
141
+ message: "Anthropic API key for Orchestrator:",
142
+ placeholder: "sk-ant-...",
143
+ validate: (v) => !v || !v.startsWith("sk-ant-")
144
+ ? "Please enter a valid Anthropic key (starts with sk-ant-)"
145
+ : undefined,
146
+ });
147
+ if (p.isCancel(orchKeyInput)) {
148
+ p.cancel("Setup cancelled.");
149
+ process.exit(0);
150
+ }
151
+ anthropicKey = orchKeyInput;
152
+ }
153
+ }
154
+ // Step 5: Generate agents + site content
155
+ const s = p.spinner();
156
+ s.start("Generating your AI team and website...");
157
+ let agents;
158
+ try {
159
+ agents = await generateAgents(company.name, company.description, provider);
160
+ s.stop("Your AI team is ready!");
161
+ }
162
+ catch (err) {
163
+ s.stop("Failed to generate agents.");
164
+ p.log.error(err instanceof Error ? err.message : "Check your API key");
165
+ p.cancel("Setup failed.");
166
+ process.exit(1);
167
+ }
168
+ // Step 6: Show agents + site preview
169
+ p.log.info(chalk.bold("Meet your team:"));
170
+ console.log();
171
+ for (const agent of agents.agents) {
172
+ console.log(` ${chalk.hex("#8b5cf6").bold(agent.name)} ${chalk.dim(`(${agent.role})`)}`);
173
+ console.log(` ${chalk.dim(agent.description)}`);
174
+ console.log();
175
+ }
176
+ if (agents.site && mode === "new") {
177
+ p.log.info(chalk.bold("Your website:"));
178
+ console.log(` ${chalk.hex("#f59e0b")(agents.site.tagline)}`);
179
+ console.log(` ${chalk.dim(agents.site.subtitle)}`);
180
+ console.log();
181
+ }
182
+ const confirm = await p.confirm({
183
+ message: "Looks good?",
184
+ initialValue: true,
185
+ });
186
+ if (p.isCancel(confirm) || !confirm) {
187
+ p.cancel("Setup cancelled.");
188
+ process.exit(0);
189
+ }
190
+ // Step 7a: Connect mode — website URL for KB
191
+ let websiteUrl = "";
192
+ if (mode === "connect") {
193
+ const urlInput = await p.text({
194
+ message: "Your existing website URL (for Knowledge Base crawling):",
195
+ placeholder: "https://my-business.com",
196
+ });
197
+ if (!p.isCancel(urlInput) && urlInput) {
198
+ websiteUrl = urlInput;
199
+ }
200
+ }
201
+ // Step 7b: Infrastructure setup (tier-dependent)
202
+ let supabaseUrl = "";
203
+ let supabaseKey = "";
204
+ let useAutomatedSupabase = false;
205
+ if (tier === "quickstart") {
206
+ useAutomatedSupabase = true;
207
+ }
208
+ else if (tier === "cloud") {
209
+ p.log.info(chalk.dim("Cloud mode: PostgreSQL runs in Docker. No Supabase needed."));
210
+ }
211
+ else {
212
+ p.log.info(`${chalk.bold("AWS Enterprise setup")}\n` +
213
+ ` ${chalk.dim("Requires:")} AWS CLI configured + CDK bootstrapped\n` +
214
+ ` ${chalk.dim("Cost:")} ~$5-15/mo (Lambda + RDS free tier)`);
215
+ }
216
+ // Step 8: Scaffold project
217
+ s.start("Creating your project...");
218
+ const slug = company.name
219
+ .toLowerCase()
220
+ .replace(/[^a-z0-9]+/g, "-")
221
+ .replace(/^-|-$/g, "");
222
+ try {
223
+ const projectDir = await scaffoldProject({
224
+ company: {
225
+ name: company.name,
226
+ description: company.description,
227
+ industry: agents.industry,
228
+ },
229
+ agents: agents.agents,
230
+ site: agents.site,
231
+ provider,
232
+ tier,
233
+ mode,
234
+ websiteUrl,
235
+ infra: {
236
+ anthropicKey,
237
+ openaiKey: provider === "openai" ? apiKey : undefined,
238
+ supabaseUrl: supabaseUrl || "PLACEHOLDER",
239
+ supabaseKey: supabaseKey || "PLACEHOLDER",
240
+ },
241
+ });
242
+ s.stop("Project created!");
243
+ // Post-scaffold automation
244
+ // Step 9: Supabase setup (quickstart only)
245
+ if (tier === "quickstart" && useAutomatedSupabase) {
246
+ const result = await setupSupabase(projectDir);
247
+ if (result.method === "local" && result.credentials) {
248
+ supabaseUrl = result.credentials.url;
249
+ supabaseKey = result.credentials.anonKey;
250
+ const { writeEnvFile } = await import("./setup/env.js");
251
+ await writeEnvFile(projectDir, {
252
+ anthropicKey,
253
+ openaiKey: provider === "openai" ? apiKey : undefined,
254
+ supabaseUrl,
255
+ supabaseKey,
256
+ serviceRoleKey: result.credentials.serviceRoleKey,
257
+ });
258
+ p.log.success(chalk.green("Supabase configured automatically!") +
259
+ chalk.dim(` → ${supabaseUrl}`));
260
+ }
261
+ else if (result.method === "remote") {
262
+ p.log.info(chalk.dim("Project linked. Get your URL and key from Settings → API."));
263
+ const supabase = await p.group({
264
+ url: () => p.text({
265
+ message: "Your Supabase project URL:",
266
+ placeholder: "https://xxx.supabase.co",
267
+ validate: (v) => !v || !v.includes("supabase")
268
+ ? "Please enter a valid Supabase URL"
269
+ : undefined,
270
+ }),
271
+ key: () => p.text({
272
+ message: "Your Supabase publishable key (anon/public):",
273
+ placeholder: "sb_publishable_...",
274
+ validate: (v) => (!v ? "Key is required" : undefined),
275
+ }),
276
+ }, {
277
+ onCancel: () => {
278
+ p.cancel("Setup cancelled.");
279
+ process.exit(0);
280
+ },
281
+ });
282
+ supabaseUrl = supabase.url;
283
+ supabaseKey = supabase.key;
284
+ const { writeEnvFile } = await import("./setup/env.js");
285
+ await writeEnvFile(projectDir, {
286
+ anthropicKey,
287
+ openaiKey: provider === "openai" ? apiKey : undefined,
288
+ supabaseUrl,
289
+ supabaseKey,
290
+ });
291
+ }
292
+ else {
293
+ p.log.info(`${chalk.bold("Supabase setup")} ${chalk.dim("(free tier works fine)")}\n` +
294
+ ` ${chalk.dim("1.")} Create a project at ${chalk.cyan("https://supabase.com/dashboard/new")}\n` +
295
+ ` ${chalk.dim("2.")} Go to Settings → API to find your URL and keys`);
296
+ const supabase = await p.group({
297
+ url: () => p.text({
298
+ message: "Your Supabase project URL:",
299
+ placeholder: "https://xxx.supabase.co",
300
+ validate: (v) => !v || !v.includes("supabase")
301
+ ? "Please enter a valid Supabase URL"
302
+ : undefined,
303
+ }),
304
+ key: () => p.text({
305
+ message: "Your Supabase publishable key (anon/public):",
306
+ placeholder: "sb_publishable_...",
307
+ validate: (v) => (!v ? "Key is required" : undefined),
308
+ }),
309
+ }, {
310
+ onCancel: () => {
311
+ p.cancel("Setup cancelled.");
312
+ process.exit(0);
313
+ },
314
+ });
315
+ supabaseUrl = supabase.url;
316
+ supabaseKey = supabase.key;
317
+ const { writeEnvFile } = await import("./setup/env.js");
318
+ await writeEnvFile(projectDir, {
319
+ anthropicKey,
320
+ openaiKey: provider === "openai" ? apiKey : undefined,
321
+ supabaseUrl,
322
+ supabaseKey,
323
+ });
324
+ }
325
+ }
326
+ // Step 10: Install dependencies
327
+ const installConfirm = await p.confirm({
328
+ message: "Install dependencies now? (npm install)",
329
+ initialValue: true,
330
+ });
331
+ if (!p.isCancel(installConfirm) && installConfirm) {
332
+ await installDependencies(projectDir);
333
+ }
334
+ // Final output
335
+ const widgetSnippet = mode === "connect"
336
+ ? [
337
+ ``,
338
+ `${chalk.bold("Embed in your existing site:")}`,
339
+ `${chalk.cyan('<script src="https://YOUR_URL/widget.js"')}`,
340
+ `${chalk.cyan(' data-agent="' + agents.agents[0].id + '"')}`,
341
+ `${chalk.cyan(' data-api="https://YOUR_URL"></script>')}`,
342
+ ]
343
+ : [];
344
+ const didInstall = !p.isCancel(installConfirm) && installConfirm === true;
345
+ const devCmd = didInstall ? "tellet dev" : "npm install && tellet dev";
346
+ if (tier === "quickstart") {
347
+ p.note([
348
+ `cd ${slug}`,
349
+ `${devCmd} ${chalk.dim("→ http://localhost:3000")}`,
350
+ ``,
351
+ `${chalk.bold("CLI commands:")}`,
352
+ `tellet status ${chalk.dim("project info")}`,
353
+ `tellet agent list ${chalk.dim("list agents")}`,
354
+ `tellet deploy ${chalk.dim("deploy guide")}`,
355
+ ``,
356
+ `Dashboard: ${chalk.dim("/dashboard")}`,
357
+ `Orchestrator: ${chalk.dim("floating button in dashboard")}`,
358
+ `Agents: ${chalk.dim(`${agents.agents.length} active`)}`,
359
+ ...widgetSnippet,
360
+ ].join("\n"), mode === "connect" ? "Your AI agents are ready" : "Your AI company is ready");
361
+ p.outro(`Deploy: ${chalk.cyan("tellet deploy")}`);
362
+ }
363
+ else if (tier === "cloud") {
364
+ p.note([
365
+ `cd ${slug}`,
366
+ ``,
367
+ `${chalk.bold("Local development:")}`,
368
+ `docker compose up ${chalk.dim("→ http://localhost:3000")}`,
369
+ ``,
370
+ `${chalk.bold("Deploy to Railway:")}`,
371
+ `tellet deploy`,
372
+ ``,
373
+ `Dashboard: ${chalk.dim("/dashboard")}`,
374
+ `Orchestrator: ${chalk.dim("floating button in dashboard")}`,
375
+ `Agents: ${chalk.dim(`${agents.agents.length} active`)}`,
376
+ ].join("\n"), "Your AI company is ready");
377
+ p.outro(`Or deploy to ${chalk.cyan("Render")}, ${chalk.cyan("Fly.io")}, or any Docker host`);
378
+ }
379
+ else {
380
+ p.note([
381
+ `cd ${slug}`,
382
+ ``,
383
+ `${chalk.bold("Local development:")}`,
384
+ `docker compose up ${chalk.dim("→ http://localhost:3000")}`,
385
+ ``,
386
+ `${chalk.bold("Deploy to AWS:")}`,
387
+ `tellet deploy`,
388
+ ``,
389
+ `Dashboard: ${chalk.dim("/dashboard")}`,
390
+ `Orchestrator: ${chalk.dim("floating button in dashboard")}`,
391
+ `Agents: ${chalk.dim(`${agents.agents.length} active`)}`,
392
+ `Est. cost: ${chalk.dim("$5-15/mo")}`,
393
+ ].join("\n"), "Your AI company is ready (AWS)");
394
+ p.outro(`Run ${chalk.cyan("tellet deploy")} to provision AWS infrastructure`);
395
+ }
396
+ }
397
+ catch (err) {
398
+ s.stop("Failed to create project.");
399
+ p.log.error(err instanceof Error ? err.message : String(err));
400
+ process.exit(1);
401
+ }
402
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Run `npm install` in the project directory with live output.
3
+ */
4
+ export declare function installDependencies(projectDir: string): Promise<boolean>;
@@ -0,0 +1,24 @@
1
+ import { spawnSync } from "child_process";
2
+ import * as p from "@clack/prompts";
3
+ /**
4
+ * Run `npm install` in the project directory with live output.
5
+ */
6
+ export async function installDependencies(projectDir) {
7
+ const s = p.spinner();
8
+ s.start("Installing dependencies...");
9
+ const result = spawnSync("npm", ["install"], {
10
+ cwd: projectDir,
11
+ stdio: "pipe",
12
+ timeout: 300_000,
13
+ });
14
+ if (result.status !== 0) {
15
+ s.stop("Failed to install dependencies.");
16
+ const stderr = result.stderr?.toString().trim();
17
+ if (stderr) {
18
+ p.log.error(stderr.slice(0, 500));
19
+ }
20
+ return false;
21
+ }
22
+ s.stop("Dependencies installed!");
23
+ return true;
24
+ }
@@ -0,0 +1,12 @@
1
+ interface EnvOptions {
2
+ anthropicKey: string;
3
+ openaiKey?: string;
4
+ supabaseUrl: string;
5
+ supabaseKey: string;
6
+ serviceRoleKey?: string;
7
+ }
8
+ /**
9
+ * Write (or overwrite) the .env.local file with Supabase + API credentials.
10
+ */
11
+ export declare function writeEnvFile(projectDir: string, options: EnvOptions): Promise<void>;
12
+ export {};
@@ -0,0 +1,26 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ /** Escape a value for .env files — wrap in quotes if it contains special chars. */
4
+ function envQuote(v) {
5
+ if (/[\s#"'\\$`!]/.test(v) || v.includes("\n")) {
6
+ return `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
7
+ }
8
+ return v;
9
+ }
10
+ /**
11
+ * Write (or overwrite) the .env.local file with Supabase + API credentials.
12
+ */
13
+ export async function writeEnvFile(projectDir, options) {
14
+ const lines = [
15
+ `# Orchestrator (always Anthropic)`,
16
+ `ANTHROPIC_API_KEY=${envQuote(options.anthropicKey)}`,
17
+ ];
18
+ if (options.openaiKey) {
19
+ lines.push(`OPENAI_API_KEY=${envQuote(options.openaiKey)}`);
20
+ }
21
+ lines.push(``, `# Supabase`, `NEXT_PUBLIC_SUPABASE_URL=${envQuote(options.supabaseUrl)}`, `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=${envQuote(options.supabaseKey)}`);
22
+ if (options.serviceRoleKey) {
23
+ lines.push(`SUPABASE_SERVICE_ROLE_KEY=${envQuote(options.serviceRoleKey)}`);
24
+ }
25
+ await fs.writeFile(path.join(projectDir, ".env.local"), lines.join("\n") + "\n");
26
+ }
@@ -0,0 +1,36 @@
1
+ /** Check if the Supabase CLI is installed and accessible. */
2
+ export declare function hasSupabaseCLI(): boolean;
3
+ /** Check if Docker is running (required for `supabase start`). */
4
+ export declare function hasDocker(): boolean;
5
+ export interface SupabaseCredentials {
6
+ url: string;
7
+ anonKey: string;
8
+ serviceRoleKey?: string;
9
+ }
10
+ /**
11
+ * Initialize Supabase in the project directory.
12
+ * Creates supabase/config.toml if not already present.
13
+ */
14
+ export declare function supabaseInit(projectDir: string): boolean;
15
+ /**
16
+ * Start local Supabase (Docker containers).
17
+ * Returns credentials on success, null on failure.
18
+ */
19
+ export declare function supabaseStartLocal(projectDir: string): Promise<SupabaseCredentials | null>;
20
+ /**
21
+ * Link to a remote Supabase project.
22
+ * Prompts for project ref, runs `supabase link`.
23
+ */
24
+ export declare function supabaseLinkRemote(projectDir: string): Promise<boolean>;
25
+ /**
26
+ * Push migrations to the database.
27
+ */
28
+ export declare function supabasePushMigrations(projectDir: string): Promise<boolean>;
29
+ /**
30
+ * Full Supabase setup flow for quickstart tier.
31
+ * Returns credentials or null if user chose manual setup.
32
+ */
33
+ export declare function setupSupabase(projectDir: string): Promise<{
34
+ credentials: SupabaseCredentials | null;
35
+ method: "local" | "remote" | "manual";
36
+ }>;