agent-flutter 0.1.3 → 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 +24 -7
- package/package.json +1 -1
- package/src/cli.js +155 -66
- package/templates/shared/rules/new-template-project.md +44 -141
- package/templates/shared/scripts/bootstrap_flutter_template.sh +508 -0
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
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
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
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
|
|
17
|
-
sync Update installed
|
|
18
|
-
list Print available skills/rules from
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
178
|
-
|
|
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 (
|
|
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
|
|
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({
|
|
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
|
|
526
|
-
lines.push(
|
|
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: ${
|
|
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: ${
|
|
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(
|
|
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
|
|
628
|
+
description: Agent Flutter local skills and rules
|
|
555
629
|
alwaysApply: false
|
|
556
630
|
---
|
|
557
|
-
Use
|
|
631
|
+
Use local instructions from \`.cursor\`.
|
|
558
632
|
|
|
559
633
|
Priority:
|
|
560
|
-
1. \`.
|
|
561
|
-
2. \`.
|
|
562
|
-
3. \`.
|
|
563
|
-
4. \`.
|
|
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
|
-
\`.
|
|
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
|
|
650
|
+
Use local instructions in \`.windsurf\`.
|
|
574
651
|
|
|
575
652
|
Required order:
|
|
576
|
-
1. Apply relevant files in \`.
|
|
577
|
-
2. If task matches a skill, load \`.
|
|
578
|
-
3.
|
|
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
|
|
663
|
+
This repository uses local instructions in \`.clinerules\`.
|
|
586
664
|
|
|
587
665
|
Execution checklist:
|
|
588
|
-
1. Read matching rule files under \`.
|
|
589
|
-
2. Apply matching skills from \`.
|
|
590
|
-
3.
|
|
591
|
-
4.
|
|
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
|
|
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.
|
|
604
|
-
4.
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
##
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
-
|
|
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
|
-
##
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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"
|