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.
Files changed (98) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +108 -0
  3. package/package.json +3 -2
  4. package/src/index.ts +0 -0
  5. package/src/prompts.test.ts +83 -0
  6. package/src/prompts.ts +3 -3
  7. package/src/scaffold.test.ts +138 -0
  8. package/src/scaffold.ts +1 -1
  9. package/src/utils.test.ts +37 -0
  10. package/templates/base/AGENTS.md.hbs +98 -0
  11. package/templates/base/Makefile.hbs +276 -0
  12. package/templates/base/README.md.hbs +88 -0
  13. package/templates/base/SECURITY.md.hbs +37 -0
  14. package/templates/base/lefthook.yml.hbs +44 -0
  15. package/templates/mobile/analysis_options.yaml +199 -0
  16. package/templates/mobile/android/app/build.gradle.kts +44 -0
  17. package/templates/mobile/android/app/src/debug/AndroidManifest.xml +7 -0
  18. package/templates/mobile/android/app/src/main/AndroidManifest.xml +45 -0
  19. package/templates/mobile/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +24 -0
  20. package/templates/mobile/android/app/src/main/kotlin/com/turniza/holidays/holidays/MainActivity.kt +5 -0
  21. package/templates/mobile/android/app/src/main/res/drawable/launch_background.xml +12 -0
  22. package/templates/mobile/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  23. package/templates/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  24. package/templates/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  25. package/templates/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  26. package/templates/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  27. package/templates/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  28. package/templates/mobile/android/app/src/main/res/values/styles.xml +18 -0
  29. package/templates/mobile/android/app/src/main/res/values-night/styles.xml +18 -0
  30. package/templates/mobile/android/app/src/profile/AndroidManifest.xml +7 -0
  31. package/templates/mobile/android/build.gradle.kts +24 -0
  32. package/templates/mobile/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  33. package/templates/mobile/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  34. package/templates/mobile/android/gradle.properties +2 -0
  35. package/templates/mobile/android/gradlew +160 -0
  36. package/templates/mobile/android/gradlew.bat +90 -0
  37. package/templates/mobile/android/settings.gradle.kts +26 -0
  38. package/templates/mobile/integration_test/app_test.dart +16 -0
  39. package/templates/mobile/ios/Flutter/AppFrameworkInfo.plist +26 -0
  40. package/templates/mobile/ios/Flutter/Debug.xcconfig +2 -0
  41. package/templates/mobile/ios/Flutter/Flutter.podspec +18 -0
  42. package/templates/mobile/ios/Flutter/Release.xcconfig +2 -0
  43. package/templates/mobile/ios/Podfile +43 -0
  44. package/templates/mobile/ios/Runner/AppDelegate.swift +13 -0
  45. package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
  46. package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
  47. package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
  48. package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
  49. package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
  50. package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
  51. package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
  52. package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
  53. package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
  54. package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
  55. package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
  56. package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
  57. package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
  58. package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
  59. package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
  60. package/templates/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
  61. package/templates/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +23 -0
  62. package/templates/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
  63. package/templates/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
  64. package/templates/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
  65. package/templates/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +5 -0
  66. package/templates/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard +37 -0
  67. package/templates/mobile/ios/Runner/Base.lproj/Main.storyboard +26 -0
  68. package/templates/mobile/ios/Runner/GeneratedPluginRegistrant.h +19 -0
  69. package/templates/mobile/ios/Runner/GeneratedPluginRegistrant.m +21 -0
  70. package/templates/mobile/ios/Runner/Info.plist +49 -0
  71. package/templates/mobile/ios/Runner/Runner-Bridging-Header.h +1 -0
  72. package/templates/mobile/ios/Runner.xcodeproj/project.pbxproj +728 -0
  73. package/templates/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  74. package/templates/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  75. package/templates/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  76. package/templates/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +101 -0
  77. package/templates/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata +10 -0
  78. package/templates/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  79. package/templates/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  80. package/templates/mobile/ios/RunnerTests/RunnerTests.swift +12 -0
  81. package/templates/mobile/lib/main.dart +64 -0
  82. package/templates/mobile/pubspec.yaml.hbs +23 -0
  83. package/templates/mobile/test/widget_test.dart +19 -0
  84. package/templates/web/.prettierrc +16 -0
  85. package/templates/web/README.md +43 -0
  86. package/templates/web/astro.config.mjs +13 -0
  87. package/templates/web/e2e/homepage.spec.ts +6 -0
  88. package/templates/web/eslint.config.mjs +42 -0
  89. package/templates/web/package.json.hbs +34 -0
  90. package/templates/web/playwright.config.ts +29 -0
  91. package/templates/web/public/favicon.ico +0 -0
  92. package/templates/web/public/favicon.svg +9 -0
  93. package/templates/web/src/pages/index.astro +17 -0
  94. package/templates/web/src/utils/format.test.ts +9 -0
  95. package/templates/web/src/utils/format.ts +7 -0
  96. package/templates/web/tsconfig.json +5 -0
  97. package/templates/web/vitest.config.ts +16 -0
  98. 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.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, '../../templates');
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