create-turniza-app 1.0.0 → 1.0.2
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/LICENSE.md +21 -0
- package/README.md +108 -0
- package/package.json +3 -2
- package/src/index.ts +0 -0
- package/src/prompts.test.ts +83 -0
- package/src/prompts.ts +3 -3
- package/src/scaffold.test.ts +138 -0
- package/src/scaffold.ts +1 -1
- package/src/utils.test.ts +37 -0
- package/templates/base/AGENTS.md.hbs +98 -0
- package/templates/base/Makefile.hbs +276 -0
- package/templates/base/README.md.hbs +88 -0
- package/templates/base/SECURITY.md.hbs +37 -0
- package/templates/base/lefthook.yml.hbs +44 -0
- package/templates/mobile/analysis_options.yaml +199 -0
- package/templates/mobile/android/app/build.gradle.kts +44 -0
- package/templates/mobile/android/app/src/debug/AndroidManifest.xml +7 -0
- package/templates/mobile/android/app/src/main/AndroidManifest.xml +45 -0
- package/templates/mobile/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +24 -0
- package/templates/mobile/android/app/src/main/kotlin/com/turniza/holidays/holidays/MainActivity.kt +5 -0
- package/templates/mobile/android/app/src/main/res/drawable/launch_background.xml +12 -0
- package/templates/mobile/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
- package/templates/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/templates/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/templates/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/templates/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/templates/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/templates/mobile/android/app/src/main/res/values/styles.xml +18 -0
- package/templates/mobile/android/app/src/main/res/values-night/styles.xml +18 -0
- package/templates/mobile/android/app/src/profile/AndroidManifest.xml +7 -0
- package/templates/mobile/android/build.gradle.kts +24 -0
- package/templates/mobile/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/templates/mobile/android/gradle/wrapper/gradle-wrapper.properties +5 -0
- package/templates/mobile/android/gradle.properties +2 -0
- package/templates/mobile/android/gradlew +160 -0
- package/templates/mobile/android/gradlew.bat +90 -0
- package/templates/mobile/android/settings.gradle.kts +26 -0
- package/templates/mobile/integration_test/app_test.dart +16 -0
- package/templates/mobile/ios/Flutter/AppFrameworkInfo.plist +26 -0
- package/templates/mobile/ios/Flutter/Debug.xcconfig +2 -0
- package/templates/mobile/ios/Flutter/Flutter.podspec +18 -0
- package/templates/mobile/ios/Flutter/Release.xcconfig +2 -0
- package/templates/mobile/ios/Podfile +43 -0
- package/templates/mobile/ios/Runner/AppDelegate.swift +13 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +23 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
- package/templates/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +5 -0
- package/templates/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard +37 -0
- package/templates/mobile/ios/Runner/Base.lproj/Main.storyboard +26 -0
- package/templates/mobile/ios/Runner/GeneratedPluginRegistrant.h +19 -0
- package/templates/mobile/ios/Runner/GeneratedPluginRegistrant.m +21 -0
- package/templates/mobile/ios/Runner/Info.plist +49 -0
- package/templates/mobile/ios/Runner/Runner-Bridging-Header.h +1 -0
- package/templates/mobile/ios/Runner.xcodeproj/project.pbxproj +728 -0
- package/templates/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/templates/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/templates/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
- package/templates/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +101 -0
- package/templates/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata +10 -0
- package/templates/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/templates/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
- package/templates/mobile/ios/RunnerTests/RunnerTests.swift +12 -0
- package/templates/mobile/lib/main.dart +64 -0
- package/templates/mobile/pubspec.yaml.hbs +23 -0
- package/templates/mobile/test/widget_test.dart +19 -0
- package/templates/web/.prettierrc +16 -0
- package/templates/web/README.md +43 -0
- package/templates/web/astro.config.mjs +13 -0
- package/templates/web/e2e/homepage.spec.ts +6 -0
- package/templates/web/eslint.config.mjs +42 -0
- package/templates/web/package.json.hbs +34 -0
- package/templates/web/playwright.config.ts +29 -0
- package/templates/web/public/favicon.ico +0 -0
- package/templates/web/public/favicon.svg +9 -0
- package/templates/web/src/pages/index.astro +17 -0
- package/templates/web/src/utils/format.test.ts +9 -0
- package/templates/web/src/utils/format.ts +7 -0
- package/templates/web/tsconfig.json +5 -0
- package/templates/web/vitest.config.ts +16 -0
- package/templates/web/wrangler.jsonc.hbs +7 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright © 2025 Turniza
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# 🚀 create-turniza-app
|
|
2
|
+
|
|
3
|
+
A CLI scaffold for creating new **Turniza monorepo apps** with Flutter mobile + Astro web (Cloudflare Workers), pre-configured with a full DevSecOps pipeline.
|
|
4
|
+
|
|
5
|
+
## What You Get
|
|
6
|
+
|
|
7
|
+
A production-ready monorepo with:
|
|
8
|
+
|
|
9
|
+
| Feature | Details |
|
|
10
|
+
|---------|---------|
|
|
11
|
+
| **Flutter mobile** | iOS + Android with 90+ lint rules + DCM |
|
|
12
|
+
| **Astro web** | SSR on Cloudflare Workers |
|
|
13
|
+
| **Makefile** | Single source of truth for all commands |
|
|
14
|
+
| **Pre-commit hooks** | Lefthook → secrets, format, lint |
|
|
15
|
+
| **Testing** | flutter_test, Vitest, Playwright |
|
|
16
|
+
| **Security** | Secrets detection, dependency audit, license check |
|
|
17
|
+
| **AI Agent context** | AGENTS.md, CLAUDE.md, GEMINI.md auto-generated |
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# With bun
|
|
23
|
+
bunx create-turniza-app my-app
|
|
24
|
+
|
|
25
|
+
# With npx
|
|
26
|
+
npx create-turniza-app my-app
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The CLI will ask you:
|
|
30
|
+
|
|
31
|
+
1. **Project name** (kebab-case)
|
|
32
|
+
2. **Organization** (default: Turniza)
|
|
33
|
+
3. **GitHub org/user** (default: Turniza)
|
|
34
|
+
4. **Emoji** for your project
|
|
35
|
+
5. **Description** of the project
|
|
36
|
+
6. **Include mobile?** (Flutter – default: yes)
|
|
37
|
+
7. **Include web?** (Astro + CF Workers – default: yes)
|
|
38
|
+
8. **Init git?** (default: yes)
|
|
39
|
+
9. **Install deps?** (default: yes)
|
|
40
|
+
|
|
41
|
+
## Generated Project Structure
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
my-app/
|
|
45
|
+
├── apps/
|
|
46
|
+
│ ├── mobile/ Flutter app (iOS + Android)
|
|
47
|
+
│ └── web/ Astro app (Cloudflare Workers SSR)
|
|
48
|
+
├── packages/ Shared code (future)
|
|
49
|
+
├── Makefile Developer commands
|
|
50
|
+
├── lefthook.yml Pre-commit hooks
|
|
51
|
+
├── .gitignore Comprehensive ignore rules
|
|
52
|
+
├── README.md Project documentation
|
|
53
|
+
├── SECURITY.md Vulnerability disclosure policy
|
|
54
|
+
├── AGENTS.md AI agent instructions
|
|
55
|
+
├── CLAUDE.md AI agent instructions (Claude)
|
|
56
|
+
└── GEMINI.md AI agent instructions (Gemini)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Prerequisites
|
|
60
|
+
|
|
61
|
+
| Tool | Install |
|
|
62
|
+
|------|---------|
|
|
63
|
+
| Bun | `nix profile install nixpkgs#bun` |
|
|
64
|
+
| Flutter | `nix profile install nixpkgs#flutter` (if using mobile) |
|
|
65
|
+
| DCM | `brew install dcm` (optional, for Dart metrics) |
|
|
66
|
+
| Lefthook | `brew install lefthook` |
|
|
67
|
+
|
|
68
|
+
## Development
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Clone this repo
|
|
72
|
+
git clone git@github.com:Turniza/create-turniza-app.git
|
|
73
|
+
cd create-turniza-app
|
|
74
|
+
|
|
75
|
+
# Install deps
|
|
76
|
+
bun install
|
|
77
|
+
|
|
78
|
+
# Test locally
|
|
79
|
+
bun run src/index.ts my-test-app
|
|
80
|
+
|
|
81
|
+
# Run tests
|
|
82
|
+
make test
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Publishing
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Login to npm (first time only)
|
|
89
|
+
npm login
|
|
90
|
+
|
|
91
|
+
# Dry run
|
|
92
|
+
make publish-dry
|
|
93
|
+
|
|
94
|
+
# Publish
|
|
95
|
+
make publish
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
After publishing, anyone can run:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
bunx create-turniza-app my-project
|
|
102
|
+
# or
|
|
103
|
+
npx create-turniza-app my-project
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT – © Turniza
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-turniza-app",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "CLI scaffold for creating new Turniza monorepo apps (Flutter + Astro + Cloudflare Workers)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"dev": "bun run src/index.ts",
|
|
11
|
+
"test": "bun test src/",
|
|
11
12
|
"typecheck": "tsc --noEmit"
|
|
12
13
|
},
|
|
13
14
|
"dependencies": {
|
|
@@ -43,4 +44,4 @@
|
|
|
43
44
|
"node": ">=18",
|
|
44
45
|
"bun": ">=1.0"
|
|
45
46
|
}
|
|
46
|
-
}
|
|
47
|
+
}
|
package/src/index.ts
CHANGED
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { validateProjectName, toTitleCase, toSnakeCase } from './prompts.js';
|
|
3
|
+
|
|
4
|
+
// ── validateProjectName ──────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
describe('validateProjectName', () => {
|
|
7
|
+
test('accepts valid kebab-case names', () => {
|
|
8
|
+
expect(validateProjectName('my-app')).toBe(true);
|
|
9
|
+
expect(validateProjectName('a')).toBe(true);
|
|
10
|
+
expect(validateProjectName('app123')).toBe(true);
|
|
11
|
+
expect(validateProjectName('my-cool-app-2')).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('rejects empty string', () => {
|
|
15
|
+
expect(validateProjectName('')).toBe('Project name is required');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('rejects names starting with a number', () => {
|
|
19
|
+
const result = validateProjectName('1app');
|
|
20
|
+
expect(result).toBeString();
|
|
21
|
+
expect(result).not.toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('rejects names with uppercase letters', () => {
|
|
25
|
+
const result = validateProjectName('MyApp');
|
|
26
|
+
expect(result).toBeString();
|
|
27
|
+
expect(result).not.toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('rejects names with special characters', () => {
|
|
31
|
+
expect(validateProjectName('my_app')).not.toBe(true);
|
|
32
|
+
expect(validateProjectName('my.app')).not.toBe(true);
|
|
33
|
+
expect(validateProjectName('my app')).not.toBe(true);
|
|
34
|
+
expect(validateProjectName('my@app')).not.toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('rejects names longer than 50 characters', () => {
|
|
38
|
+
const longName = 'a' + '-bcd'.repeat(20); // 81 chars
|
|
39
|
+
const result = validateProjectName(longName);
|
|
40
|
+
expect(result).toBe('Must be 50 characters or less');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('accepts name exactly 50 characters', () => {
|
|
44
|
+
const name = 'a'.repeat(50);
|
|
45
|
+
expect(validateProjectName(name)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ── toTitleCase ──────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
describe('toTitleCase', () => {
|
|
52
|
+
test('converts kebab-case to Title Case', () => {
|
|
53
|
+
expect(toTitleCase('my-cool-app')).toBe('My Cool App');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('converts snake_case to Title Case', () => {
|
|
57
|
+
expect(toTitleCase('my_cool_app')).toBe('My Cool App');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('handles single word', () => {
|
|
61
|
+
expect(toTitleCase('app')).toBe('App');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('handles mixed delimiters', () => {
|
|
65
|
+
expect(toTitleCase('my-cool_app')).toBe('My Cool App');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ── toSnakeCase ─────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
describe('toSnakeCase', () => {
|
|
72
|
+
test('converts kebab-case to snake_case', () => {
|
|
73
|
+
expect(toSnakeCase('my-cool-app')).toBe('my_cool_app');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('leaves snake_case unchanged', () => {
|
|
77
|
+
expect(toSnakeCase('my_app')).toBe('my_app');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('leaves single word unchanged', () => {
|
|
81
|
+
expect(toSnakeCase('app')).toBe('app');
|
|
82
|
+
});
|
|
83
|
+
});
|
package/src/prompts.ts
CHANGED
|
@@ -16,18 +16,18 @@ export interface ProjectOptions {
|
|
|
16
16
|
compatibilityDate: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function toTitleCase(str: string): string {
|
|
19
|
+
export function toTitleCase(str: string): string {
|
|
20
20
|
return str
|
|
21
21
|
.split(/[-_]/)
|
|
22
22
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
23
23
|
.join(' ');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function toSnakeCase(str: string): string {
|
|
26
|
+
export function toSnakeCase(str: string): string {
|
|
27
27
|
return str.replace(/-/g, '_');
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function validateProjectName(name: string): boolean | string {
|
|
30
|
+
export function validateProjectName(name: string): boolean | string {
|
|
31
31
|
if (!name) return 'Project name is required';
|
|
32
32
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
33
33
|
return 'Must be lowercase, start with a letter, and contain only letters, numbers, and hyphens';
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import { tmpdir } from 'os';
|
|
3
|
+
import { mkdtemp, rm, readFile, writeFile, mkdir, readdir, stat } from 'fs/promises';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { scaffoldProject } from './scaffold.js';
|
|
6
|
+
import type { ProjectOptions } from './prompts.js';
|
|
7
|
+
|
|
8
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function makeOptions(overrides: Partial<ProjectOptions> = {}): ProjectOptions {
|
|
11
|
+
return {
|
|
12
|
+
projectName: 'test-project',
|
|
13
|
+
projectTitle: 'Test Project',
|
|
14
|
+
projectDescription: 'A test project',
|
|
15
|
+
projectNameSnake: 'test_project',
|
|
16
|
+
org: 'TestOrg',
|
|
17
|
+
orgDomain: 'testorg.com',
|
|
18
|
+
githubOrg: 'TestOrg',
|
|
19
|
+
emoji: '🧪',
|
|
20
|
+
includeMobile: false,
|
|
21
|
+
includeWeb: false,
|
|
22
|
+
initGit: false,
|
|
23
|
+
installDeps: false,
|
|
24
|
+
compatibilityDate: '2026-01-01',
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let tmpDir: string;
|
|
30
|
+
|
|
31
|
+
beforeEach(async () => {
|
|
32
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'scaffold-test-'));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(async () => {
|
|
36
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ── scaffoldProject ─────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
describe('scaffoldProject', () => {
|
|
42
|
+
test('creates base project structure', async () => {
|
|
43
|
+
const origCwd = process.cwd();
|
|
44
|
+
process.chdir(tmpDir);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
await scaffoldProject(makeOptions());
|
|
48
|
+
} finally {
|
|
49
|
+
process.chdir(origCwd);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const projectDir = join(tmpDir, 'test-project');
|
|
53
|
+
const dirStat = await stat(projectDir);
|
|
54
|
+
expect(dirStat.isDirectory()).toBe(true);
|
|
55
|
+
|
|
56
|
+
// packages/.gitkeep should always exist
|
|
57
|
+
const gitkeep = join(projectDir, 'packages', '.gitkeep');
|
|
58
|
+
const gitkeepStat = await stat(gitkeep);
|
|
59
|
+
expect(gitkeepStat.isFile()).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('throws when target directory already exists', async () => {
|
|
63
|
+
const origCwd = process.cwd();
|
|
64
|
+
process.chdir(tmpDir);
|
|
65
|
+
|
|
66
|
+
// Create the directory upfront
|
|
67
|
+
await mkdir(join(tmpDir, 'existing-project'));
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await expect(
|
|
71
|
+
scaffoldProject(makeOptions({ projectName: 'existing-project' })),
|
|
72
|
+
).rejects.toThrow('already exists');
|
|
73
|
+
} finally {
|
|
74
|
+
process.chdir(origCwd);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('renders Handlebars templates with project context', async () => {
|
|
79
|
+
const origCwd = process.cwd();
|
|
80
|
+
process.chdir(tmpDir);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await scaffoldProject(makeOptions({ projectName: 'hbs-test' }));
|
|
84
|
+
} finally {
|
|
85
|
+
process.chdir(origCwd);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const projectDir = join(tmpDir, 'hbs-test');
|
|
89
|
+
|
|
90
|
+
// README.md should exist (from README.md.hbs) and contain the project name
|
|
91
|
+
const readmePath = join(projectDir, 'README.md');
|
|
92
|
+
const readme = await readFile(readmePath, 'utf-8');
|
|
93
|
+
expect(readme).toContain('hbs-test');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('creates mobile directory only when includeMobile is true', async () => {
|
|
97
|
+
const origCwd = process.cwd();
|
|
98
|
+
process.chdir(tmpDir);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await scaffoldProject(makeOptions({ projectName: 'no-mobile', includeMobile: false }));
|
|
102
|
+
} finally {
|
|
103
|
+
process.chdir(origCwd);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const mobileDir = join(tmpDir, 'no-mobile', 'apps', 'mobile');
|
|
107
|
+
await expect(stat(mobileDir)).rejects.toThrow();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('creates web directory only when includeWeb is true', async () => {
|
|
111
|
+
const origCwd = process.cwd();
|
|
112
|
+
process.chdir(tmpDir);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await scaffoldProject(makeOptions({ projectName: 'no-web', includeWeb: false }));
|
|
116
|
+
} finally {
|
|
117
|
+
process.chdir(origCwd);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const webDir = join(tmpDir, 'no-web', 'apps', 'web');
|
|
121
|
+
await expect(stat(webDir)).rejects.toThrow();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('creates mobile directory when includeMobile is true', async () => {
|
|
125
|
+
const origCwd = process.cwd();
|
|
126
|
+
process.chdir(tmpDir);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await scaffoldProject(makeOptions({ projectName: 'with-mobile', includeMobile: true }));
|
|
130
|
+
} finally {
|
|
131
|
+
process.chdir(origCwd);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const mobileDir = join(tmpDir, 'with-mobile', 'apps', 'mobile');
|
|
135
|
+
const mobileStat = await stat(mobileDir);
|
|
136
|
+
expect(mobileStat.isDirectory()).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
});
|
package/src/scaffold.ts
CHANGED
|
@@ -5,7 +5,7 @@ import chalk from 'chalk';
|
|
|
5
5
|
import type { ProjectOptions } from './prompts.js';
|
|
6
6
|
import { exec } from './utils.js';
|
|
7
7
|
|
|
8
|
-
const TEMPLATES_DIR = resolve(import.meta.dirname, '
|
|
8
|
+
const TEMPLATES_DIR = resolve(import.meta.dirname, '../templates');
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Main scaffold function: copies templates, processes Handlebars, and runs post-setup.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { tmpdir } from 'os';
|
|
3
|
+
import { mkdtemp } from 'fs/promises';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { exec } from './utils.js';
|
|
6
|
+
|
|
7
|
+
describe('exec', () => {
|
|
8
|
+
test('returns trimmed stdout on success', async () => {
|
|
9
|
+
const result = await exec('echo hello');
|
|
10
|
+
expect(result).toBe('hello');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('trims trailing whitespace from output', async () => {
|
|
14
|
+
const result = await exec('printf " spaced "');
|
|
15
|
+
expect(result).toBe('spaced');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('rejects on non-zero exit code', async () => {
|
|
19
|
+
await expect(exec('exit 1')).rejects.toThrow('failed (exit 1)');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('rejects with stderr content in error message', async () => {
|
|
23
|
+
await expect(exec('echo "oops" >&2 && exit 1')).rejects.toThrow('oops');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('respects cwd option', async () => {
|
|
27
|
+
const tmp = await mkdtemp(join(tmpdir(), 'exec-test-'));
|
|
28
|
+
const result = await exec('pwd', { cwd: tmp });
|
|
29
|
+
// macOS /private/tmp vs /tmp – resolve both
|
|
30
|
+
expect(result).toContain(tmp.replace('/private', ''));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('handles multi-line output', async () => {
|
|
34
|
+
const result = await exec('printf "line1\\nline2"');
|
|
35
|
+
expect(result).toBe('line1\nline2');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# AGENTS.md – AI Agent Instructions
|
|
2
|
+
|
|
3
|
+
> This file provides context for AI coding agents (Claude, Gemini, etc.) working on this repository.
|
|
4
|
+
|
|
5
|
+
## Repository Overview
|
|
6
|
+
|
|
7
|
+
**{{projectTitle}}** – {{projectDescription}}
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
{{projectName}}/
|
|
11
|
+
{{#if includeMobile}}
|
|
12
|
+
├── apps/mobile/ ← Flutter (iOS + Android)
|
|
13
|
+
{{/if}}
|
|
14
|
+
{{#if includeWeb}}
|
|
15
|
+
├── apps/web/ ← Astro (Cloudflare Workers SSR)
|
|
16
|
+
{{/if}}
|
|
17
|
+
├── packages/ ← Shared code (future)
|
|
18
|
+
├── Makefile ← Single source of truth for all commands
|
|
19
|
+
└── lefthook.yml ← Pre-commit hooks (delegates to Makefile)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Technology Stack
|
|
23
|
+
|
|
24
|
+
| Component | Technology | Package Manager |
|
|
25
|
+
|-----------|-----------|----------------|
|
|
26
|
+
{{#if includeMobile}}
|
|
27
|
+
| Mobile | Flutter 3.38 / Dart | pub |
|
|
28
|
+
{{/if}}
|
|
29
|
+
{{#if includeWeb}}
|
|
30
|
+
| Web | Astro + TypeScript | bun |
|
|
31
|
+
| Web Deploy | Cloudflare Workers | wrangler |
|
|
32
|
+
{{/if}}
|
|
33
|
+
{{#if includeMobile}}
|
|
34
|
+
| Linting (Dart) | flutter_lints + DCM (integrated in analysis_options.yaml) | – |
|
|
35
|
+
{{/if}}
|
|
36
|
+
{{#if includeWeb}}
|
|
37
|
+
| Linting (Web) | ESLint (strict TS + Astro plugin) | – |
|
|
38
|
+
| Formatting | dart format + Prettier (Astro plugin) | – |
|
|
39
|
+
{{/if}}
|
|
40
|
+
{{#if includeMobile}}
|
|
41
|
+
| Testing (Mobile) | flutter_test + integration_test | – |
|
|
42
|
+
{{/if}}
|
|
43
|
+
{{#if includeWeb}}
|
|
44
|
+
| Testing (Web) | Vitest (unit) + Playwright (e2e) | – |
|
|
45
|
+
{{/if}}
|
|
46
|
+
| Pre-commit | Lefthook → Makefile targets | – |
|
|
47
|
+
|
|
48
|
+
## Commands Reference
|
|
49
|
+
|
|
50
|
+
All commands are defined in the Makefile. Run `make` or `make help` for the full list.
|
|
51
|
+
|
|
52
|
+
## Code Style & Conventions
|
|
53
|
+
|
|
54
|
+
{{#if includeMobile}}
|
|
55
|
+
### Dart / Flutter (`apps/mobile/`)
|
|
56
|
+
- **Strict analysis**: `analysis_options.yaml` with 90+ lint rules + DCM rules
|
|
57
|
+
- Prefer `const` constructors, `final` variables, single quotes
|
|
58
|
+
- Always use `require_trailing_commas`
|
|
59
|
+
- No `print()` – use proper logging
|
|
60
|
+
- Max cyclomatic complexity: 20, max function lines: 50
|
|
61
|
+
- One widget per file (private widgets excepted)
|
|
62
|
+
|
|
63
|
+
{{/if}}
|
|
64
|
+
{{#if includeWeb}}
|
|
65
|
+
### TypeScript / Astro (`apps/web/`)
|
|
66
|
+
- **Strict TypeScript**: enabled in `tsconfig.json`
|
|
67
|
+
- **ESLint**: strict TS rules (no `any`, no `eval`, no `console.log`)
|
|
68
|
+
- **Prettier**: single quotes, trailing commas, 100 char width
|
|
69
|
+
- Use `const` over `let`, never `var`
|
|
70
|
+
- Colocate tests: `*.test.ts` next to source files
|
|
71
|
+
|
|
72
|
+
{{/if}}
|
|
73
|
+
## CI Architecture
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
Makefile (single source of truth)
|
|
77
|
+
├── lefthook.yml → calls make targets on pre-commit
|
|
78
|
+
└── make ci → runs full pipeline locally
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
No GitHub Actions configured yet – all checks run locally.
|
|
82
|
+
|
|
83
|
+
## Branching & Commits
|
|
84
|
+
- Branch from `main`
|
|
85
|
+
- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:`, `security:`
|
|
86
|
+
- Pre-commit hooks run automatically via lefthook
|
|
87
|
+
|
|
88
|
+
## Security
|
|
89
|
+
- Never commit secrets, API keys, or tokens
|
|
90
|
+
- All dependencies are audited via `make security`
|
|
91
|
+
{{#if includeMobile}}
|
|
92
|
+
- DCM activated with license (stored in `~/.dcm/`, never in repo)
|
|
93
|
+
{{/if}}
|
|
94
|
+
- See `SECURITY.md` for disclosure policy
|
|
95
|
+
|
|
96
|
+
## Design
|
|
97
|
+
- UI design assisted via **Google Stitch** (MCP available)
|
|
98
|
+
- Always refer to the latest Stitch project for design reference
|