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 +51 -0
- package/src/commands/create.ts +197 -0
- package/src/commands/index.ts +2 -0
- package/src/commands/templates.ts +32 -0
- package/src/index.ts +18 -0
- package/src/lib/git.ts +34 -0
- package/src/lib/index.ts +4 -0
- package/src/lib/setup.ts +45 -0
- package/src/lib/template.ts +115 -0
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,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
|
+
}
|
package/src/lib/index.ts
ADDED
package/src/lib/setup.ts
ADDED
|
@@ -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
|
+
}
|