agent-flutter 0.1.2 → 0.1.4

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: `.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/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.2",
3
+ "version": "0.1.4",
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,38 +142,50 @@ async function applyPack({
142
142
  mode,
143
143
  }) {
144
144
  const verb = mode === 'sync' ? 'Synced' : 'Installed';
145
- const sharedTarget = path.join(projectRoot, '.agent-flutter');
146
- if ((await exists(sharedTarget)) && !force) {
147
- console.log(`Using existing shared pack: ${sharedTarget}`);
148
- } else {
145
+ const syncDirectory = async ({ sourceDir, destinationDir, label }) => {
146
+ if ((await exists(destinationDir)) && !force) {
147
+ console.log(`Skipped ${label} (exists): ${destinationDir}`);
148
+ return;
149
+ }
149
150
  await copyTemplateDirectory({
150
- sourceDir: templateRoot,
151
- destinationDir: sharedTarget,
151
+ sourceDir,
152
+ destinationDir,
152
153
  projectRoot,
153
154
  force: true,
154
155
  });
155
- console.log(`${verb} shared pack: ${sharedTarget}`);
156
- }
156
+ console.log(`${verb} ${label}: ${destinationDir}`);
157
+ };
157
158
 
158
159
  if (ideTargets.has('trae')) {
159
160
  const traeTarget = path.join(projectRoot, '.trae');
160
- if ((await exists(traeTarget)) && !force) {
161
- console.log(`Skipped Trae adapter (exists): ${traeTarget}`);
162
- } else {
163
- await copyTemplateDirectory({
164
- sourceDir: templateRoot,
165
- destinationDir: traeTarget,
166
- projectRoot,
167
- force: true,
168
- });
169
- console.log(`${verb} Trae adapter: ${traeTarget}`);
170
- }
161
+ await syncDirectory({
162
+ sourceDir: templateRoot,
163
+ destinationDir: traeTarget,
164
+ label: 'Trae adapter',
165
+ });
171
166
  }
172
167
 
173
- const skills = await loadSkillMetadata(path.join(sharedTarget, 'skills'));
174
- const rules = await loadRuleMetadata(path.join(sharedTarget, 'rules'));
168
+ const skills = await loadSkillMetadata(path.join(templateRoot, 'skills'));
169
+ const rules = await loadRuleMetadata(path.join(templateRoot, 'rules'));
175
170
 
176
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
+
177
189
  const agentsPath = path.join(projectRoot, 'AGENTS.md');
178
190
  const written = await writeTextFile(
179
191
  agentsPath,
@@ -182,6 +194,7 @@ async function applyPack({
182
194
  projectName: path.basename(projectRoot),
183
195
  skills,
184
196
  rules,
197
+ packRoot: '.codex',
185
198
  }),
186
199
  { force },
187
200
  );
@@ -193,6 +206,22 @@ async function applyPack({
193
206
  }
194
207
 
195
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
+
196
225
  const cursorPath = path.join(projectRoot, '.cursor', 'rules', 'agent-flutter.mdc');
197
226
  const written = await writeTextFile(
198
227
  cursorPath,
@@ -207,6 +236,22 @@ async function applyPack({
207
236
  }
208
237
 
209
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
+
210
255
  const windsurfPath = path.join(projectRoot, '.windsurf', 'rules', 'agent-flutter.md');
211
256
  const written = await writeTextFile(
212
257
  windsurfPath,
@@ -221,6 +266,22 @@ async function applyPack({
221
266
  }
222
267
 
223
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
+
224
285
  const clinePath = path.join(projectRoot, '.clinerules', 'agent-flutter.md');
225
286
  const written = await writeTextFile(
226
287
  clinePath,
@@ -235,6 +296,45 @@ async function applyPack({
235
296
  }
236
297
 
237
298
  if (ideTargets.has('github')) {
299
+ const githubSkillsPath = path.join(projectRoot, '.github', 'skills');
300
+ if ((await exists(githubSkillsPath)) && !force) {
301
+ console.log(`Skipped GitHub skills (exists): ${githubSkillsPath}`);
302
+ } else {
303
+ await copyTemplateDirectory({
304
+ sourceDir: path.join(templateRoot, 'skills'),
305
+ destinationDir: githubSkillsPath,
306
+ projectRoot,
307
+ force: true,
308
+ });
309
+ console.log(`${verb} GitHub skills: ${githubSkillsPath}`);
310
+ }
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
+
325
+ const githubRulesPath = path.join(projectRoot, '.github', 'rules');
326
+ if ((await exists(githubRulesPath)) && !force) {
327
+ console.log(`Skipped GitHub rules (exists): ${githubRulesPath}`);
328
+ } else {
329
+ await copyTemplateDirectory({
330
+ sourceDir: path.join(templateRoot, 'rules'),
331
+ destinationDir: githubRulesPath,
332
+ projectRoot,
333
+ force: true,
334
+ });
335
+ console.log(`${verb} GitHub rules: ${githubRulesPath}`);
336
+ }
337
+
238
338
  const githubPath = path.join(projectRoot, '.github', 'copilot-instructions.md');
239
339
  const written = await writeTextFile(
240
340
  githubPath,
@@ -250,32 +350,34 @@ async function applyPack({
250
350
  }
251
351
 
252
352
  async function detectInstalledIdeTargets(projectRoot) {
253
- const checks = [
254
- ['trae', path.join(projectRoot, '.trae')],
255
- ['codex', path.join(projectRoot, 'AGENTS.md')],
256
- ['cursor', path.join(projectRoot, '.cursor', 'rules', 'agent-flutter.mdc')],
257
- ['windsurf', path.join(projectRoot, '.windsurf', 'rules', 'agent-flutter.md')],
258
- ['cline', path.join(projectRoot, '.clinerules', 'agent-flutter.md')],
259
- ['github', path.join(projectRoot, '.github', 'copilot-instructions.md')],
260
- ];
261
-
262
- const results = await Promise.all(
263
- checks.map(async ([ide, targetPath]) => [ide, await exists(targetPath)]),
264
- );
265
-
266
353
  const detected = new Set();
267
- for (const [ide, installed] of results) {
268
- if (installed) detected.add(ide);
354
+ if (await exists(path.join(projectRoot, '.trae'))) detected.add('trae');
355
+ if (
356
+ (await exists(path.join(projectRoot, 'AGENTS.md')))
357
+ || (await exists(path.join(projectRoot, '.codex', 'skills')))
358
+ ) {
359
+ detected.add('codex');
360
+ }
361
+ if (await exists(path.join(projectRoot, '.cursor', 'rules', 'agent-flutter.mdc'))) {
362
+ detected.add('cursor');
363
+ }
364
+ if (await exists(path.join(projectRoot, '.windsurf', 'rules', 'agent-flutter.md'))) {
365
+ detected.add('windsurf');
366
+ }
367
+ if (await exists(path.join(projectRoot, '.clinerules', 'agent-flutter.md'))) {
368
+ detected.add('cline');
369
+ }
370
+ if (
371
+ (await exists(path.join(projectRoot, '.github', 'copilot-instructions.md')))
372
+ || (await exists(path.join(projectRoot, '.github', 'skills')))
373
+ ) {
374
+ detected.add('github');
269
375
  }
270
376
  return detected;
271
377
  }
272
378
 
273
379
  async function runList(options) {
274
- const projectRoot = path.resolve(options.get('cwd', process.cwd()));
275
- const sharedTarget = path.join(projectRoot, '.agent-flutter');
276
- const templateRoot = await exists(sharedTarget)
277
- ? sharedTarget
278
- : path.join(getPackageRoot(), 'templates', 'shared');
380
+ const templateRoot = path.join(getPackageRoot(), 'templates', 'shared');
279
381
 
280
382
  const skills = await loadSkillMetadata(path.join(templateRoot, 'skills'));
281
383
  const rules = await loadRuleMetadata(path.join(templateRoot, 'rules'));
@@ -373,6 +475,7 @@ async function copyTemplateEntries({ sourceDir, destinationDir, projectRoot }) {
373
475
  } else {
374
476
  await fs.copyFile(fromPath, toPath);
375
477
  }
478
+ await copyFileMode(fromPath, toPath);
376
479
  }
377
480
  }
378
481
 
@@ -484,23 +587,29 @@ function parseFrontmatter(content) {
484
587
  return data;
485
588
  }
486
589
 
487
- function buildCodexAgents({ projectRoot, projectName, skills, rules }) {
590
+ function buildCodexAgents({
591
+ projectRoot,
592
+ projectName,
593
+ skills,
594
+ rules,
595
+ packRoot,
596
+ }) {
488
597
  const lines = [];
489
598
  lines.push(`# AGENTS.md instructions for ${projectName}`);
