agent-flutter 0.1.3 → 0.1.5

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/README.md CHANGED
@@ -26,6 +26,24 @@ npx agent-flutter@latest sync --ide trae,codex,github
26
26
  npx agent-flutter@latest list --cwd /path/to/project
27
27
  ```
28
28
 
29
+ ## Bootstrap new Flutter project (script)
30
+
31
+ After `init`, run the script from the IDE-specific folder:
32
+
33
+ ```bash
34
+ bash .trae/scripts/bootstrap_flutter_template.sh
35
+ ```
36
+
37
+ Examples by IDE:
38
+
39
+ ```bash
40
+ bash .codex/scripts/bootstrap_flutter_template.sh
41
+ bash .cursor/scripts/bootstrap_flutter_template.sh
42
+ bash .windsurf/scripts/bootstrap_flutter_template.sh
43
+ bash .clinerules/scripts/bootstrap_flutter_template.sh
44
+ bash .github/scripts/bootstrap_flutter_template.sh
45
+ ```
46
+
29
47
  ## Publish to npm (one-time setup, Trusted Publishing)
30
48
 
31
49
  1. On npm package settings, add a **Trusted publisher**:
@@ -52,10 +70,9 @@ npm run release:major
52
70
 
53
71
  ## Installed files
54
72
 
55
- - Shared pack (for Trae/Codex/Cursor/Windsurf/Cline): `.agent-flutter/`
56
- - Trae: `.trae/`
57
- - Codex: `AGENTS.md`
58
- - Cursor: `.cursor/rules/agent-flutter.mdc`
59
- - Windsurf: `.windsurf/rules/agent-flutter.md`
60
- - Cline: `.clinerules/agent-flutter.md`
61
- - GitHub: `.github/skills/`, `.github/rules/`, `.github/copilot-instructions.md`
73
+ - Trae: `.trae/` (skills/rules/scripts)
74
+ - Codex: `.codex/` + `AGENTS.md`
75
+ - Cursor: `.cursor/skills/`, `.cursor/rules/shared/`, `.cursor/scripts/`, `.cursor/rules/agent-flutter.mdc`
76
+ - Windsurf: `.windsurf/skills/`, `.windsurf/rules/shared/`, `.windsurf/scripts/`, `.windsurf/rules/agent-flutter.md`
77
+ - Cline: `.clinerules/skills/`, `.clinerules/rules/`, `.clinerules/scripts/`, `.clinerules/agent-flutter.md`
78
+ - GitHub: `.github/skills/`, `.github/rules/`, `.github/scripts/`, `.github/copilot-instructions.md`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-flutter",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Portable Flutter skill/rule pack initializer for multiple AI IDEs.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -13,9 +13,9 @@ Usage:
13
13
  npx agent-flutter@latest list [--cwd <project_dir>]
14
14
 
15
15
  Commands:
16
- init Install shared Flutter skills/rules and IDE adapters.
17
- sync Update installed shared pack and adapters from latest template.
18
- list Print available skills/rules from the shared pack.
16
+ init Install Flutter skills/rules/scripts for selected IDE adapters.
17
+ sync Update installed adapters from latest template.
18
+ list Print available skills/rules from package template.
19
19
  `;
20
20
 
