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 +24 -7
- package/package.json +1 -1
- package/src/cli.js +195 -72
- 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/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,38 +142,50 @@ async function applyPack({
|
|
|
142
142
|
mode,
|
|
143
143
|
}) {
|
|
144
144
|
const verb = mode === 'sync' ? 'Synced' : 'Installed';
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
151
|
-
destinationDir
|
|
151
|
+
sourceDir,
|
|
152
|
+
destinationDir,
|
|
152
153
|
projectRoot,
|
|
153
154
|
force: true,
|
|
154
155
|
});
|
|
155
|
-
console.log(`${verb}
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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(
|
|
174
|
-
const rules = await loadRuleMetadata(path.join(
|
|
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
|
-
|
|
268
|
-
|
|
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
|
|
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({
|
|
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
|
|
492
|
-
lines.push(
|
|
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: ${
|
|
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: ${
|
|
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(
|
|
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
|
|
628
|
+
description: Agent Flutter local skills and rules
|
|
521
629
|
alwaysApply: false
|
|
522
630
|
---
|
|
523
|
-
Use
|
|
631
|
+
Use local instructions from \`.cursor\`.
|
|
524
632
|
|
|
525
633
|
Priority:
|
|
526
|
-
1. \`.
|
|
527
|
-
2. \`.
|
|
528
|
-
3. \`.
|
|
529
|
-
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\`
|
|
530
638
|
|
|
531
639
|
When a task matches a skill, load the corresponding \`SKILL.md\` under:
|
|
532
|
-
\`.
|
|
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
|
|
650
|
+
Use local instructions in \`.windsurf\`.
|
|
540
651
|
|
|
541
652
|
Required order:
|
|
542
|
-
1. Apply relevant files in \`.
|
|
543
|
-
2. If task matches a skill, load \`.
|
|
544
|
-
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.
|
|
545
657
|
`;
|
|
546
658
|
}
|
|
547
659
|
|
|
548
660
|
function buildClineRule() {
|
|
549
661
|
return `# Agent Flutter Cline Rule
|
|
550
662
|
|
|
551
|
-
This repository uses
|
|
663
|
+
This repository uses local instructions in \`.clinerules\`.
|
|
552
664
|
|
|
553
665
|
Execution checklist:
|
|
554
|
-
1. Read matching rule files under \`.
|
|
555
|
-
2. Apply matching skills from \`.
|
|
556
|
-
3.
|
|
557
|
-
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.
|
|
558
671
|
`;
|
|
559
672
|
}
|
|
560
673
|
|
|
561
674
|
function buildGithubCopilotInstructions() {
|
|
562
675
|
return `# Agent Flutter Copilot Instructions
|
|
563
676
|
|
|
564
|
-
This repository uses
|
|
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 \`.
|
|
568
|
-
2. If task matches a skill, read \`.
|
|
569
|
-
3.
|
|
570
|
-
4.
|
|
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
|
|
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"
|