490
599
  lines.push('');
491
- lines.push('## Agent Flutter Shared Pack');
492
- 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}\`.`);
493
602
  lines.push('');
494
603
  lines.push('### Available skills');
495
604
  for (const skill of skills) {
496
605
  lines.push(
497
- `- ${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')})`,
498
607
  );
499
608
  }
500
609
  lines.push('');
501
610
  lines.push('### Available rules');
502
611
  for (const rule of rules) {
503
- lines.push(`- ${rule.file} (file: ${toPosixPath(rule.path)})`);
612
+ lines.push(`- ${rule.file} (file: ${path.posix.join(packRoot, 'rules', rule.file)})`);
504
613
  }
505
614
  lines.push('');
506
615
  lines.push('### Trigger rules');
@@ -510,64 +619,69 @@ function buildCodexAgents({ projectRoot, projectName, skills, rules }) {
510
619
  lines.push('');
511
620
  lines.push('### Location policy');
512
621
  lines.push(`- Project root: ${toPosixPath(projectRoot)}`);
513
- lines.push('- Shared pack root: `.agent-flutter`');
514
- lines.push('- Do not duplicate skill/rule content outside the shared pack unless required.');
622
+ lines.push(`- Local pack root: \`${packRoot}\``);
515
623
  return `${lines.join('\n')}\n`;
516
624
  }
