create-amiki 1.0.2 → 1.0.3

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 (2) hide show
  1. package/index.js +729 -9
  2. package/package.json +2 -4
package/index.js CHANGED
@@ -3,21 +3,741 @@
3
3
  // Copyright (c) 2026 amiki-framework Contributors
4
4
 
5
5
  /**
6
- * create-amiki — Thin wrapper around @amiki/create.
6
+ * create-amiki — Scaffolding wizard for amiki-framework projects.
7
7
  *
8
- * This package exists so that `bun create amiki` works.
9
- * Bun resolves "create-<name>" from npm, and this package
10
- * simply delegates to the real implementation in @amiki/create.
8
+ * Self-contained standalone package (no external dependencies).
9
+ * Run via: bun create amiki, npx create-amiki, or bunx create-amiki
11
10
  *
12
11
  * Usage:
13
- * bun create amiki
14
- * bun create amiki my-project
15
- * npx create-amiki
12
+ * bun create amiki Interactive wizard
13
+ * bun create amiki my-project Create in ./my-project
14
+ * create-amiki --preset small my-project Quick-start with preset
16
15
  */
17
16
 
18
- import { main } from '@amiki/create';
17
+ import { writeFile, mkdir } from 'node:fs/promises';
18
+ import { join, dirname } from 'node:path';
19
+ import { existsSync } from 'node:fs';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Types
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /**
26
+ * @typedef {'small' | 'cluster' | 'multi-service'} Preset
27
+ * @typedef {{ projectName: string, preset: Preset, features: string[], useDocker: boolean }} Answers
28
+ * @typedef {{ path: string, content: string }} FileSpec
29
+ */
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // CLI argument parsing
33
+ // ---------------------------------------------------------------------------
34
+
35
+ const args = process.argv.slice(2);
36
+
37
+ function parseArgs() {
38
+ /** @type {string | undefined} */
39
+ let targetDir;
40
+ /** @type {string | undefined} */
41
+ let preset;
42
+
43
+ for (let i = 0; i < args.length; i++) {
44
+ const arg = args[i];
45
+ if (!arg) continue;
46
+
47
+ if (arg === '--preset' || arg === '-p') {
48
+ preset = args[++i];
49
+ } else if (!arg.startsWith('-')) {
50
+ targetDir = arg;
51
+ }
52
+ }
53
+
54
+ return { targetDir, preset };
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Helpers
59
+ // ---------------------------------------------------------------------------
60
+
61
+ function tsconfigContent() {
62
+ return JSON.stringify({
63
+ compilerOptions: {
64
+ target: 'ESNext',
65
+ module: 'ESNext',
66
+ moduleResolution: 'bundler',
67
+ moduleDetection: 'force',
68
+ lib: ['ESNext'],
69
+ types: ['bun-types'],
70
+ strict: true,
71
+ noUncheckedIndexedAccess: true,
72
+ noImplicitOverride: true,
73
+ forceConsistentCasingInFileNames: true,
74
+ isolatedModules: true,
75
+ verbatimModuleSyntax: true,
76
+ esModuleInterop: true,
77
+ skipLibCheck: true,
78
+ allowImportingTsExtensions: true,
79
+ noEmit: true,
80
+ },
81
+ include: ['src'],
82
+ }, null, 2);
83
+ }
84
+
85
+ /** @param {Answers} answers */
86
+ function readmeContent(answers) {
87
+ return `# ${answers.projectName}
88
+
89
+ A Discord bot built with [amiki-framework](https://github.com/amiki-framework/amiki).
90
+
91
+ ## Preset: ${answers.preset}
92
+
93
+ This project was scaffolded with \`amiki init\`.
94
+
95
+ ## Getting started
96
+
97
+ \`\`\`bash
98
+ # Install dependencies
99
+ bun install
100
+
101
+ # Set up your bot token
102
+ cp .env.example .env
103
+ # Edit .env with your Discord bot token
104
+
105
+ # Run
106
+ bun run src/index.ts
107
+ \`\`\`
108
+
109
+ ## Commands
110
+
111
+ | Command | Description |
112
+ |---------|-------------|
113
+ | \`bun run src/index.ts\` | Start the bot |
114
+ | \`bun run typecheck\` | Type-check the project |
115
+
116
+ ## License
117
+
118
+ MIT
119
+ `;
120
+ }
121
+
122
+ function envContent() {
123
+ return '# Discord bot token (https://discord.com/developers/applications)\nDISCORD_TOKEN=\n\n# Environment\nNODE_ENV=development\n';
124
+ }
125
+
126
+ function dockerfileContent() {
127
+ return 'FROM oven/bun:latest\n\nWORKDIR /app\n\nCOPY package.json bun.lock ./\nRUN bun install --frozen-lockfile\n\nCOPY . .\n\nCMD ["bun", "run", "src/index.ts"]\n';
128
+ }
129
+
130
+ /** @param {Answers} answers */
131
+ function dockerComposeContent(answers) {
132
+ if (answers.preset === 'multi-service') {
133
+ return `version: '3.8'
134
+
135
+ services:
136
+ gateway:
137
+ build:
138
+ context: ./services/gateway
139
+ dockerfile: Dockerfile
140
+ environment:
141
+ - NODE_ENV=production
142
+ - DISCORD_TOKEN=\${DISC_TOKEN}
143
+ restart: unless-stopped
144
+
145
+ api:
146
+ build:
147
+ context: ./services/api
148
+ dockerfile: Dockerfile
149
+ ports:
150
+ - "9090:9090"
151
+ environment:
152
+ - NODE_ENV=production
153
+ restart: unless-stopped
154
+
155
+ worker:
156
+ build:
157
+ context: ./services/worker
158
+ dockerfile: Dockerfile
159
+ environment:
160
+ - NODE_ENV=production
161
+ - DISCORD_TOKEN=\${DISC_TOKEN}
162
+ restart: unless-stopped
163
+ depends_on:
164
+ - gateway
165
+ `;
166
+ }
167
+
168
+ return `version: '3.8'
169
+
170
+ services:
171
+ app:
172
+ build: .
173
+ environment:
174
+ - NODE_ENV=production
175
+ - DISCORD_TOKEN=\${DISCORD_TOKEN}
176
+ restart: unless-stopped
177
+ `;
178
+ }
179
+
180
+ /** @param {Record<string,string>} deps @param {Record<string,string>} devDeps */
181
+ function packageJsonContent(answers, deps, devDeps) {
182
+ return JSON.stringify({
183
+ name: answers.projectName,
184
+ version: '0.1.0',
185
+ private: true,
186
+ type: 'module',
187
+ scripts: {
188
+ start: 'bun run src/index.ts',
189
+ typecheck: 'tsc --noEmit',
190
+ },
191
+ dependencies: deps,
192
+ devDependencies: devDeps,
193
+ }, null, 2);
194
+ }
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // Preset: small
198
+ // ---------------------------------------------------------------------------
199
+
200
+ /** @param {Answers} answers @returns {FileSpec[]} */
201
+ function smallTemplate(answers) {
202
+ const files = [];
203
+
204
+ files.push({
205
+ path: 'package.json',
206
+ content: packageJsonContent(answers,
207
+ { '@amiki/core': '^0.1.0', '@amiki/types': '^0.1.0' },
208
+ { typescript: '^5.8.0', 'bun-types': '^1.3.0' },
209
+ ),
210
+ });
211
+
212
+ files.push({ path: 'tsconfig.json', content: tsconfigContent() });
213
+
214
+ files.push({
215
+ path: 'src/index.ts',
216
+ content: `#!/usr/bin/env bun
217
+ // SPDX-License-Identifier: MIT
218
+
219
+ import { GatewayClient, GatewayIntent } from '@amiki/core';
220
+ import type { GatewayPayload, MessageCreateEventData } from '@amiki/types';
221
+
222
+ const token = process.env.DISCORD_TOKEN;
223
+ if (!token) {
224
+ console.error('DISCORD_TOKEN is required');
225
+ process.exit(1);
226
+ }
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // Gateway client
230
+ // ---------------------------------------------------------------------------
231
+
232
+ const client = new GatewayClient({
233
+ token,
234
+ intents:
235
+ GatewayIntent.Guilds |
236
+ GatewayIntent.GuildMessages |
237
+ GatewayIntent.MessageContent,
238
+ totalShards: 1,
239
+ shardId: 0,
240
+ });
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // Event handlers
244
+ // ---------------------------------------------------------------------------
245
+
246
+ client.on('ready', (payload) => {
247
+ const user = payload.d?.user;
248
+ console.log(\`Logged in as \${user?.username ?? 'unknown'} (\${user?.id ?? '?'})\`);
249
+ });
250
+
251
+ client.on('message_create', (payload) => {
252
+ const msg = payload.d;
253
+ if (!msg || msg.author?.bot) return;
254
+
255
+ console.log(\`[\${msg.channel_id}] \${msg.author?.username}: \${msg.content}\`);
256
+
257
+ // Simple ping-pong
258
+ if (msg.content === '!ping') {
259
+ console.log('Pong!');
260
+ }
261
+ });
262
+
263
+ // ---------------------------------------------------------------------------
264
+ // Start
265
+ // ---------------------------------------------------------------------------
266
+
267
+ console.log('Starting bot...');
268
+ await client.connect();
269
+ console.log('Bot is running. Press Ctrl+C to stop.');
270
+
271
+ // Graceful shutdown
272
+ process.on('SIGINT', async () => {
273
+ console.log('\\nShutting down...');
274
+ await client.disconnect();
275
+ process.exit(0);
276
+ });
277
+ `,
278
+ });
279
+
280
+ files.push({ path: '.env.example', content: envContent() });
281
+ files.push({ path: 'README.md', content: readmeContent(answers) });
282
+
283
+ if (answers.features.includes('docker')) {
284
+ files.push({ path: 'Dockerfile', content: dockerfileContent() });
285
+ if (answers.useDocker) {
286
+ files.push({ path: 'docker-compose.yml', content: dockerComposeContent(answers) });
287
+ }
288
+ }
289
+
290
+ return files;
291
+ }
292
+
293
+ // ---------------------------------------------------------------------------
294
+ // Preset: cluster
295
+ // ---------------------------------------------------------------------------
296
+
297
+ /** @param {Answers} answers @returns {FileSpec[]} */
298
+ function clusterTemplate(answers) {
299
+ const files = [];
300
+
301
+ files.push({
302
+ path: 'package.json',
303
+ content: packageJsonContent(answers,
304
+ {
305
+ '@amiki/core': '^0.1.0',
306
+ '@amiki/cluster': '^0.1.0',
307
+ '@amiki/metrics': '^0.1.0',
308
+ '@amiki/types': '^0.1.0',
309
+ },
310
+ { typescript: '^5.8.0', 'bun-types': '^1.3.0' },
311
+ ),
312
+ });
313
+
314
+ files.push({ path: 'tsconfig.json', content: tsconfigContent() });
315
+
316
+ files.push({
317
+ path: 'src/index.ts',
318
+ content: `#!/usr/bin/env bun
319
+ // SPDX-License-Identifier: MIT
320
+
321
+ import { ClusterManager } from '@amiki/cluster';
322
+ import { GatewayIntent } from '@amiki/types';
323
+
324
+ const token = process.env.DISCORD_TOKEN;
325
+ if (!token) {
326
+ console.error('DISCORD_TOKEN is required');
327
+ process.exit(1);
328
+ }
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // Cluster configuration
332
+ // ---------------------------------------------------------------------------
333
+
334
+ const cluster = new ClusterManager({
335
+ token,
336
+ intents:
337
+ GatewayIntent.Guilds |
338
+ GatewayIntent.GuildMessages |
339
+ GatewayIntent.MessageContent,
340
+ totalShards: Number(process.env.TOTAL_SHARDS ?? 1),
341
+ shardsPerWorker: Number(process.env.SHARDS_PER_WORKER ?? 8),
342
+ maxConcurrency: Number(process.env.MAX_CONCURRENCY ?? 1),
343
+ mode: /** @type {'direct' | 'proxy'} */ (process.env.CLUSTER_MODE ?? 'direct'),
344
+
345
+ onEvent: (event) => {
346
+ switch (event.type) {
347
+ case 'cluster_started':
348
+ console.log(\`Cluster started: \${event.topology.workerCount} workers, \${event.topology.totalShards} shards\`);
349
+ break;
350
+ case 'worker_started':
351
+ console.log(\`Worker \${event.workerId} started\`);
352
+ break;
353
+ case 'worker_unhealthy':
354
+ console.warn(\`Worker \${event.workerId} unhealthy — restarting\`);
355
+ break;
356
+ case 'worker_restarted':
357
+ console.log(\`Worker \${event.workerId} restarted\`);
358
+ break;
359
+ case 'cluster_stopped':
360
+ console.log('Cluster stopped');
361
+ break;
362
+ case 'error':
363
+ console.error(\`Cluster error: \${event.error.message}\`);
364
+ break;
365
+ }
366
+ },
367
+ });
368
+
369
+ // ---------------------------------------------------------------------------
370
+ // Start
371
+ // ---------------------------------------------------------------------------
372
+
373
+ try {
374
+ await cluster.start();
375
+ console.log(\`Cluster ready — \${cluster.shardTopology?.workerCount} workers\`);
376
+ } catch (err) {
377
+ console.error('Failed to start cluster:', err);
378
+ process.exit(1);
379
+ }
380
+
381
+ // Graceful shutdown
382
+ process.on('SIGINT', async () => {
383
+ console.log('\\nShutting down cluster...');
384
+ await cluster.stop('SIGINT');
385
+ process.exit(0);
386
+ });
387
+
388
+ process.on('SIGTERM', async () => {
389
+ console.log('\\nShutting down cluster...');
390
+ await cluster.stop('SIGTERM');
391
+ process.exit(0);
392
+ });
393
+ `,
394
+ });
395
+
396
+ if (answers.features.includes('metrics')) {
397
+ files.push({
398
+ path: 'src/metrics.ts',
399
+ content: `import { MetricRegistry, MetricsServer, registerProcessMetrics } from '@amiki/metrics';
400
+
401
+ export function createMetricsServer(port = 9090) {
402
+ const registry = new MetricRegistry({
403
+ prefix: 'amiki',
404
+ defaultLabels: {
405
+ service: 'discord-bot',
406
+ environment: process.env.NODE_ENV ?? 'development',
407
+ },
408
+ });
409
+
410
+ registerProcessMetrics(registry);
411
+
412
+ const server = new MetricsServer({ port, registry });
413
+ server.start();
414
+
415
+ console.log(\`Metrics: http://localhost:\${port}/metrics\`);
416
+ return server;
417
+ }
418
+ `,
419
+ });
420
+ }
421
+
422
+ files.push({
423
+ path: '.env.example',
424
+ content: `${envContent()}# Sharding\nTOTAL_SHARDS=1\nSHARDS_PER_WORKER=8\nMAX_CONCURRENCY=1\nCLUSTER_MODE=direct\n\n# Metrics\nMETRICS_PORT=9090\n`,
425
+ });
426
+
427
+ files.push({ path: 'README.md', content: readmeContent(answers) });
428
+
429
+ if (answers.features.includes('docker')) {
430
+ files.push({ path: 'Dockerfile', content: dockerfileContent() });
431
+ if (answers.useDocker) {
432
+ files.push({ path: 'docker-compose.yml', content: dockerComposeContent(answers) });
433
+ }
434
+ }
435
+
436
+ return files;
437
+ }
438
+
439
+ // ---------------------------------------------------------------------------
440
+ // Preset: multi-service
441
+ // ---------------------------------------------------------------------------
442
+
443
+ /** @param {Answers} answers @returns {FileSpec[]} */
444
+ function multiServiceTemplate(answers) {
445
+ const files = [];
446
+
447
+ // Root
448
+ files.push({
449
+ path: 'package.json',
450
+ content: JSON.stringify({
451
+ name: answers.projectName,
452
+ version: '0.1.0',
453
+ private: true,
454
+ type: 'module',
455
+ scripts: {
456
+ 'dev:gateway': 'bun run --watch services/gateway/src/index.ts',
457
+ 'dev:api': 'bun run --watch services/api/src/index.ts',
458
+ 'dev:worker': 'bun run --watch services/worker/src/index.ts',
459
+ typecheck: 'tsc --noEmit',
460
+ },
461
+ devDependencies: { typescript: '^5.8.0', 'bun-types': '^1.3.0' },
462
+ }, null, 2),
463
+ });
464
+
465
+ files.push({ path: 'tsconfig.json', content: tsconfigContent() });
466
+
467
+ // Gateway service
468
+ files.push({
469
+ path: 'services/gateway/package.json',
470
+ content: packageJsonContent(answers,
471
+ { '@amiki/core': '^0.1.0', '@amiki/cluster': '^0.1.0', '@amiki/transport': '^0.1.0', '@amiki/types': '^0.1.0' },
472
+ {},
473
+ ),
474
+ });
475
+
476
+ files.push({
477
+ path: 'services/gateway/src/index.ts',
478
+ content: `#!/usr/bin/env bun
479
+ // SPDX-License-Identifier: MIT
480
+
481
+ import { ClusterManager } from '@amiki/cluster';
482
+ import { GatewayIntent } from '@amiki/types';
483
+
484
+ const token = process.env.DISCORD_TOKEN;
485
+ if (!token) throw new Error('DISCORD_TOKEN required');
486
+
487
+ const cluster = new ClusterManager({
488
+ token,
489
+ intents: GatewayIntent.Guilds | GatewayIntent.GuildMessages,
490
+ totalShards: Number(process.env.TOTAL_SHARDS ?? 1),
491
+ shardsPerWorker: Number(process.env.SHARDS_PER_WORKER ?? 4),
492
+ maxConcurrency: 1,
493
+ mode: 'proxy',
494
+ onEvent: (e) => {
495
+ if (e.type === 'cluster_started') {
496
+ console.log(\`Gateway ready: \${e.topology.workerCount} workers\`);
497
+ }
498
+ },
499
+ });
500
+
501
+ await cluster.start();
502
+
503
+ process.on('SIGINT', async () => { await cluster.stop('SIGINT'); process.exit(0); });
504
+ `,
505
+ });
506
+
507
+ // API service
508
+ files.push({
509
+ path: 'services/api/package.json',
510
+ content: packageJsonContent(answers,
511
+ { '@amiki/core': '^0.1.0', '@amiki/metrics': '^0.1.0', '@amiki/transport': '^0.1.0', '@amiki/types': '^0.1.0' },
512
+ {},
513
+ ),
514
+ });
515
+
516
+ files.push({
517
+ path: 'services/api/src/index.ts',
518
+ content: `#!/usr/bin/env bun
519
+ // SPDX-License-Identifier: MIT
520
+
521
+ import { MetricRegistry, MetricsServer, registerProcessMetrics } from '@amiki/metrics';
522
+
523
+ const registry = new MetricRegistry({
524
+ prefix: 'amiki',
525
+ defaultLabels: { service: 'api', environment: process.env.NODE_ENV ?? 'development' },
526
+ });
527
+
528
+ registerProcessMetrics(registry);
529
+
530
+ const server = new MetricsServer({ port: Number(process.env.PORT ?? 9090), registry });
531
+ server.start();
532
+
533
+ console.log(\`API server running on port \${process.env.PORT ?? 9090}\`);
534
+
535
+ process.on('SIGINT', () => { server.stop(); process.exit(0); });
536
+ `,
537
+ });
538
+
539
+ // Worker service
540
+ files.push({
541
+ path: 'services/worker/package.json',
542
+ content: packageJsonContent(answers,
543
+ { '@amiki/core': '^0.1.0', '@amiki/transport': '^0.1.0', '@amiki/types': '^0.1.0' },
544
+ {},
545
+ ),
546
+ });
547
+
548
+ files.push({
549
+ path: 'services/worker/src/index.ts',
550
+ content: `#!/usr/bin/env bun
551
+ // SPDX-License-Identifier: MIT
552
+
553
+ import { InProcTransport } from '@amiki/transport';
554
+ import type { GatewayPayload, MessageCreateEventData } from '@amiki/types';
555
+
556
+ const transport = new InProcTransport();
557
+ await transport.connect();
558
+ await transport.subscribe('proxy.events.*', async (msg) => {
559
+ const event = /** @type {string} */ (msg.type);
560
+ if (event === 'message_create') {
561
+ const payload = JSON.parse(new TextDecoder().decode(msg.payload));
562
+ const d = payload.d;
563
+ if (d && !d.author?.bot) {
564
+ console.log(\`[\${d.channel_id}] \${d.author?.username}: \${d.content}\`);
565
+ }
566
+ }
567
+ });
568
+
569
+ console.log('Worker ready, waiting for events...');
570
+
571
+ process.on('SIGINT', async () => { await transport.disconnect(); process.exit(0); });
572
+ `,
573
+ });
574
+
575
+ files.push({
576
+ path: 'config/default.ts',
577
+ content: `export const config = {
578
+ env: process.env.NODE_ENV ?? 'development',
579
+ transport: { type: 'inproc' },
580
+ };
581
+ `,
582
+ });
583
+
584
+ files.push({
585
+ path: '.env.example',
586
+ content: `${envContent()}# Sharding\nTOTAL_SHARDS=1\nSHARDS_PER_WORKER=4\n\n# Services\nPORT=9090\n`,
587
+ });
588
+
589
+ files.push({
590
+ path: 'README.md',
591
+ content: `# ${answers.projectName}
592
+
593
+ Multi-service Discord bot built with amiki-framework.
594
+
595
+ ## Services
596
+
597
+ | Service | Description |
598
+ |---------|-------------|
599
+ | \`gateway\` | Discord Gateway connection, shard management |
600
+ | \`api\` | HTTP API + Prometheus metrics |
601
+ | \`worker\` | Event processing, business logic |
602
+
603
+ ## Getting started
604
+
605
+ \`\`\`bash
606
+ bun install
607
+ cp .env.example .env
608
+ # Edit .env with your Discord bot token
609
+ bun run dev:gateway # Terminal 1
610
+ bun run dev:api # Terminal 2
611
+ bun run dev:worker # Terminal 3
612
+ \`\`\`
613
+
614
+ ## Docker
615
+
616
+ \`\`\`bash
617
+ docker compose up
618
+ \`\`\`
619
+ `,
620
+ });
621
+
622
+ files.push({ path: 'Dockerfile', content: dockerfileContent() });
623
+ files.push({ path: 'docker-compose.yml', content: dockerComposeContent(answers) });
624
+
625
+ return files;
626
+ }
627
+
628
+ // ---------------------------------------------------------------------------
629
+ // Template selector
630
+ // ---------------------------------------------------------------------------
631
+
632
+ /** @param {Answers} answers @returns {FileSpec[]} */
633
+ function generateFiles(answers) {
634
+ switch (answers.preset) {
635
+ case 'small': return smallTemplate(answers);
636
+ case 'cluster': return clusterTemplate(answers);
637
+ case 'multi-service': return multiServiceTemplate(answers);
638
+ default: return smallTemplate(answers);
639
+ }
640
+ }
641
+
642
+ // ---------------------------------------------------------------------------
643
+ // Quick-start with preset (non-interactive)
644
+ // ---------------------------------------------------------------------------
645
+
646
+ /** @param {string} preset @param {string} targetDir */
647
+ async function quickStart(preset, targetDir) {
648
+ const projectName = targetDir.split(/[/\\]/).pop() ?? 'amiki-app';
649
+
650
+ /** @type {Answers} */
651
+ const answers = {
652
+ projectName,
653
+ preset: /** @type {Preset} */ (preset),
654
+ features: [],
655
+ useDocker: false,
656
+ };
657
+
658
+ const files = generateFiles(answers);
659
+
660
+ for (const file of files) {
661
+ const fullPath = join(process.cwd(), targetDir, file.path);
662
+ const dir = dirname(fullPath);
663
+
664
+ if (!existsSync(dir)) {
665
+ await mkdir(dir, { recursive: true });
666
+ }
667
+ await writeFile(fullPath, file.content, 'utf-8');
668
+ }
669
+
670
+ console.log(`\n✔ Created ${files.length} files in ${projectName}/\n`);
671
+ console.log(' Next steps:');
672
+ console.log(` cd ${projectName}`);
673
+ console.log(' bun install');
674
+ console.log(' cp .env.example .env');
675
+ console.log(' bun run src/index.ts\n');
676
+ }
677
+
678
+ // ---------------------------------------------------------------------------
679
+ // Interactive wizard (dynamic import of @amiki/create if available)
680
+ // ---------------------------------------------------------------------------
681
+
682
+ async function runWizard(targetDir) {
683
+ try {
684
+ const mod = await import('@amiki/create');
685
+ if (mod && typeof mod.runWizard === 'function') {
686
+ await mod.runWizard(targetDir);
687
+ return;
688
+ }
689
+ } catch {
690
+ // @amiki/create not available — run minimal wizard inline
691
+ }
692
+ // Fallback: prompt for project name and preset
693
+ const projectName = targetDir ?? 'my-amiki-bot';
694
+ const preset = 'small';
695
+
696
+ /** @type {Answers} */
697
+ const answers = {
698
+ projectName,
699
+ preset: /** @type {Preset} */ (preset),
700
+ features: [],
701
+ useDocker: false,
702
+ };
703
+
704
+ const files = generateFiles(answers);
705
+ const resolvedDir = join(process.cwd(), projectName);
706
+
707
+ for (const file of files) {
708
+ const fullPath = join(resolvedDir, file.path);
709
+ const dir = dirname(fullPath);
710
+ if (!existsSync(dir)) {
711
+ await mkdir(dir, { recursive: true });
712
+ }
713
+ await writeFile(fullPath, file.content, 'utf-8');
714
+ }
715
+
716
+ console.log(`\n✔ Created ${files.length} files in ${projectName}/\n`);
717
+ console.log(' Next steps:');
718
+ console.log(` cd ${projectName}`);
719
+ console.log(' bun install');
720
+ console.log(' cp .env.example .env');
721
+ console.log(' bun run src/index.ts\n');
722
+ }
723
+
724
+ // ---------------------------------------------------------------------------
725
+ // Main
726
+ // ---------------------------------------------------------------------------
727
+
728
+ async function main() {
729
+ const { targetDir, preset } = parseArgs();
730
+
731
+ if (preset && targetDir) {
732
+ await quickStart(preset, targetDir);
733
+ return;
734
+ }
735
+
736
+ // Interactive wizard
737
+ await runWizard(targetDir);
738
+ }
19
739
 
20
740
  main().catch((err) => {
21
- console.error('Error:', err);
741
+ console.error('Error:', err?.message ?? err);
22
742
  process.exit(1);
23
743
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-amiki",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Scaffolding wizard for amiki-framework — thin wrapper around @amiki/create. Run: bun create amiki",
5
5
  "license": "GPL-2.0-only",
6
6
  "type": "module",
@@ -38,7 +38,5 @@
38
38
  "bun": ">=1.3.0",
39
39
  "node": ">=18.0.0"
40
40
  },
41
- "dependencies": {
42
- "@amiki/create": "^1.0.0"
43
- }
41
+ "dependencies": {}
44
42
  }