a3-stack-cli 0.1.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.
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "a3-stack-cli",
3
+ "version": "0.1.0",
4
+ "description": "A3 Stack CLI - Create modern full-stack apps with SvelteKit, Better Auth, and Kysely",
5
+ "type": "module",
6
+ "bin": {
7
+ "a3": "./src/index.ts"
8
+ },
9
+ "main": "./src/lib/index.ts",
10
+ "exports": {
11
+ ".": "./src/lib/index.ts",
12
+ "./commands": "./src/commands/index.ts"
13
+ },
14
+ "files": [
15
+ "src"
16
+ ],
17
+ "scripts": {
18
+ "dev": "bun run src/index.ts",
19
+ "typecheck": "tsc --noEmit",
20
+ "prepublishOnly": "bun run typecheck"
21
+ },
22
+ "dependencies": {
23
+ "citty": "^0.1.6",
24
+ "consola": "^3.4.0",
25
+ "giget": "^2.0.0",
26
+ "picocolors": "^1.1.1",
27
+ "prompts": "^2.4.2"
28
+ },
29
+ "devDependencies": {
30
+ "@types/bun": "^1.3.5",
31
+ "@types/prompts": "^2.4.9",
32
+ "typescript": "^5.9.3"
33
+ },
34
+ "keywords": [
35
+ "a3-stack",
36
+ "sveltekit",
37
+ "better-auth",
38
+ "kysely",
39
+ "cli",
40
+ "scaffold"
41
+ ],
42
+ "author": "Adam Augustinsky",
43
+ "license": "MIT",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/AdamAugustinsky/a3-stack"
47
+ },
48
+ "engines": {
49
+ "bun": ">=1.0.0"
50
+ }
51
+ }
@@ -0,0 +1,197 @@
1
+ import { defineCommand } from 'citty';
2
+ import consola from 'consola';
3
+ import prompts from 'prompts';
4
+ import pc from 'picocolors';
5
+ import { downloadTemplate, listTemplates, processTemplate } from '../lib/template';
6
+ import { initGit, isGitInstalled } from '../lib/git';
7
+ import { installDependencies, runPostInstall } from '../lib/setup';
8
+ import { resolve } from 'path';
9
+ import { existsSync } from 'fs';
10
+
11
+ export const createCommand = defineCommand({
12
+ meta: {
13
+ name: 'create',
14
+ description: 'Create a new A3 Stack project',
15
+ },
16
+ args: {
17
+ name: {
18
+ type: 'positional',
19
+ description: 'Project name',
20
+ required: false,
21
+ },
22
+ template: {
23
+ type: 'string',
24
+ alias: 't',
25
+ description: 'Template to use (e.g., kysely)',
26
+ },
27
+ git: {
28
+ type: 'boolean',
29
+ description: 'Initialize git repository',
30
+ default: true,
31
+ },
32
+ install: {
33
+ type: 'boolean',
34
+ alias: 'i',
35
+ description: 'Install dependencies',
36
+ default: true,
37
+ },
38
+ },
39
+ async run({ args }) {
40
+ console.log('');
41
+ consola.box({
42
+ title: 'A3 Stack CLI',
43
+ message: 'Create modern full-stack apps',
44
+ style: {
45
+ borderColor: 'cyan',
46
+ },
47
+ });
48
+ console.log('');
49
+
50
+ // Get available templates
51
+ const templates = await listTemplates();
52
+
53
+ // Check if target directory already exists (if name provided)
54
+ if (args.name) {
55
+ const targetDir = resolve(process.cwd(), args.name);
56
+ if (existsSync(targetDir)) {
57
+ consola.error(`Directory ${pc.cyan(args.name)} already exists`);
58
+ return;
59
+ }
60
+ }
61
+
62
+ // Interactive prompts
63
+ const response = await prompts(
64
+ [
65
+ {
66
+ type: args.name ? null : 'text',
67
+ name: 'name',
68
+ message: 'Project name:',
69
+ initial: 'my-a3-app',
70
+ validate: (value: string) => {
71
+ if (!value) return 'Project name is required';
72
+ if (!/^[a-z0-9-_]+$/i.test(value)) {
73
+ return 'Project name can only contain letters, numbers, dashes, and underscores';
74
+ }
75
+ const targetDir = resolve(process.cwd(), value);
76
+ if (existsSync(targetDir)) {
77
+ return `Directory ${value} already exists`;
78
+ }
79
+ return true;
80
+ },
81
+ },
82
+ {
83
+ type: args.template ? null : 'select',
84
+ name: 'template',
85
+ message: 'Select a template:',
86
+ choices: templates.map((t) => ({
87
+ title: `${t.displayName}`,
88
+ description: t.description,
89
+ value: t.name,
90
+ })),
91
+ },
92
+ {
93
+ type: 'confirm',
94
+ name: 'git',
95
+ message: 'Initialize git repository?',
96
+ initial: true,
97
+ },
98
+ {
99
+ type: 'confirm',
100
+ name: 'install',
101
+ message: 'Install dependencies?',
102
+ initial: true,
103
+ },
104
+ ],
105
+ {
106
+ onCancel: () => {
107
+ consola.info('Cancelled');
108
+ process.exit(0);
109
+ },
110
+ }
111
+ );
112
+
113
+ const projectName = args.name || response.name;
114
+ const template = args.template || response.template;
115
+ const shouldInitGit = args.git !== false && response.git !== false;
116
+ const shouldInstall = args.install !== false && response.install !== false;
117
+
118
+ if (!projectName || !template) {
119
+ consola.error('Missing required options');
120
+ return;
121
+ }
122
+
123
+ const targetDir = resolve(process.cwd(), projectName);
124
+
125
+ console.log('');
126
+ consola.start(`Creating ${pc.cyan(projectName)} with ${pc.green(template)} template...`);
127
+
128
+ // Download template
129
+ try {
130
+ await downloadTemplate(template, targetDir);
131
+ consola.success('Downloaded template');
132
+ } catch (error) {
133
+ consola.error(`Failed to download template: ${error}`);
134
+ return;
135
+ }
136
+
137
+ // Process template (replace placeholders)
138
+ try {
139
+ await processTemplate(targetDir, { projectName });
140
+ consola.success('Processed template');
141
+ } catch (error) {
142
+ consola.error(`Failed to process template: ${error}`);
143
+ return;
144
+ }
145
+
146
+ // Initialize git
147
+ if (shouldInitGit) {
148
+ const gitInstalled = await isGitInstalled();
149
+ if (gitInstalled) {
150
+ try {
151
+ await initGit(targetDir);
152
+ consola.success('Initialized git repository');
153
+ } catch (error) {
154
+ consola.warn(`Failed to initialize git: ${error}`);
155
+ }
156
+ } else {
157
+ consola.warn('Git is not installed, skipping git initialization');
158
+ }
159
+ }
160
+
161
+ // Install dependencies
162
+ if (shouldInstall) {
163
+ try {
164
+ await installDependencies(targetDir);
165
+ consola.success('Installed dependencies');
166
+ } catch (error) {
167
+ consola.warn(`Failed to install dependencies: ${error}`);
168
+ consola.info('You can install them manually with: bun install');
169
+ }
170
+ }
171
+
172
+ // Print success message
173
+ console.log('');
174
+ consola.box({
175
+ title: pc.green('Success!'),
176
+ message: `Project ${pc.cyan(projectName)} created successfully`,
177
+ style: {
178
+ borderColor: 'green',
179
+ },
180
+ });
181
+
182
+ console.log('');
183
+ consola.info(`${pc.bold('Next steps:')}`);
184
+ console.log('');
185
+ console.log(` ${pc.cyan('cd')} ${projectName}`);
186
+
187
+ if (!shouldInstall) {
188
+ console.log(` ${pc.cyan('bun install')}`);
189
+ }
190
+
191
+ console.log(` ${pc.cyan('bun run scripts/setup-project.ts')} ${pc.dim('# Configure environment')}`);
192
+ console.log(` ${pc.cyan('bun run dev')}`);
193
+ console.log('');
194
+ consola.info(`Visit ${pc.cyan('http://localhost:5173')} to see your app`);
195
+ console.log('');
196
+ },
197
+ });
@@ -0,0 +1,2 @@
1
+ export { createCommand } from './create';
2
+ export { templatesCommand } from './templates';
@@ -0,0 +1,32 @@
1
+ import { defineCommand } from 'citty';
2
+ import consola from 'consola';
3
+ import pc from 'picocolors';
4
+ import { listTemplates } from '../lib/template';
5
+
6
+ export const templatesCommand = defineCommand({
7
+ meta: {
8
+ name: 'templates',
9
+ description: 'List available templates',
10
+ },
11
+ async run() {
12
+ console.log('');
13
+ consola.info(`${pc.bold('Available templates:')}`);
14
+ console.log('');
15
+
16
+ const templates = await listTemplates();
17
+
18
+ for (const template of templates) {
19
+ console.log(` ${pc.cyan(pc.bold(template.name))} - ${template.displayName}`);
20
+ console.log(` ${pc.dim(template.description)}`);
21
+ console.log('');
22
+ console.log(` ${pc.dim('Features:')}`);
23
+ for (const feature of template.features) {
24
+ console.log(` ${pc.dim('•')} ${feature}`);
25
+ }
26
+ console.log('');
27
+ }
28
+
29
+ consola.info(`Create a project with: ${pc.cyan('a3 create my-app --template kysely')}`);
30
+ console.log('');
31
+ },
32
+ });
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bun
2
+ import { defineCommand, runMain } from 'citty';
3
+ import { createCommand } from './commands/create';
4
+ import { templatesCommand } from './commands/templates';
5
+
6
+ const main = defineCommand({
7
+ meta: {
8
+ name: 'a3',
9
+ version: '0.1.0',
10
+ description: 'A3 Stack CLI - Create modern full-stack apps with SvelteKit, Better Auth, and Kysely',
11
+ },
12
+ subCommands: {
13
+ create: createCommand,
14
+ templates: templatesCommand,
15
+ },
16
+ });
17
+
18
+ runMain(main);
package/src/lib/git.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { $ } from 'bun';
2
+
3
+ /**
4
+ * Check if git is installed
5
+ */
6
+ export async function isGitInstalled(): Promise<boolean> {
7
+ try {
8
+ await $`git --version`.quiet();
9
+ return true;
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Initialize a git repository
17
+ */
18
+ export async function initGit(dir: string): Promise<void> {
19
+ await $`git init`.cwd(dir).quiet();
20
+ await $`git add -A`.cwd(dir).quiet();
21
+ await $`git commit -m "Initial commit from a3-cli"`.cwd(dir).quiet();
22
+ }
23
+
24
+ /**
25
+ * Check if a directory is a git repository
26
+ */
27
+ export async function isGitRepo(dir: string): Promise<boolean> {
28
+ try {
29
+ await $`git rev-parse --is-inside-work-tree`.cwd(dir).quiet();
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
@@ -0,0 +1,4 @@
1
+ // Re-export all library functions for use as a module
2
+ export * from './template';
3
+ export * from './git';
4
+ export * from './setup';
@@ -0,0 +1,45 @@
1
+ import { $ } from 'bun';
2
+ import consola from 'consola';
3
+
4
+ /**
5
+ * Install dependencies using bun
6
+ */
7
+ export async function installDependencies(dir: string): Promise<void> {
8
+ consola.start('Installing dependencies...');
9
+ await $`bun install`.cwd(dir);
10
+ }
11
+
12
+ /**
13
+ * Run post-install script if it exists
14
+ */
15
+ export async function runPostInstall(dir: string, script?: string): Promise<void> {
16
+ if (!script) {
17
+ // Default to running setup-project.ts
18
+ script = 'bun run scripts/setup-project.ts';
19
+ }
20
+
21
+ try {
22
+ consola.start('Running post-install setup...');
23
+ const [cmd, ...args] = script.split(' ');
24
+ await $`${cmd} ${args}`.cwd(dir);
25
+ consola.success('Post-install setup complete');
26
+ } catch (error) {
27
+ // Post-install is optional
28
+ consola.warn('Post-install script failed or not found');
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Detect the preferred package manager
34
+ */
35
+ export function detectPackageManager(): 'bun' | 'npm' | 'pnpm' | 'yarn' {
36
+ // Check for bun.lockb
37
+ const userAgent = process.env.npm_config_user_agent || '';
38
+
39
+ if (userAgent.includes('bun')) return 'bun';
40
+ if (userAgent.includes('pnpm')) return 'pnpm';
41
+ if (userAgent.includes('yarn')) return 'yarn';
42
+
43
+ // Default to bun for this CLI
44
+ return 'bun';
45
+ }
@@ -0,0 +1,115 @@
1
+ import { downloadTemplate as gigetDownload } from 'giget';
2
+ import { join } from 'path';
3
+ import { readFile, writeFile, readdir } from 'fs/promises';
4
+
5
+ export interface Template {
6
+ name: string;
7
+ displayName: string;
8
+ description: string;
9
+ features: string[];
10
+ icon: string;
11
+ version: string;
12
+ postInstall?: string;
13
+ }
14
+
15
+ export interface TemplateVariables {
16
+ projectName: string;
17
+ [key: string]: string;
18
+ }
19
+
20
+ const GITHUB_REPO = 'AdamAugustinsky/a3-stack';
21
+
22
+ /**
23
+ * List available templates
24
+ * TODO: In the future, fetch this from the GitHub API
25
+ */
26
+ export async function listTemplates(): Promise<Template[]> {
27
+ return [
28
+ {
29
+ name: 'kysely',
30
+ displayName: 'A3 Stack + Kysely',
31
+ description: 'SvelteKit 5 + Better Auth + Kysely with PostgreSQL',
32
+ features: [
33
+ 'Type-safe SQL with Kysely',
34
+ 'PostgreSQL with Docker',
35
+ 'Better Auth authentication',
36
+ 'Organization/multi-tenant support',
37
+ 'shadcn-svelte components',
38
+ 'TailwindCSS v4',
39
+ 'Remote functions (experimental)',
40
+ 'Atlas database migrations',
41
+ ],
42
+ icon: 'database',
43
+ version: '1.0.0',
44
+ postInstall: 'bun run scripts/setup-project.ts',
45
+ },
46
+ ];
47
+ }
48
+
49
+ /**
50
+ * Download a template from GitHub
51
+ */
52
+ export async function downloadTemplate(
53
+ templateName: string,
54
+ targetDir: string
55
+ ): Promise<void> {
56
+ await gigetDownload(`github:${GITHUB_REPO}/templates/${templateName}`, {
57
+ dir: targetDir,
58
+ force: false,
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Process template by replacing placeholders
64
+ */
65
+ export async function processTemplate(
66
+ dir: string,
67
+ variables: TemplateVariables
68
+ ): Promise<void> {
69
+ // Update package.json name
70
+ const pkgPath = join(dir, 'package.json');
71
+ let pkgContent = await readFile(pkgPath, 'utf-8');
72
+
73
+ // Replace all template variables
74
+ for (const [key, value] of Object.entries(variables)) {
75
+ const placeholder = `{{${key}}}`;
76
+ pkgContent = pkgContent.replace(new RegExp(placeholder, 'g'), value);
77
+ }
78
+
79
+ await writeFile(pkgPath, pkgContent);
80
+
81
+ // Remove template.json if it exists
82
+ try {
83
+ const { unlink } = await import('fs/promises');
84
+ await unlink(join(dir, 'template.json'));
85
+ } catch {
86
+ // template.json might not exist, that's fine
87
+ }
88
+
89
+ // Remove .claude directory if it exists (AI assistant specific configs)
90
+ try {
91
+ const { rm } = await import('fs/promises');
92
+ await rm(join(dir, '.claude'), { recursive: true, force: true });
93
+ } catch {
94
+ // .claude might not exist, that's fine
95
+ }
96
+
97
+ // Remove CLAUDE.md, AGENTS.md if they exist
98
+ const filesToRemove = ['CLAUDE.md', 'AGENTS.md', 'SVELTE5-BOUNDARY-REFACTOR-GUIDE.md'];
99
+ for (const file of filesToRemove) {
100
+ try {
101
+ const { unlink } = await import('fs/promises');
102
+ await unlink(join(dir, file));
103
+ } catch {
104
+ // File might not exist, that's fine
105
+ }
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Get template metadata
111
+ */
112
+ export async function getTemplateInfo(templateName: string): Promise<Template | null> {
113
+ const templates = await listTemplates();
114
+ return templates.find((t) => t.name === templateName) || null;
115
+ }