517
625
 
518
626
  function buildCursorRule() {
519
627
  return `---
520
- description: Agent Flutter shared skills and rules
628
+ description: Agent Flutter local skills and rules
521
629
  alwaysApply: false
522
630
  ---
523
- Use shared instructions from \`.agent-flutter\`.
631
+ Use local instructions from \`.cursor\`.
524
632
 
525
633
  Priority:
526
- 1. \`.agent-flutter/rules/ui.md\`
527
- 2. \`.agent-flutter/rules/integration-api.md\`
528
- 3. \`.agent-flutter/rules/document-workflow-function.md\`
529
- 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\`
530
638
 
531
639
  When a task matches a skill, load the corresponding \`SKILL.md\` under:
532
- \`.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\`
533
644
  `;
534
645
  }
535
646
 
536
647
  function buildWindsurfRule() {
537
648
  return `# Agent Flutter Rules
538
649
 
539
- Use the shared rule/skill pack at \`.agent-flutter\`.
650
+ Use local instructions in \`.windsurf\`.
540
651
 
541
652
  Required order:
542
- 1. Apply relevant files in \`.agent-flutter/rules/\`.
543
- 2. If task matches a skill, load \`.agent-flutter/skills/<skill>/SKILL.md\`.
544
- 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.
545
657
  `;
546
658
  }
547
659
 
548
660
  function buildClineRule() {
549
661
  return `# Agent Flutter Cline Rule
550
662
 
551
- This repository uses shared instructions in \`.agent-flutter\`.
663
+ This repository uses local instructions in \`.clinerules\`.
552
664
 
553
665
  Execution checklist:
554
- 1. Read matching rule files under \`.agent-flutter/rules\`.
555
- 2. Apply matching skills from \`.agent-flutter/skills\`.
556
- 3. Preserve Flutter architecture conventions and localization requirements.
557
- 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.
558
671
  `;
559
672
  }
560
673
 
561
674
  function buildGithubCopilotInstructions() {
562
675
  return `# Agent Flutter Copilot Instructions
563
676
 
564
- This repository uses a shared local instruction pack in \`.agent-flutter\`.
677
+ This repository uses local instruction packs in \`.github/skills\`, \`.github/rules\`, and \`.github/scripts\`.
565
678
 
566
679
  Follow this order when generating code:
567
- 1. Read applicable files in \`.agent-flutter/rules/\`.
568
- 2. If task matches a skill, read \`.agent-flutter/skills/<skill>/SKILL.md\`.
569
- 3. Keep architecture, localization, and UI conventions aligned with the shared pack.
570
- 4. Update specs/docs when UI/API behavior changes.
680
+ 1. Read applicable files in \`.github/rules/\`.
681
+ 2. If task matches a skill, read \`.github/skills/<skill>/SKILL.md\`.
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.
571
685
  `;
572
686
  }
573
687
 
@@ -588,6 +702,15 @@ async function safeStat(filePath) {
588
702
  }
589
703
  }