21
21
  export async function runCli(argv) {
@@ -142,45 +142,50 @@ async function applyPack({
142
142
  mode,
143
143
  }) {
144
144
  const verb = mode === 'sync' ? 'Synced' : 'Installed';
145
- const sharedPackIdes = new Set(['trae', 'codex', 'cursor', 'windsurf', 'cline']);
146
- const requiresSharedPack = [...ideTargets].some((ide) => sharedPackIdes.has(ide));
147
- const sharedTarget = path.join(projectRoot, '.agent-flutter');
148
- if (requiresSharedPack) {
149
- if ((await exists(sharedTarget)) && !force) {
150
- console.log(`Using existing shared pack: ${sharedTarget}`);
151
- } else {
152
- await copyTemplateDirectory({
153
- sourceDir: templateRoot,
154
- destinationDir: sharedTarget,
155
- projectRoot,
156
- force: true,
157
- });
158
- console.log(`${verb} shared pack: ${sharedTarget}`);
145
+ const syncDirectory = async ({ sourceDir, destinationDir, label }) => {
146
+ if ((await exists(destinationDir)) && !force) {
147
+ console.log(`Skipped ${label} (exists): ${destinationDir}`);
148
+ return;
159
149
  }
160
- }
150
+ await copyTemplateDirectory({
151
+ sourceDir,
152
+ destinationDir,
153
+ projectRoot,
154
+ force: true,
155
+ });
156
+ console.log(`${verb} ${label}: ${destinationDir}`);
157
+ };
161
158
 
162
159
  if (ideTargets.has('trae')) {
163
160
  const traeTarget = path.join(projectRoot, '.trae');
164
- if ((await exists(traeTarget)) && !force) {
165
- console.log(`Skipped Trae adapter (exists): ${traeTarget}`);
166
- } else {
167
- await copyTemplateDirectory({
168
- sourceDir: templateRoot,
169
- destinationDir: traeTarget,
170
- projectRoot,
171
- force: true,
172
- });
173
- console.log(`${verb} Trae adapter: ${traeTarget}`);
174
- }
161
+ await syncDirectory({
162
+ sourceDir: templateRoot,
163
+ destinationDir: traeTarget,
164
+ label: 'Trae adapter',
165
+ });
175
166
  }
176
167
 
177
- const metadataSource = requiresSharedPack
178
- ? sharedTarget
179
- : templateRoot;
180
- const skills = await loadSkillMetadata(path.join(metadataSource, 'skills'));
181
- const rules = await loadRuleMetadata(path.join(metadataSource, 'rules'));
168
+ const skills = await loadSkillMetadata(path.join(templateRoot, 'skills'));
169
+ const rules = await loadRuleMetadata(path.join(templateRoot, 'rules'));
182
170
 
183
171
  if (ideTargets.has('codex')) {
172
+ const codexRoot = path.join(projectRoot, '.codex');
173
+ await syncDirectory({
174
+ sourceDir: path.join(templateRoot, 'skills'),
175
+ destinationDir: path.join(codexRoot, 'skills'),
176
+ label: 'Codex skills',
177
+ });
178
+ await syncDirectory({
179
+ sourceDir: path.join(templateRoot, 'scripts'),
180
+ destinationDir: path.join(codexRoot, 'scripts'),
181
+ label: 'Codex scripts',
182
+ });
183
+ await syncDirectory({
184
+ sourceDir: path.join(templateRoot, 'rules'),
185
+ destinationDir: path.join(codexRoot, 'rules'),
186
+ label: 'Codex rules',
187
+ });
188
+
184
189
  const agentsPath = path.join(projectRoot, 'AGENTS.md');
185
190
  const written = await writeTextFile(
186
191
  agentsPath,
@@ -189,6 +194,7 @@ async function applyPack({
189
194
  projectName: path.basename(projectRoot),
190
195
  skills,
191
196
  rules,
197
+ packRoot: '.codex',
192
198
  }),
193
199
  { force },
194
200
  );
@@ -200,6 +206,22 @@ async function applyPack({
200
206
  }
201
207
 
202
208
  if (ideTargets.has('cursor')) {
209
+ await syncDirectory({
210
+ sourceDir: path.join(templateRoot, 'skills'),
211
+ destinationDir: path.join(projectRoot, '.cursor', 'skills'),
212
+ label: 'Cursor skills',
213
+ });
214
+ await syncDirectory({
215
+ sourceDir: path.join(templateRoot, 'scripts'),
216
+ destinationDir: path.join(projectRoot, '.cursor', 'scripts'),
217
+ label: 'Cursor scripts',
218
+ });
219
+ await syncDirectory({
220
+ sourceDir: path.join(templateRoot, 'rules'),
221
+ destinationDir: path.join(projectRoot, '.cursor', 'rules', 'shared'),
222
+ label: 'Cursor rules',
223
+ });
224
+
203
225
  const cursorPath = path.join(projectRoot, '.cursor', 'rules', 'agent-flutter.mdc');
204
226
  const written = await writeTextFile(
205
227
  cursorPath,
@@ -214,6 +236,22 @@ async function applyPack({
214
236
  }
215
237
 
216
238
  if (ideTargets.has('windsurf')) {
239
+ await syncDirectory({
240
+ sourceDir: path.join(templateRoot, 'skills'),
241
+ destinationDir: path.join(projectRoot, '.windsurf', 'skills'),
242
+ label: 'Windsurf skills',
243
+ });
244
+ await syncDirectory({
245
+ sourceDir: path.join(templateRoot, 'scripts'),
246
+ destinationDir: path.join(projectRoot, '.windsurf', 'scripts'),
247
+ label: 'Windsurf scripts',
248
+ });
249
+ await syncDirectory({
250
+ sourceDir: path.join(templateRoot, 'rules'),
251
+ destinationDir: path.join(projectRoot, '.windsurf', 'rules', 'shared'),
252
+ label: 'Windsurf rules',
253
+ });
254
+
217
255
  const windsurfPath = path.join(projectRoot, '.windsurf', 'rules', 'agent-flutter.md');
218
256
  const written = await writeTextFile(
219
257
  windsurfPath,
@@ -228,6 +266,22 @@ async function applyPack({
228
266
  }
229
267
 
230
268
  if (ideTargets.has('cline')) {
269
+ await syncDirectory({
270
+ sourceDir: path.join(templateRoot, 'skills'),
271
+ destinationDir: path.join(projectRoot, '.clinerules', 'skills'),
272
+ label: 'Cline skills',
273
+ });
274
+ await syncDirectory({
275
+ sourceDir: path.join(templateRoot, 'scripts'),
276
+ destinationDir: path.join(projectRoot, '.clinerules', 'scripts'),
277
+ label: 'Cline scripts',
278
+ });
279
+ await syncDirectory({
280
+ sourceDir: path.join(templateRoot, 'rules'),
281
+ destinationDir: path.join(projectRoot, '.clinerules', 'rules'),
282
+ label: 'Cline rules',
283
+ });
284
+
231
285
  const clinePath = path.join(projectRoot, '.clinerules', 'agent-flutter.md');
232
286
  const written = await writeTextFile(
233
287
  clinePath,
@@ -255,6 +309,19 @@ async function applyPack({
255
309
  console.log(`${verb} GitHub skills: ${githubSkillsPath}`);
256
310
  }
257
311
 
312
+ const githubScriptsPath = path.join(projectRoot, '.github', 'scripts');
313
+ if ((await exists(githubScriptsPath)) && !force) {
314
+ console.log(`Skipped GitHub scripts (exists): ${githubScriptsPath}`);
315
+ } else {
316
+ await copyTemplateDirectory({
317
+ sourceDir: path.join(templateRoot, 'scripts'),
318
+ destinationDir: githubScriptsPath,
319
+ projectRoot,
320
+ force: true,
321
+ });
322
+ console.log(`${verb} GitHub scripts: ${githubScriptsPath}`);
323
+ }
324
+
258
325
  const githubRulesPath = path.join(projectRoot, '.github', 'rules');
259
326
  if ((await exists(githubRulesPath)) && !force) {
260
327
  console.log(`Skipped GitHub rules (exists): ${githubRulesPath}`);
@@ -285,7 +352,12 @@ async function applyPack({
285
352
  async function detectInstalledIdeTargets(projectRoot) {
286
353
  const detected = new Set();
287
354
  if (await exists(path.join(projectRoot, '.trae'))) detected.add('trae');
288
- if (await exists(path.join(projectRoot, 'AGENTS.md'))) detected.add('codex');
355
+ if (
356
+ (await exists(path.join(projectRoot, 'AGENTS.md')))
357
+ || (await exists(path.join(projectRoot, '.codex', 'skills')))
358
+ ) {
359
+ detected.add('codex');
360
+ }
289
361
  if (await exists(path.join(projectRoot, '.cursor', 'rules', 'agent-flutter.mdc'))) {
290
362
  detected.add('cursor');
291
363
  }
@@ -305,11 +377,7 @@ async function detectInstalledIdeTargets(projectRoot) {
305
377
  }
306
378
 
307
379
  async function runList(options) {
308
- const projectRoot = path.resolve(options.get('cwd', process.cwd()));
309
- const sharedTarget = path.join(projectRoot, '.agent-flutter');
310
- const templateRoot = await exists(sharedTarget)
311
- ? sharedTarget
312
- : path.join(getPackageRoot(), 'templates', 'shared');
380
+ const templateRoot = path.join(getPackageRoot(), 'templates', 'shared');
313
381
 
314
382
  const skills = await loadSkillMetadata(path.join(templateRoot, 'skills'));
315
383
  const rules = await loadRuleMetadata(path.join(templateRoot, 'rules'));
@@ -407,6 +475,7 @@ async function copyTemplateEntries({ sourceDir, destinationDir, projectRoot }) {
407
475
  } else {
408
476
  await fs.copyFile(fromPath, toPath);
409
477
  }
478
+ await copyFileMode(fromPath, toPath);
410
479
  }
411
480
  }
412
481
 
@@ -518,23 +587,29 @@ function parseFrontmatter(content) {
518
587
  return data;
519
588
  }
520
589
 
521
- function buildCodexAgents({ projectRoot, projectName, skills, rules }) {
590
+ function buildCodexAgents({
591
+ projectRoot,
592
+ projectName,
593
+ skills,
594
+ rules,
595
+ packRoot,
596
+ }) {
522
597
  const lines = [];
523
598
  lines.push(`# AGENTS.md instructions for ${projectName}`);
524
599
  lines.push('');
525
- lines.push('## Agent Flutter Shared Pack');
526
- lines.push('This project uses a shared local pack installed at `.agent-flutter`.');
600
+ lines.push('## Agent Flutter Local Pack');
601
+ lines.push(`This project uses local instructions installed at \`${packRoot}\`.`);
527
602
  lines.push('');
528
603
  lines.push('### Available skills');
529
604
  for (const skill of skills) {
530
605
  lines.push(
531
- `- ${skill.slug}: ${skill.description || 'No description'} (file: ${toPosixPath(skill.path)})`,
606
+ `- ${skill.slug}: ${skill.description || 'No description'} (file: ${path.posix.join(packRoot, 'skills', skill.slug, 'SKILL.md')})`,
532
607
  );
533
608
  }
534
609
  lines.push('');
535
610
  lines.push('### Available rules');
536
611
  for (const rule of rules) {
537
- lines.push(`- ${rule.file} (file: ${toPosixPath(rule.path)})`);
612
+ lines.push(`- ${rule.file} (file: ${path.posix.join(packRoot, 'rules', rule.file)})`);
538
613
  }
539
614
  lines.push('');
540
615
  lines.push('### Trigger rules');
@@ -544,64 +619,69 @@ function buildCodexAgents({ projectRoot, projectName, skills, rules }) {
544
619
  lines.push('');
545
620
  lines.push('### Location policy');
546
621
  lines.push(`- Project root: ${toPosixPath(projectRoot)}`);
547
- lines.push('- Shared pack root: `.agent-flutter`');
548
- lines.push('- Do not duplicate skill/rule content outside the shared pack unless required.');
622
+ lines.push(`- Local pack root: \`${packRoot}\``);
549
623
  return `${lines.join('\n')}\n`;
550
624
  }
551
625
 
552
626
  function buildCursorRule() {
553
627
  return `---
554
- description: Agent Flutter shared skills and rules
628
+ description: Agent Flutter local skills and rules
555
629
  alwaysApply: false
556
630
  ---
557
- Use shared instructions from \`.agent-flutter\`.
631
+ Use local instructions from \`.cursor\`.
558
632
 
559
633
  Priority:
560
- 1. \`.agent-flutter/rules/ui.md\`
561
- 2. \`.agent-flutter/rules/integration-api.md\`
562
- 3. \`.agent-flutter/rules/document-workflow-function.md\`
563
- 4. \`.agent-flutter/rules/unit-test.md\` and \`.agent-flutter/rules/widget-test.md\`
634
+ 1. \`.cursor/rules/shared/ui.md\`
635
+ 2. \`.cursor/rules/shared/integration-api.md\`
636
+ 3. \`.cursor/rules/shared/document-workflow-function.md\`
637
+ 4. \`.cursor/rules/shared/unit-test.md\` and \`.cursor/rules/shared/widget-test.md\`
564
638
 
565
639
  When a task matches a skill, load the corresponding \`SKILL.md\` under:
566
- \`.agent-flutter/skills/<skill>/SKILL.md\`
640
+ \`.cursor/skills/<skill>/SKILL.md\`
641
+
642
+ For new project scaffolding, run:
643
+ \`bash .cursor/scripts/bootstrap_flutter_template.sh\`
567
644
  `;
568
645
  }
569
646
 
570
647
  function buildWindsurfRule() {
571
648
  return `# Agent Flutter Rules
572
649
 
573
- Use the shared rule/skill pack at \`.agent-flutter\`.
650
+ Use local instructions in \`.windsurf\`.
574
651
 
575
652
  Required order:
576
- 1. Apply relevant files in \`.agent-flutter/rules/\`.
577
- 2. If task matches a skill, load \`.agent-flutter/skills/<skill>/SKILL.md\`.
578
- 3. Keep spec documentation synchronized after UI/API changes.
653
+ 1. Apply relevant files in \`.windsurf/rules/shared/\`.
654
+ 2. If task matches a skill, load \`.windsurf/skills/<skill>/SKILL.md\`.
655
+ 3. For new project scaffolding, run \`bash .windsurf/scripts/bootstrap_flutter_template.sh\`.
656
+ 4. Keep spec documentation synchronized after UI/API changes.
579
657
  `;
580
658
  }
581
659
 
582
660
  function buildClineRule() {
583
661
  return `# Agent Flutter Cline Rule
584
662
 
585
- This repository uses shared instructions in \`.agent-flutter\`.
663
+ This repository uses local instructions in \`.clinerules\`.
586
664
 
587
665
  Execution checklist:
588
- 1. Read matching rule files under \`.agent-flutter/rules\`.
589
- 2. Apply matching skills from \`.agent-flutter/skills\`.
590
- 3. Preserve Flutter architecture conventions and localization requirements.
591
- 4. Update docs/specs after behavior changes.
666
+ 1. Read matching rule files under \`.clinerules/rules\`.
667
+ 2. Apply matching skills from \`.clinerules/skills\`.
668
+ 3. For new project scaffolding, run \`bash .clinerules/scripts/bootstrap_flutter_template.sh\`.
669
+ 4. Preserve Flutter architecture conventions and localization requirements.
670
+ 5. Update docs/specs after behavior changes.
592
671
  `;
593
672
  }
594
673
 
595
674
  function buildGithubCopilotInstructions() {
596
675
  return `# Agent Flutter Copilot Instructions
597
676
 
598
- This repository uses local instruction packs in \`.github/skills\` and \`.github/rules\`.
677
+ This repository uses local instruction packs in \`.github/skills\`, \`.github/rules\`, and \`.github/scripts\`.
599
678
 
600
679
  Follow this order when generating code:
601
680
  1. Read applicable files in \`.github/rules/\`.
602
681
  2. If task matches a skill, read \`.github/skills/<skill>/SKILL.md\`.
603
- 3. Keep architecture, localization, and UI conventions aligned with the shared pack.
604
- 4. Update specs/docs when UI/API behavior changes.
682
+ 3. For new project scaffolding, run \`bash .github/scripts/bootstrap_flutter_template.sh\`.
683
+ 4. Keep architecture, localization, and UI conventions aligned with local instructions.
684
+ 5. Update specs/docs when UI/API behavior changes.
605
685
  `;
606
686
  }
607
687
 
@@ -622,6 +702,15 @@ async function safeStat(filePath) {
622
702
  }
623
703
  }
624
704
 
705
+ async function copyFileMode(fromPath, toPath) {
706
+ try {
707
+ const fromStat = await fs.stat(fromPath);
708
+ await fs.chmod(toPath, fromStat.mode);
709
+ } catch {
710
+ // ignore permission propagation failures on unsupported filesystems
711
+ }
712
+ }
713
+
625
714
  function toPosixPath(value) {
626
715
  return String(value || '').replaceAll('\\', '/');
627
716
  }
@@ -1,148 +1,51 @@
1
1
  ---
2
2
  alwaysApply: false
3
3
  ---
4
- # Template Project Creation Workflow
5
-
6
- This workflow describes the step-by-step process to generate a new Flutter project that matches the architecture and standards of the current codebase.
7
-
8
- ## 1. Project Initialization (FVM)
9
-
10
- ### 1.1 FVM Setup & Create Project
11
- Use FVM (Flutter Version Management) to ensure consistent Flutter versions.
12
-
13
- 1. **Install FVM** (if needed):
14
- ```bash
15
- dart pub global activate fvm
16
- ```
17
-
18
- 2. **Initialize & Create Project**:
19
- ```bash
20
- mkdir project_name
21
- cd project_name
22
-
23
- # Initialize FVM with specific version (e.g., 3.38.5)
24
- fvm use 3.38.5 --force
25
-
26
- # Create project using FVM version
27
- fvm flutter create . --org com.example
28
- ```
29
-
30
- ### 1.2 IDE Configuration (VS Code)
31
- Create/Update `.vscode/settings.json` to use the FVM version:
32
- ```json
33
- {
34
- "dart.flutterSdkPath": ".fvm/flutter_sdk",
35
- "search.exclude": {
36
- "**/.fvm": true
37
- },
38
- "files.watcherExclude": {
39
- "**/.fvm": true
40
- }
41
- }
4
+ # New Template Project (Script-first)
5
+
6
+ Use the bootstrap script instead of generating the full workflow text.
7
+
8
+ ## Script path
9
+ - Trae: `.trae/scripts/bootstrap_flutter_template.sh`
10
+ - Codex: `.codex/scripts/bootstrap_flutter_template.sh`
11
+ - Cursor: `.cursor/scripts/bootstrap_flutter_template.sh`
12
+ - Windsurf: `.windsurf/scripts/bootstrap_flutter_template.sh`
13
+ - Cline: `.clinerules/scripts/bootstrap_flutter_template.sh`
14
+ - GitHub: `.github/scripts/bootstrap_flutter_template.sh`
15
+
16
+ ## Run
17
+ Interactive mode:
18
+ ```bash
19
+ bash .codex/scripts/bootstrap_flutter_template.sh
42
20
  ```
43
21
 
44
- ### 1.3 Update `pubspec.yaml`
45
- Add the standard dependencies required for the project architecture.
46
-
47
- **Core Dependencies:**
48
- - `get`: ^4.6.x (Routing & DI)
49
- - `flutter_bloc`: ^8.x.x (State Management)
50
- - `equatable`: ^2.x.x
51
- - `dio`: ^5.x.x (Network)
52
- - `retrofit`: ^4.x.x (API Client)
53
- - `json_annotation`: ^4.x.x
54
- - `flutter_dotenv`: (Environment variables)
55
- - `flutter_svg`: (SVG support)
56
- - `intl`: (Formatting)
57
-
58
- **Dev Dependencies:**
59
- - `build_runner`: (Code generation)
60
- - `retrofit_generator`:
61
- - `json_serializable`:
62
-
63
- ## 2. Directory Structure Scaffolding
64
-
65
- Remove the default `lib/main.dart` and create the following directory structure under `lib/src/`:
66
-
67
- ```
68
- lib/
69
- main.dart
70
- src/
71
- api/ # Retrofit API clients & interceptors
72
- core/
73
- managers/ # System managers (Permission, Connectivity)
74
- model/ # Data models (Request/Response)
75
- repository/ # Data repositories
76
- di/ # Dependency Injection setup
77
- enums/ # App-wide enums
78
- extensions/ # Dart extensions (Theme, Color, String)
79
- helper/ # Utils helpers (Validation, Logger)
80
- locale/ # Localization (TranslationManager)
81
- ui/ # Feature modules
82
- base/ # Base classes (BasePage, BaseViewModel)
83
- main/ # Root/Shell Page (BottomNav)
84
- home/ # Home Feature
85
- widgets/ # Shared UI components (AppInput, AppButton)
86
- routing/ # Per-tab Routers
87
- utils/ # Constants, Colors, Styles, Assets
22
+ Non-interactive mode:
23
+ ```bash
24
+ bash .codex/scripts/bootstrap_flutter_template.sh \
25
+ --name link_home_mobile \
26
+ --org com.company \
27
+ --flutter-version stable \
28
+ --dir ~/workspace \
29
+ --force \
30
+ --non-interactive
88
31
  ```
89
32
 
90
- ## 3. Core Module Setup
91
-
92
- ### 3.1 Dependency Injection (`lib/src/di/`)
93
- Create `di_graph_setup.dart` to initialize core services before the app starts.
94
- - **Components**:
95
- - `EnvironmentModule`: Load `.env` config.
96
- - `NetworkModule`: Setup Dio client.
97
- - `RepositoryModule`: Register Repositories.
98
- - `ManagerModule`: Register Global Managers.
99
-
100
- ### 3.2 Utilities (`lib/src/utils/`)
101
- Create placeholder files for design system constants:
102
- - `app_colors.dart`: Define `AppColors` class.
103
- - `app_styles.dart`: Define `AppStyles` class.
104
- - `app_assets.dart`: Define `AppAssets` class.
105
- - `app_pages.dart`: Define `AppPages` (GetX Route definition).
106
-
107
- ### 3.3 Localization (`lib/src/locale/`)
108
- Implement `TranslationManager` extending `Translations` from GetX.
109
-
110
- ## 4. App Entry Point (`main.dart`)
111
-
112
- Configure `main.dart` to:
113
- 1. Load Environment variables (`dotenv`).
114
- 2. Initialize Dependencies (`setupDependenciesGraph()`).
115
- 3. Run `GetMaterialApp` with:
116
- - `initialRoute`: `AppPages.main`
117
- - `getPages`: `AppPages.pages`
118
- - `theme`: Custom Theme.
119
-
120
- ## 5. Main Shell Implementation (Bottom Navigation)
121
-
122
- Follow the guide in `create-bottombar.md` to implement the root navigation shell.
123
-
124
- 1. **Enums**: Create `BottomNavigationPage` enum in `lib/src/enums/`.
125
- 2. **Bloc**: Create `MainBloc` in `lib/src/ui/main/bloc/` to manage tab state.
126
- 3. **UI**: Create `MainPage` using `IndexedStack` and `CupertinoTabView` for nested navigation.
127
- 4. **Widget**: Create `AppBottomNavigationBar`.
128
-
129
- ## 6. Base Feature Creation (Home)
130
-
131
- Create a standard "Home" feature to verify the setup:
132
- 1. **Folder**: `lib/src/ui/home/`.
133
- 2. **Files**:
134
- - `home_page.dart`: The UI.
135
- - `bloc/home_bloc.dart`: The Logic.
136
- - `binding/home_binding.dart`: DI for the module.
137
- 3. **Routing**:
138
- - Add `HomeRouter` in `lib/src/ui/routing/`.
139
- - Register in `AppPages`.
140
-
141
- ## 7. Environment Setup
142
-
143
- Create root-level environment files:
144
- - `.env`
145
- - `.env.staging`
146
- - `.env.prod`
147
-
148
- Add `.env*` to `.gitignore`.
33
+ ## Inputs
34
+ - `--name`: project folder name (required in non-interactive mode).
35
+ - `--org`: reverse-domain org id (default: `com.example`).
36
+ - `--flutter-version`: Flutter version for FVM (default: `stable`).
37
+ - `--dir`: parent folder to create project in (default: current directory).
38
+ - `--force`: allow overwrite in an existing non-empty directory.
39
+
40
+ ## What the script does
41
+ 1. Ensures FVM exists (auto-installs with `dart pub global activate fvm` if needed).
42
+ 2. Creates Flutter project with FVM and selected version.
43
+ 3. Adds core dependencies and dev dependencies.
44
+ 4. Creates architecture folders and starter files (`main`, DI, locale, routing, home feature).
45
+ 5. Creates `.env`, `.env.staging`, `.env.prod` and updates `.gitignore`.
46
+
47
+ ## Validation
48
+ ```bash
49
+ cd <project_name>
50
+ fvm flutter run
51
+ ```
@@ -0,0 +1,586 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'EOF'
6
+ Bootstrap a new Flutter template project (script-first workflow).
7
+
8
+ Usage:
9
+ bootstrap_flutter_template.sh [options]
10
+
11
+ Options:
12
+ -n, --name <project_name> Project folder name (required if --non-interactive)
13
+ -o, --org <org_id> Reverse-domain org id (default: com.example)
14
+ -f, --flutter-version <version> Flutter version for FVM (default: stable)
15
+ -d, --dir <parent_dir> Parent directory (default: current dir)
16
+ --force Allow create/overwrite in non-empty existing directory
17
+ --non-interactive Do not prompt for missing values
18
+ -h, --help Show this help
19
+
20
+ Examples:
21
+ ./bootstrap_flutter_template.sh
22
+ ./bootstrap_flutter_template.sh --name my_app --org com.company --flutter-version 3.38.5
23
+ ./bootstrap_flutter_template.sh --name crm_mobile --dir ~/workspace --force
24
+ EOF
25
+ }
26
+
27
+ PROJECT_NAME=""
28
+ ORG_ID="com.example"
29
+ FLUTTER_VERSION="stable"
30
+ PARENT_DIR="$(pwd)"
31
+ FORCE=0
32
+ NON_INTERACTIVE=0
33
+ FVM_BIN=""
34
+
35
+ while [[ $# -gt 0 ]]; do
36
+ case "$1" in
37
+ -n|--name)
38
+ PROJECT_NAME="${2:-}"
39
+ shift 2
40
+ ;;
41
+ -o|--org)
42
+ ORG_ID="${2:-}"
43
+ shift 2
44
+ ;;
45
+ -f|--flutter-version)
46
+ FLUTTER_VERSION="${2:-}"
47
+ shift 2
48
+ ;;
49
+ -d|--dir)
50
+ PARENT_DIR="${2:-}"
51
+ shift 2
52
+ ;;
53
+ --force)
54
+ FORCE=1
55
+ shift
56
+ ;;
57
+ --non-interactive)
58
+ NON_INTERACTIVE=1
59
+ shift
60
+ ;;
61
+ -h|--help)
62
+ usage
63
+ exit 0
64
+ ;;
65
+ *)
66
+ echo "Unknown option: $1" >&2
67
+ usage
68
+ exit 1
69
+ ;;
70
+ esac
71
+ done
72
+
73
+ prompt_required() {
74
+ local current="$1"
75
+ local message="$2"
76
+ if [[ -n "$current" ]]; then
77
+ printf '%s' "$current"
78
+ return
79
+ fi
80
+ if [[ "$NON_INTERACTIVE" -eq 1 ]]; then
81
+ printf '%s' ""
82
+ return
83
+ fi
84
+ read -r -p "$message" current
85
+ printf '%s' "$current"
86
+ }
87
+
88
+ prompt_with_default() {
89
+ local current="$1"
90
+ local message="$2"
91
+ local fallback="$3"
92
+ local input=""
93
+
94
+ if [[ "$NON_INTERACTIVE" -eq 1 ]]; then
95
+ if [[ -n "$current" ]]; then
96
+ printf '%s' "$current"
97
+ else
98
+ printf '%s' "$fallback"
99
+ fi
100
+ return
101
+ fi
102
+
103
+ read -r -p "$message" input
104
+ if [[ -n "$input" ]]; then
105
+ printf '%s' "$input"
106
+ return
107
+ fi
108
+
109
+ if [[ -n "$current" ]]; then
110
+ printf '%s' "$current"
111
+ else
112
+ printf '%s' "$fallback"
113
+ fi
114
+ }
115
+
116
+ normalize_project_name() {
117
+ local value="$1"
118
+ value="$(printf '%s' "$value" \
119
+ | tr '[:upper:]' '[:lower:]' \
120
+ | sed -E 's/[^a-z0-9_]+/_/g; s/_+/_/g; s/^_+|_+$//g')"
121
+ if [[ -z "$value" ]]; then
122
+ value="flutter_app"
123
+ fi
124
+ if [[ "$value" =~ ^[0-9] ]]; then
125
+ value="app_$value"
126
+ fi
127
+ printf '%s' "$value"
128
+ }
129
+
130
+ append_path_once() {
131
+ local target="$1"
132
+ [[ -n "$target" ]] || return
133
+ [[ -d "$target" ]] || return
134
+ case ":$PATH:" in
135
+ *":$target:"*) ;;
136
+ *) export PATH="$PATH:$target" ;;
137
+ esac
138
+ }
139
+
140
+ ensure_dart() {
141
+ if command -v dart >/dev/null 2>&1; then
142
+ return
143
+ fi
144
+
145
+ if command -v flutter >/dev/null 2>&1; then
146
+ local flutter_cmd=""
147
+ local flutter_bin=""
148
+ local flutter_dart_bin=""
149
+ flutter_cmd="$(command -v flutter)"
150
+ flutter_bin="$(cd "$(dirname "$flutter_cmd")" && pwd)"
151
+ flutter_dart_bin="$flutter_bin/cache/dart-sdk/bin"
152
+ append_path_once "$flutter_dart_bin"
153
+ fi
154
+
155
+ append_path_once "/opt/homebrew/opt/dart/libexec/bin"
156
+ append_path_once "/usr/local/opt/dart/libexec/bin"
157
+
158
+ if ! command -v dart >/dev/null 2>&1; then
159
+ cat >&2 <<'EOF'
160
+ Error: dart command not found.
161
+ Please install Dart (or Flutter) and ensure dart is available in PATH.
162
+ Example (macOS with Homebrew):
163
+ brew tap dart-lang/dart
164
+ brew install dart
165
+ EOF
166
+ exit 1
167
+ fi
168
+ }
169
+
170
+ discover_working_fvm() {
171
+ local candidates=()
172
+ local candidate=""
173
+ local pub_cache_fvm="$HOME/.pub-cache/bin/fvm"
174
+
175
+ if command -v fvm >/dev/null 2>&1; then
176
+ candidates+=("$(command -v fvm)")
177
+ fi
178
+
179
+ if command -v which >/dev/null 2>&1; then
180
+ while IFS= read -r candidate; do
181
+ [[ -n "$candidate" ]] || continue
182
+ candidates+=("$candidate")
183
+ done < <(which -a fvm 2>/dev/null | awk '!seen[$0]++')
184
+ fi
185
+
186
+ if [[ -x "$pub_cache_fvm" ]]; then
187
+ candidates+=("$pub_cache_fvm")
188
+ fi
189
+
190
+ for candidate in "${candidates[@]}"; do
191
+ if "$candidate" --version >/dev/null 2>&1; then
192
+ FVM_BIN="$candidate"
193
+ return 0
194
+ fi
195
+ done
196
+
197
+ return 1
198
+ }
199
+
200
+ run_fvm() {
201
+ "$FVM_BIN" "$@"
202
+ }
203
+
204
+ ensure_fvm() {
205
+ append_path_once "$HOME/.pub-cache/bin"
206
+
207
+ if discover_working_fvm; then
208
+ return
209
+ fi
210
+
211
+ ensure_dart
212
+
213
+ if discover_working_fvm; then
214
+ return
215
+ fi
216
+
217
+ echo "Installing FVM..."
218
+ dart pub global activate fvm >/dev/null
219
+ append_path_once "$HOME/.pub-cache/bin"
220
+
221
+ if ! discover_working_fvm; then
222
+ echo "Error: FVM installation failed or fvm is not runnable." >&2
223
+ exit 1
224
+ fi
225
+ }
226
+
227
+ ensure_line_in_file() {
228
+ local file="$1"
229
+ local line="$2"
230
+ touch "$file"
231
+ if ! grep -Fxq "$line" "$file"; then
232
+ printf '%s\n' "$line" >>"$file"
233
+ fi
234
+ }
235
+
236
+ PROJECT_NAME="$(prompt_required "$PROJECT_NAME" "Project name: ")"
237
+ if [[ -z "$PROJECT_NAME" ]]; then
238
+ echo "Error: project name is required." >&2
239
+ exit 1
240
+ fi
241
+
242
+ ORG_ID="$(prompt_with_default "$ORG_ID" "Org id (default: $ORG_ID): " "com.example")"
243
+ FLUTTER_VERSION="$(prompt_with_default "$FLUTTER_VERSION" "Flutter version (default: $FLUTTER_VERSION): " "stable")"
244
+
245
+ APP_PACKAGE_NAME="$(normalize_project_name "$PROJECT_NAME")"
246
+ if [[ ! -d "$PARENT_DIR" ]]; then
247
+ echo "Error: parent directory does not exist: $PARENT_DIR" >&2
248
+ exit 1
249
+ fi
250
+ PARENT_DIR="$(cd "$PARENT_DIR" && pwd)"
251
+ PROJECT_DIR="$PARENT_DIR/$PROJECT_NAME"
252
+
253
+ if [[ -d "$PROJECT_DIR" ]] && [[ -n "$(ls -A "$PROJECT_DIR" 2>/dev/null || true)" ]] && [[ "$FORCE" -ne 1 ]]; then
254
+ echo "Error: '$PROJECT_DIR' exists and is not empty. Use --force to continue." >&2
255
+ exit 1
256
+ fi
257
+
258
+ echo "Bootstrapping project:"
259
+ echo "- Directory: $PROJECT_DIR"
260
+ echo "- App package name: $APP_PACKAGE_NAME"
261
+ echo "- Org: $ORG_ID"
262
+ echo "- Flutter version: $FLUTTER_VERSION"
263
+
264
+ ensure_fvm
265
+
266
+ mkdir -p "$PROJECT_DIR"
267
+ cd "$PROJECT_DIR"
268
+
269
+ run_fvm use "$FLUTTER_VERSION" --force
270
+
271
+ create_args=(run_fvm flutter create . --org "$ORG_ID" --project-name "$APP_PACKAGE_NAME")
272
+ if [[ "$FORCE" -eq 1 ]]; then
273
+ create_args+=(--overwrite)
274
+ fi
275
+ "${create_args[@]}"
276
+
277
+ mkdir -p .vscode
278
+ cat >.vscode/settings.json <<'JSON'
279
+ {
280
+ "dart.flutterSdkPath": ".fvm/flutter_sdk",
281
+ "search.exclude": {
282
+ "**/.fvm": true
283
+ },
284
+ "files.watcherExclude": {
285
+ "**/.fvm": true
286
+ }
287
+ }
288
+ JSON
289
+
290
+ run_fvm flutter pub add get flutter_bloc equatable dio retrofit json_annotation flutter_dotenv flutter_svg intl
291
+ run_fvm flutter pub add --dev build_runner retrofit_generator json_serializable
292
+
293
+ mkdir -p \
294
+ lib/src/api \
295
+ lib/src/core/managers \
296
+ lib/src/core/model/request \
297
+ lib/src/core/model/response \
298
+ lib/src/core/repository \
299
+ lib/src/di \
300
+ lib/src/enums \
301
+ lib/src/extensions \
302
+ lib/src/helper \
303
+ lib/src/locale \
304
+ lib/src/ui/base \
305
+ lib/src/ui/main \
306
+ lib/src/ui/home/binding \
307
+ lib/src/ui/home/bloc \
308
+ lib/src/ui/routing \
309
+ lib/src/ui/widgets \
310
+ lib/src/utils
311
+
312
+ cat >lib/main.dart <<EOF
313
+ import 'package:flutter/material.dart';
314
+ import 'package:get/get.dart';
315
+
316
+ import 'package:$APP_PACKAGE_NAME/src/di/di_graph_setup.dart';
317
+ import 'package:$APP_PACKAGE_NAME/src/locale/translation_manager.dart';
318
+ import 'package:$APP_PACKAGE_NAME/src/utils/app_pages.dart';
319
+
320
+ Future<void> main() async {
321
+ WidgetsFlutterBinding.ensureInitialized();
322
+ await setupDependenciesGraph();
323
+ runApp(const App());
324
+ }
325
+
326
+ class App extends StatelessWidget {
327
+ const App({super.key});
328
+
329
+ @override
330
+ Widget build(BuildContext context) {
331
+ return GetMaterialApp(
332
+ debugShowCheckedModeBanner: false,
333
+ initialRoute: AppPages.main,
334
+ getPages: AppPages.pages,
335
+ translations: TranslationManager(),
336
+ locale: TranslationManager.defaultLocale,
337
+ fallbackLocale: TranslationManager.fallbackLocale,
338
+ );
339
+ }
340
+ }
341
+ EOF
342
+
343
+ cat >lib/src/di/di_graph_setup.dart <<'EOF'
344
+ import 'environment_module.dart';
345
+ import 'register_core_module.dart';
346
+ import 'register_manager_module.dart';
347
+
348
+ Future<void> setupDependenciesGraph() async {
349
+ await registerEnvironmentModule();
350
+ await registerCoreModule();
351
+ await registerManagerModule();
352
+ }
353
+ EOF
354
+
355
+ cat >lib/src/di/environment_module.dart <<'EOF'
356
+ import 'package:flutter_dotenv/flutter_dotenv.dart';
357
+
358
+ Future<void> registerEnvironmentModule() async {
359
+ if (dotenv.isInitialized) return;
360
+ await dotenv.load(fileName: '.env');
361
+ }
362
+ EOF
363
+
364
+ cat >lib/src/di/register_core_module.dart <<'EOF'
365
+ Future<void> registerCoreModule() async {
366
+ // Register core services/repositories here.
367
+ }
368
+ EOF
369
+
370
+ cat >lib/src/di/register_manager_module.dart <<'EOF'
371
+ Future<void> registerManagerModule() async {
372
+ // Register app-wide managers here.
373
+ }
374
+ EOF
375
+
376
+ cat >lib/src/utils/app_colors.dart <<'EOF'
377
+ import 'package:flutter/material.dart';
378
+
379
+ class AppColors {
380
+ AppColors._();
381
+
382
+ static const Color primary = Color(0xFF1F6FEB);
383
+ static const Color textPrimary = Color(0xFF1F2937);
384
+ static const Color white = Color(0xFFFFFFFF);
385
+ }
386
+ EOF
387
+
388
+ cat >lib/src/utils/app_styles.dart <<'EOF'
389
+ import 'package:flutter/material.dart';
390
+
391
+ import 'app_colors.dart';
392
+
393
+ class AppStyles {
394
+ AppStyles._();
395
+
396
+ static const TextStyle h1 = TextStyle(
397
+ fontSize: 24,
398
+ fontWeight: FontWeight.w700,
399
+ color: AppColors.textPrimary,
400
+ );
401
+ }
402
+ EOF
403
+
404
+ cat >lib/src/utils/app_assets.dart <<'EOF'
405
+ class AppAssets {
406
+ AppAssets._();
407
+
408
+ // Define image/icon paths here.
409
+ }
410
+ EOF
411
+
412
+ cat >lib/src/utils/app_pages.dart <<EOF
413
+ import 'package:get/get.dart';
414
+
415
+ import 'package:$APP_PACKAGE_NAME/src/ui/home/binding/home_binding.dart';
416
+ import 'package:$APP_PACKAGE_NAME/src/ui/home/home_page.dart';
417
+ import 'package:$APP_PACKAGE_NAME/src/ui/main/main_page.dart';
418
+
419
+ class AppPages {
420
+ AppPages._();
421
+
422
+ static const String main = '/';
423
+ static const String home = '/home';
424
+
425
+ static final List<GetPage<dynamic>> pages = <GetPage<dynamic>>[
426
+ GetPage(
427
+ name: main,
428
+ page: () => const MainPage(),
429
+ binding: HomeBinding(),
430
+ ),
431
+ GetPage(
432
+ name: home,
433
+ page: () => const HomePage(),
434
+ binding: HomeBinding(),
435
+ ),
436
+ ];
437
+ }
438
+ EOF
439
+
440
+ cat >lib/src/locale/locale_key.dart <<'EOF'
441
+ class LocaleKey {
442
+ LocaleKey._();
443
+
444
+ static const String homeTitle = 'home_title';
445
+ }
446
+ EOF
447
+
448
+ cat >lib/src/locale/lang_en.dart <<'EOF'
449
+ import 'locale_key.dart';
450
+
451
+ final Map<String, String> enUs = <String, String>{
452
+ LocaleKey.homeTitle: 'Home',
453
+ };
454
+ EOF
455
+
456
+ cat >lib/src/locale/lang_ja.dart <<'EOF'
457
+ import 'locale_key.dart';
458
+
459
+ final Map<String, String> jaJp = <String, String>{
460
+ LocaleKey.homeTitle: 'ホーム',
461
+ };
462
+ EOF
463
+
464
+ cat >lib/src/locale/translation_manager.dart <<'EOF'
465
+ import 'dart:ui';
466
+
467
+ import 'package:get/get.dart';
468
+
469
+ import 'lang_en.dart';
470
+ import 'lang_ja.dart';
471
+
472
+ class TranslationManager extends Translations {
473
+ static const Locale defaultLocale = Locale('en', 'US');
474
+ static const Locale fallbackLocale = Locale('en', 'US');
475
+ static const List<Locale> appLocales = <Locale>[
476
+ Locale('en', 'US'),
477
+ Locale('ja', 'JP'),
478
+ ];
479
+
480
+ @override
481
+ Map<String, Map<String, String>> get keys => <String, Map<String, String>>{
482
+ 'en_US': enUs,
483
+ 'ja_JP': jaJp,
484
+ };
485
+ }
486
+ EOF
487
+
488
+ cat >lib/src/ui/main/main_page.dart <<EOF
489
+ import 'package:flutter/material.dart';
490
+
491
+ import 'package:$APP_PACKAGE_NAME/src/ui/home/home_page.dart';
492
+
493
+ class MainPage extends StatelessWidget {
494
+ const MainPage({super.key});
495
+
496
+ @override
497
+ Widget build(BuildContext context) {
498
+ return const HomePage();
499
+ }
500
+ }
501
+ EOF
502
+
503
+ cat >lib/src/ui/home/home_page.dart <<'EOF'
504
+ import 'package:flutter/material.dart';
505
+ import 'package:get/get.dart';
506
+
507
+ import '../../locale/locale_key.dart';
508
+
509
+ class HomePage extends StatelessWidget {
510
+ const HomePage({super.key});
511
+
512
+ @override
513
+ Widget build(BuildContext context) {
514
+ return Scaffold(
515
+ appBar: AppBar(
516
+ title: Text(LocaleKey.homeTitle.tr),
517
+ ),
518
+ body: Center(
519
+ child: Text(LocaleKey.homeTitle.tr),
520
+ ),
521
+ );
522
+ }
523
+ }
524
+ EOF
525
+
526
+ cat >lib/src/ui/home/binding/home_binding.dart <<EOF
527
+ import 'package:get/get.dart';
528
+
529
+ import 'package:$APP_PACKAGE_NAME/src/ui/home/bloc/home_bloc.dart';
530
+
531
+ class HomeBinding extends Bindings {
532
+ @override
533
+ void dependencies() {
534
+ if (!Get.isRegistered<HomeBloc>()) {
535
+ Get.lazyPut<HomeBloc>(HomeBloc.new);
536
+ }
537
+ }
538
+ }
539
+ EOF
540
+
541
+ cat >lib/src/ui/home/bloc/home_bloc.dart <<'EOF'
542
+ import 'package:flutter_bloc/flutter_bloc.dart';
543
+
544
+ class HomeBloc extends Cubit<int> {
545
+ HomeBloc() : super(0);
546
+ }
547
+ EOF
548
+
549
+ cat >lib/src/ui/routing/home_router.dart <<EOF
550
+ import 'package:flutter/material.dart';
551
+
552
+ import 'package:$APP_PACKAGE_NAME/src/ui/home/home_page.dart';
553
+
554
+ Route<dynamic> homeRouter(RouteSettings settings) {
555
+ return MaterialPageRoute<void>(
556
+ settings: settings,
557
+ builder: (_) => const HomePage(),
558
+ );
559
+ }
560
+ EOF
561
+
562
+ cat >.env <<'EOF'
563
+ API_BASE_URL=https://api.example.com
564
+ EOF
565
+
566
+ cat >.env.staging <<'EOF'
567
+ API_BASE_URL=https://staging-api.example.com
568
+ EOF
569
+
570
+ cat >.env.prod <<'EOF'
571
+ API_BASE_URL=https://api.example.com
572
+ EOF
573
+
574
+ ensure_line_in_file .gitignore ".env"
575
+ ensure_line_in_file .gitignore ".env.staging"
576
+ ensure_line_in_file .gitignore ".env.prod"
577
+
578
+ if [[ -n "$FVM_BIN" ]]; then
579
+ run_fvm dart format lib >/dev/null || true
580
+ fi
581
+
582
+ echo ""
583
+ echo "Bootstrap completed."
584
+ echo "Next steps:"
585
+ echo "1) cd \"$PROJECT_DIR\""
586
+ echo "2) fvm flutter run"