590
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
+
591
714
  function toPosixPath(value) {
592
715
  return String(value || '').replaceAll('\\', '/');
593
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,508 @@
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
+
34
+ while [[ $# -gt 0 ]]; do
35
+ case "$1" in
36
+ -n|--name)
37
+ PROJECT_NAME="${2:-}"
38
+ shift 2
39
+ ;;
40
+ -o|--org)
41
+ ORG_ID="${2:-}"
42
+ shift 2
43
+ ;;
44
+ -f|--flutter-version)
45
+ FLUTTER_VERSION="${2:-}"
46
+ shift 2
47
+ ;;
48
+ -d|--dir)
49
+ PARENT_DIR="${2:-}"
50
+ shift 2
51
+ ;;
52
+ --force)
53
+ FORCE=1
54
+ shift
55
+ ;;
56
+ --non-interactive)
57
+ NON_INTERACTIVE=1
58
+ shift
59
+ ;;
60
+ -h|--help)
61
+ usage
62
+ exit 0
63
+ ;;
64
+ *)
65
+ echo "Unknown option: $1" >&2
66
+ usage
67
+ exit 1
68
+ ;;
69
+ esac
70
+ done
71
+
72
+ prompt_required() {
73
+ local current="$1"
74
+ local message="$2"
75
+ if [[ -n "$current" ]]; then
76
+ printf '%s' "$current"
77
+ return
78
+ fi
79
+ if [[ "$NON_INTERACTIVE" -eq 1 ]]; then
80
+ printf '%s' ""
81
+ return
82
+ fi
83
+ read -r -p "$message" current
84
+ printf '%s' "$current"
85
+ }
86
+
87
+ prompt_with_default() {
88
+ local current="$1"
89
+ local message="$2"
90
+ local fallback="$3"
91
+ local input=""
92
+
93
+ if [[ "$NON_INTERACTIVE" -eq 1 ]]; then
94
+ if [[ -n "$current" ]]; then
95
+ printf '%s' "$current"
96
+ else
97
+ printf '%s' "$fallback"
98
+ fi
99
+ return
100
+ fi
101
+
102
+ read -r -p "$message" input
103
+ if [[ -n "$input" ]]; then
104
+ printf '%s' "$input"
105
+ return
106
+ fi
107
+
108
+ if [[ -n "$current" ]]; then
109
+ printf '%s' "$current"
110
+ else
111
+ printf '%s' "$fallback"
112
+ fi
113
+ }
114
+
115
+ normalize_project_name() {
116
+ local value="$1"
117
+ value="$(printf '%s' "$value" \
118
+ | tr '[:upper:]' '[:lower:]' \
119
+ | sed -E 's/[^a-z0-9_]+/_/g; s/_+/_/g; s/^_+|_+$//g')"
120
+ if [[ -z "$value" ]]; then
121
+ value="flutter_app"
122
+ fi
123
+ if [[ "$value" =~ ^[0-9] ]]; then
124
+ value="app_$value"
125
+ fi
126
+ printf '%s' "$value"
127
+ }
128
+
129
+ ensure_fvm() {
130
+ if [[ -d "$HOME/.pub-cache/bin" ]]; then
131
+ export PATH="$HOME/.pub-cache/bin:$PATH"
132
+ fi
133
+ if command -v fvm >/dev/null 2>&1; then
134
+ return
135
+ fi
136
+ if ! command -v dart >/dev/null 2>&1; then
137
+ echo "Error: fvm not found and dart not available to install fvm." >&2
138
+ exit 1
139
+ fi
140
+ echo "Installing FVM..."
141
+ dart pub global activate fvm >/dev/null
142
+ export PATH="$PATH:$HOME/.pub-cache/bin"
143
+ if ! command -v fvm >/dev/null 2>&1; then
144
+ echo "Error: fvm installation failed." >&2
145
+ exit 1
146
+ fi
147
+ }
148
+
149
+ ensure_line_in_file() {
150
+ local file="$1"
151
+ local line="$2"
152
+ touch "$file"
153
+ if ! grep -Fxq "$line" "$file"; then
154
+ printf '%s\n' "$line" >>"$file"
155
+ fi
156
+ }
157
+
158
+ PROJECT_NAME="$(prompt_required "$PROJECT_NAME" "Project name: ")"
159
+ if [[ -z "$PROJECT_NAME" ]]; then
160
+ echo "Error: project name is required." >&2
161
+ exit 1
162
+ fi
163
+
164
+ ORG_ID="$(prompt_with_default "$ORG_ID" "Org id (default: $ORG_ID): " "com.example")"
165
+ FLUTTER_VERSION="$(prompt_with_default "$FLUTTER_VERSION" "Flutter version (default: $FLUTTER_VERSION): " "stable")"
166
+
167
+ APP_PACKAGE_NAME="$(normalize_project_name "$PROJECT_NAME")"
168
+ if [[ ! -d "$PARENT_DIR" ]]; then
169
+ echo "Error: parent directory does not exist: $PARENT_DIR" >&2
170
+ exit 1
171
+ fi
172
+ PARENT_DIR="$(cd "$PARENT_DIR" && pwd)"
173
+ PROJECT_DIR="$PARENT_DIR/$PROJECT_NAME"
174
+
175
+ if [[ -d "$PROJECT_DIR" ]] && [[ -n "$(ls -A "$PROJECT_DIR" 2>/dev/null || true)" ]] && [[ "$FORCE" -ne 1 ]]; then
176
+ echo "Error: '$PROJECT_DIR' exists and is not empty. Use --force to continue." >&2
177
+ exit 1
178
+ fi
179
+
180
+ echo "Bootstrapping project:"
181
+ echo "- Directory: $PROJECT_DIR"
182
+ echo "- App package name: $APP_PACKAGE_NAME"
183
+ echo "- Org: $ORG_ID"
184
+ echo "- Flutter version: $FLUTTER_VERSION"
185
+
186
+ ensure_fvm
187
+
188
+ mkdir -p "$PROJECT_DIR"
189
+ cd "$PROJECT_DIR"
190
+
191
+ fvm use "$FLUTTER_VERSION" --force
192
+
193
+ create_args=(fvm flutter create . --org "$ORG_ID" --project-name "$APP_PACKAGE_NAME")
194
+ if [[ "$FORCE" -eq 1 ]]; then
195
+ create_args+=(--overwrite)
196
+ fi
197
+ "${create_args[@]}"
198
+
199
+ mkdir -p .vscode
200
+ cat >.vscode/settings.json <<'JSON'
201
+ {
202
+ "dart.flutterSdkPath": ".fvm/flutter_sdk",
203
+ "search.exclude": {
204
+ "**/.fvm": true
205
+ },
206
+ "files.watcherExclude": {
207
+ "**/.fvm": true
208
+ }
209
+ }
210
+ JSON
211
+
212
+ fvm flutter pub add get flutter_bloc equatable dio retrofit json_annotation flutter_dotenv flutter_svg intl
213
+ fvm flutter pub add --dev build_runner retrofit_generator json_serializable
214
+
215
+ mkdir -p \
216
+ lib/src/api \
217
+ lib/src/core/managers \
218
+ lib/src/core/model/request \
219
+ lib/src/core/model/response \
220
+ lib/src/core/repository \
221
+ lib/src/di \
222
+ lib/src/enums \
223
+ lib/src/extensions \
224
+ lib/src/helper \
225
+ lib/src/locale \
226
+ lib/src/ui/base \
227
+ lib/src/ui/main \
228
+ lib/src/ui/home/binding \
229
+ lib/src/ui/home/bloc \
230
+ lib/src/ui/routing \
231
+ lib/src/ui/widgets \
232
+ lib/src/utils
233
+
234
+ cat >lib/main.dart <<EOF
235
+ import 'package:flutter/material.dart';
236
+ import 'package:get/get.dart';
237
+
238
+ import 'package:$APP_PACKAGE_NAME/src/di/di_graph_setup.dart';
239
+ import 'package:$APP_PACKAGE_NAME/src/locale/translation_manager.dart';
240
+ import 'package:$APP_PACKAGE_NAME/src/utils/app_pages.dart';
241
+
242
+ Future<void> main() async {
243
+ WidgetsFlutterBinding.ensureInitialized();
244
+ await setupDependenciesGraph();
245
+ runApp(const App());
246
+ }
247
+
248
+ class App extends StatelessWidget {
249
+ const App({super.key});
250
+
251
+ @override
252
+ Widget build(BuildContext context) {
253
+ return GetMaterialApp(
254
+ debugShowCheckedModeBanner: false,
255
+ initialRoute: AppPages.main,
256
+ getPages: AppPages.pages,
257
+ translations: TranslationManager(),
258
+ locale: TranslationManager.defaultLocale,
259
+ fallbackLocale: TranslationManager.fallbackLocale,
260
+ );
261
+ }
262
+ }
263
+ EOF
264
+
265
+ cat >lib/src/di/di_graph_setup.dart <<'EOF'
266
+ import 'environment_module.dart';
267
+ import 'register_core_module.dart';
268
+ import 'register_manager_module.dart';
269
+
270
+ Future<void> setupDependenciesGraph() async {
271
+ await registerEnvironmentModule();
272
+ await registerCoreModule();
273
+ await registerManagerModule();
274
+ }
275
+ EOF
276
+
277
+ cat >lib/src/di/environment_module.dart <<'EOF'
278
+ import 'package:flutter_dotenv/flutter_dotenv.dart';
279
+
280
+ Future<void> registerEnvironmentModule() async {
281
+ if (dotenv.isInitialized) return;
282
+ await dotenv.load(fileName: '.env');
283
+ }
284
+ EOF
285
+
286
+ cat >lib/src/di/register_core_module.dart <<'EOF'
287
+ Future<void> registerCoreModule() async {
288
+ // Register core services/repositories here.
289
+ }
290
+ EOF
291
+
292
+ cat >lib/src/di/register_manager_module.dart <<'EOF'
293
+ Future<void> registerManagerModule() async {
294
+ // Register app-wide managers here.
295
+ }
296
+ EOF
297
+
298
+ cat >lib/src/utils/app_colors.dart <<'EOF'
299
+ import 'package:flutter/material.dart';
300
+
301
+ class AppColors {
302
+ AppColors._();
303
+
304
+ static const Color primary = Color(0xFF1F6FEB);
305
+ static const Color textPrimary = Color(0xFF1F2937);
306
+ static const Color white = Color(0xFFFFFFFF);
307
+ }
308
+ EOF
309
+
310
+ cat >lib/src/utils/app_styles.dart <<'EOF'
311
+ import 'package:flutter/material.dart';
312
+
313
+ import 'app_colors.dart';
314
+
315
+ class AppStyles {
316
+ AppStyles._();
317
+
318
+ static const TextStyle h1 = TextStyle(
319
+ fontSize: 24,
320
+ fontWeight: FontWeight.w700,
321
+ color: AppColors.textPrimary,
322
+ );
323
+ }
324
+ EOF
325
+
326
+ cat >lib/src/utils/app_assets.dart <<'EOF'
327
+ class AppAssets {
328
+ AppAssets._();
329
+
330
+ // Define image/icon paths here.
331
+ }
332
+ EOF
333
+
334
+ cat >lib/src/utils/app_pages.dart <<EOF
335
+ import 'package:get/get.dart';
336
+
337
+ import 'package:$APP_PACKAGE_NAME/src/ui/home/binding/home_binding.dart';
338
+ import 'package:$APP_PACKAGE_NAME/src/ui/home/home_page.dart';
339
+ import 'package:$APP_PACKAGE_NAME/src/ui/main/main_page.dart';
340
+
341
+ class AppPages {
342
+ AppPages._();
343
+
344
+ static const String main = '/';
345
+ static const String home = '/home';
346
+
347
+ static final List<GetPage<dynamic>> pages = <GetPage<dynamic>>[
348
+ GetPage(
349
+ name: main,
350
+ page: () => const MainPage(),
351
+ binding: HomeBinding(),
352
+ ),
353
+ GetPage(
354
+ name: home,
355
+ page: () => const HomePage(),
356
+ binding: HomeBinding(),
357
+ ),
358
+ ];
359
+ }
360
+ EOF
361
+
362
+ cat >lib/src/locale/locale_key.dart <<'EOF'
363
+ class LocaleKey {
364
+ LocaleKey._();
365
+
366
+ static const String homeTitle = 'home_title';
367
+ }
368
+ EOF
369
+
370
+ cat >lib/src/locale/lang_en.dart <<'EOF'
371
+ import 'locale_key.dart';
372
+
373
+ final Map<String, String> enUs = <String, String>{
374
+ LocaleKey.homeTitle: 'Home',
375
+ };
376
+ EOF
377
+
378
+ cat >lib/src/locale/lang_ja.dart <<'EOF'
379
+ import 'locale_key.dart';
380
+
381
+ final Map<String, String> jaJp = <String, String>{
382
+ LocaleKey.homeTitle: 'ホーム',
383
+ };
384
+ EOF
385
+
386
+ cat >lib/src/locale/translation_manager.dart <<'EOF'
387
+ import 'dart:ui';
388
+
389
+ import 'package:get/get.dart';
390
+
391
+ import 'lang_en.dart';
392
+ import 'lang_ja.dart';
393
+
394
+ class TranslationManager extends Translations {
395
+ static const Locale defaultLocale = Locale('en', 'US');
396
+ static const Locale fallbackLocale = Locale('en', 'US');
397
+ static const List<Locale> appLocales = <Locale>[
398
+ Locale('en', 'US'),
399
+ Locale('ja', 'JP'),
400
+ ];
401
+
402
+ @override
403
+ Map<String, Map<String, String>> get keys => <String, Map<String, String>>{
404
+ 'en_US': enUs,
405
+ 'ja_JP': jaJp,
406
+ };
407
+ }
408
+ EOF
409
+
410
+ cat >lib/src/ui/main/main_page.dart <<EOF
411
+ import 'package:flutter/material.dart';
412
+
413
+ import 'package:$APP_PACKAGE_NAME/src/ui/home/home_page.dart';
414
+
415
+ class MainPage extends StatelessWidget {
416
+ const MainPage({super.key});
417
+
418
+ @override
419
+ Widget build(BuildContext context) {
420
+ return const HomePage();
421
+ }
422
+ }
423
+ EOF
424
+
425
+ cat >lib/src/ui/home/home_page.dart <<'EOF'
426
+ import 'package:flutter/material.dart';
427
+ import 'package:get/get.dart';
428
+
429
+ import '../../locale/locale_key.dart';
430
+
431
+ class HomePage extends StatelessWidget {
432
+ const HomePage({super.key});
433
+
434
+ @override
435
+ Widget build(BuildContext context) {
436
+ return Scaffold(
437
+ appBar: AppBar(
438
+ title: Text(LocaleKey.homeTitle.tr),
439
+ ),
440
+ body: Center(
441
+ child: Text(LocaleKey.homeTitle.tr),
442
+ ),
443
+ );
444
+ }
445
+ }
446
+ EOF
447
+
448
+ cat >lib/src/ui/home/binding/home_binding.dart <<EOF
449
+ import 'package:get/get.dart';
450
+
451
+ import 'package:$APP_PACKAGE_NAME/src/ui/home/bloc/home_bloc.dart';
452
+
453
+ class HomeBinding extends Bindings {
454
+ @override
455
+ void dependencies() {
456
+ if (!Get.isRegistered<HomeBloc>()) {
457
+ Get.lazyPut<HomeBloc>(HomeBloc.new);
458
+ }
459
+ }
460
+ }
461
+ EOF
462
+
463
+ cat >lib/src/ui/home/bloc/home_bloc.dart <<'EOF'
464
+ import 'package:flutter_bloc/flutter_bloc.dart';
465
+
466
+ class HomeBloc extends Cubit<int> {
467
+ HomeBloc() : super(0);
468
+ }
469
+ EOF
470
+
471
+ cat >lib/src/ui/routing/home_router.dart <<EOF
472
+ import 'package:flutter/material.dart';
473
+
474
+ import 'package:$APP_PACKAGE_NAME/src/ui/home/home_page.dart';
475
+
476
+ Route<dynamic> homeRouter(RouteSettings settings) {
477
+ return MaterialPageRoute<void>(
478
+ settings: settings,
479
+ builder: (_) => const HomePage(),
480
+ );
481
+ }
482
+ EOF
483
+
484
+ cat >.env <<'EOF'
485
+ API_BASE_URL=https://api.example.com
486
+ EOF
487
+
488
+ cat >.env.staging <<'EOF'
489
+ API_BASE_URL=https://staging-api.example.com
490
+ EOF
491
+
492
+ cat >.env.prod <<'EOF'
493
+ API_BASE_URL=https://api.example.com
494
+ EOF
495
+
496
+ ensure_line_in_file .gitignore ".env"
497
+ ensure_line_in_file .gitignore ".env.staging"
498
+ ensure_line_in_file .gitignore ".env.prod"
499
+
500
+ if command -v fvm >/dev/null 2>&1; then
501
+ fvm dart format lib >/dev/null || true
502
+ fi
503
+
504
+ echo ""
505
+ echo "Bootstrap completed."
506
+ echo "Next steps:"
507
+ echo "1) cd \"$PROJECT_DIR\""
508
+ echo "2) fvm flutter run"