agent-flutter 0.1.6 → 0.1.8

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
@@ -70,8 +70,9 @@ npm run release:major
70
70
 
71
71
  ## Installed files
72
72
 
73
+ - Workspace utilities: `spec/`, `tool/`, `tools/`, `.vscode/tasks.json`
73
74
  - Trae: `.trae/` (skills/rules/scripts)
74
- - Codex: `.codex/` + `AGENTS.md`
75
+ - Codex: `.codex/` (skills/rules/scripts) + `AGENTS.md`
75
76
  - Cursor: `.cursor/skills/`, `.cursor/rules/shared/`, `.cursor/scripts/`, `.cursor/rules/agent-flutter.mdc`
76
77
  - Windsurf: `.windsurf/skills/`, `.windsurf/rules/shared/`, `.windsurf/scripts/`, `.windsurf/rules/agent-flutter.md`
77
78
  - Cline: `.clinerules/skills/`, `.clinerules/rules/`, `.clinerules/scripts/`, `.clinerules/agent-flutter.md`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-flutter",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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
@@ -142,6 +142,7 @@ async function applyPack({
142
142
  mode,
143
143
  }) {
144
144
  const verb = mode === 'sync' ? 'Synced' : 'Installed';
145
+ const sharedUtilityDirs = ['spec', 'tool', 'tools'];
145
146
  const syncDirectory = async ({ sourceDir, destinationDir, label }) => {
146
147
  if ((await exists(destinationDir)) && !force) {
147
148
  console.log(`Skipped ${label} (exists): ${destinationDir}`);
@@ -155,13 +156,77 @@ async function applyPack({
155
156
  });
156
157
  console.log(`${verb} ${label}: ${destinationDir}`);
157
158
  };
159
+ const syncTemplateFile = async ({ sourcePath, destinationPath, label }) => {
160
+ if ((await exists(destinationPath)) && !force) {
161
+ console.log(`Skipped ${label} (exists): ${destinationPath}`);
162
+ return;
163
+ }
164
+ await fs.mkdir(path.dirname(destinationPath), { recursive: true });
165
+ if (isTextFile(path.basename(sourcePath))) {
166
+ const raw = await fs.readFile(sourcePath, 'utf8');
167
+ const transformed = replaceProjectPlaceholders(raw, projectRoot);
168
+ await fs.writeFile(destinationPath, transformed, 'utf8');
169
+ } else {
170
+ await fs.copyFile(sourcePath, destinationPath);
171
+ }
172
+ await copyFileMode(sourcePath, destinationPath);
173
+ console.log(`${verb} ${label}: ${destinationPath}`);
174
+ };
175
+ const syncWorkspaceUtilities = async () => {
176
+ for (const dirName of sharedUtilityDirs) {
177
+ await syncDirectory({
178
+ sourceDir: path.join(templateRoot, dirName),
179
+ destinationDir: path.join(projectRoot, dirName),
180
+ label: `Workspace ${dirName}`,
181
+ });
182
+ }
183
+ };
184
+ const syncWorkspaceVscodeTasks = async () => {
185
+ const sourcePath = path.join(templateRoot, 'vscode', 'tasks.json');
186
+ if (!(await exists(sourcePath))) return;
187
+ const raw = await fs.readFile(sourcePath, 'utf8');
188
+ const transformed = replaceProjectPlaceholders(raw, projectRoot);
189
+ const targetPath = path.join(projectRoot, '.vscode', 'tasks.json');
190
+ const written = await writeTextFile(
191
+ targetPath,
192
+ transformed,
193
+ { force },
194
+ );
195
+ console.log(
196
+ written
197
+ ? `${verb} Workspace VS Code tasks: ${targetPath}`
198
+ : `Skipped Workspace VS Code tasks (exists): ${targetPath}`,
199
+ );
200
+ };
201
+ await syncWorkspaceUtilities();
202
+ await syncWorkspaceVscodeTasks();
158
203
 
159
204
  if (ideTargets.has('trae')) {
160
205
  const traeTarget = path.join(projectRoot, '.trae');
161
206
  await syncDirectory({
162
- sourceDir: templateRoot,
163
- destinationDir: traeTarget,
164
- label: 'Trae adapter',
207
+ sourceDir: path.join(templateRoot, 'skills'),
208
+ destinationDir: path.join(traeTarget, 'skills'),
209
+ label: 'Trae skills',
210
+ });
211
+ await syncDirectory({
212
+ sourceDir: path.join(templateRoot, 'scripts'),
213
+ destinationDir: path.join(traeTarget, 'scripts'),
214
+ label: 'Trae scripts',
215
+ });
216
+ await syncDirectory({
217
+ sourceDir: path.join(templateRoot, 'rules'),
218
+ destinationDir: path.join(traeTarget, 'rules'),
219
+ label: 'Trae rules',
220
+ });
221
+ await syncTemplateFile({
222
+ sourcePath: path.join(templateRoot, '.ignore'),
223
+ destinationPath: path.join(traeTarget, '.ignore'),
224
+ label: 'Trae ignore',
225
+ });
226
+ await syncTemplateFile({
227
+ sourcePath: path.join(templateRoot, 'TEMPLATES.md'),
228
+ destinationPath: path.join(traeTarget, 'TEMPLATES.md'),
229
+ label: 'Trae templates',
165
230
  });
166
231
  }
167
232
 
@@ -334,7 +399,6 @@ async function applyPack({
334
399
  });
335
400
  console.log(`${verb} GitHub rules: ${githubRulesPath}`);
336
401
  }
337
-
338
402
  const githubPath = path.join(projectRoot, '.github', 'copilot-instructions.md');
339
403
  const written = await writeTextFile(
340
404
  githubPath,
@@ -0,0 +1,111 @@
1
+ import 'dart:io';
2
+
3
+ void main() {
4
+ final projectRoot = Directory.current.path;
5
+ final modelRoot =
6
+ Directory('$projectRoot/lib/src/core/model'); // absolute at runtime
7
+
8
+ final uiDomainFiles = <File>[];
9
+ final requestFiles = <File>[];
10
+ final responseFiles = <File>[];
11
+
12
+ if (!modelRoot.existsSync()) {
13
+ stderr.writeln('Model root not found: ${modelRoot.path}');
14
+ exit(1);
15
+ }
16
+
17
+ // Collect files
18
+ for (final entity in modelRoot.listSync(recursive: true)) {
19
+ if (entity is File && entity.path.endsWith('.dart')) {
20
+ final p = entity.path;
21
+ if (p.contains('/request/')) {
22
+ requestFiles.add(entity);
23
+ } else if (p.contains('/response/')) {
24
+ responseFiles.add(entity);
25
+ } else {
26
+ uiDomainFiles.add(entity);
27
+ }
28
+ }
29
+ }
30
+
31
+ String relPath(File f) =>
32
+ f.path.replaceFirst(projectRoot, '').replaceFirst(RegExp(r'^/'), '');
33
+
34
+ List<_Decl> parseDecls(File file) {
35
+ final lines = file.readAsLinesSync();
36
+ final decls = <_Decl>[];
37
+ for (var i = 0; i < lines.length; i++) {
38
+ final line = lines[i].trim();
39
+ final classMatch = RegExp(r'^class\s+([A-Za-z0-9_]+)').firstMatch(line);
40
+ final enumMatch = RegExp(r'^enum\s+([A-Za-z0-9_]+)').firstMatch(line);
41
+ if (classMatch != null || enumMatch != null) {
42
+ final kind = classMatch != null ? 'class' : 'enum';
43
+ final name = (classMatch ?? enumMatch)!.group(1)!;
44
+ // Collect preceding doc comments (/// ...)
45
+ final docLines = <String>[];
46
+ var j = i - 1;
47
+ while (j >= 0) {
48
+ final prev = lines[j].trim();
49
+ if (prev.startsWith('///')) {
50
+ docLines.insert(0, prev.replaceFirst('///', '').trim());
51
+ j--;
52
+ continue;
53
+ }
54
+ break;
55
+ }
56
+ final description = docLines.isEmpty ? null : docLines.join(' ');
57
+ decls.add(_Decl(kind, name, description));
58
+ }
59
+ }
60
+ return decls;
61
+ }
62
+
63
+ String section(String title, List<File> files) {
64
+ final b = StringBuffer('## $title\n\n');
65
+ if (files.isEmpty) {
66
+ b.writeln('- (none)\n');
67
+ return b.toString();
68
+ }
69
+ for (final f in files) {
70
+ final decls = parseDecls(f);
71
+ final path = relPath(f);
72
+ if (decls.isEmpty) {
73
+ b.writeln('- $path');
74
+ } else {
75
+ for (final d in decls) {
76
+ b.writeln('- ${d.kind} ${d.name} — $path');
77
+ if (d.description != null && d.description!.isNotEmpty) {
78
+ b.writeln(' - ${d.description}');
79
+ }
80
+ }
81
+ }
82
+ }
83
+ b.writeln();
84
+ return b.toString();
85
+ }
86
+
87
+ final out = StringBuffer()
88
+ ..writeln('# Model Registry (AUTO-GENERATED)\n')
89
+ ..writeln(
90
+ '> Do not edit manually. Run: dart run tool/generate_model_registry.dart\n')
91
+ ..writeln(section('UI/Domain Models', uiDomainFiles))
92
+ ..writeln(section('Request Models', requestFiles))
93
+ ..writeln(section('Response Models', responseFiles));
94
+
95
+ // Ensure spec directory exists
96
+ final specDir = Directory('$projectRoot/spec');
97
+ if (!specDir.existsSync()) {
98
+ specDir.createSync(recursive: true);
99
+ }
100
+ final outputFile =
101
+ File('${specDir.path}/model-registry.md'); // overwrite existing
102
+ outputFile.writeAsStringSync(out.toString());
103
+ stdout.writeln('Generated ${outputFile.path}');
104
+ }
105
+
106
+ class _Decl {
107
+ final String kind;
108
+ final String name;
109
+ final String? description;
110
+ _Decl(this.kind, this.name, this.description);
111
+ }
@@ -0,0 +1,173 @@
1
+ import 'dart:io';
2
+
3
+ void main() {
4
+ final projectRoot = Directory.current.path;
5
+ final uiRoot = Directory('$projectRoot/lib/src/ui');
6
+ if (!uiRoot.existsSync()) {
7
+ stderr.writeln('UI root not found: ${uiRoot.path}');
8
+ exit(1);
9
+ }
10
+
11
+ final specDir = Directory('$projectRoot/spec');
12
+ if (!specDir.existsSync()) {
13
+ specDir.createSync(recursive: true);
14
+ }
15
+ final outputFile = File('${specDir.path}/ui-workflow.md');
16
+
17
+ final sections = <String>[];
18
+ for (final featureDir in uiRoot.listSync()) {
19
+ if (featureDir is! Directory) continue;
20
+ final name = featureDir.uri.pathSegments.lastWhere(
21
+ (s) => s.isNotEmpty,
22
+ orElse: () => featureDir.path.split('/').last,
23
+ );
24
+
25
+ final pages = <_FileDoc>[];
26
+ final components = <_FileDoc>[];
27
+ final hasBinding = Directory('${featureDir.path}/binding').existsSync();
28
+ final hasInteractor =
29
+ Directory('${featureDir.path}/interactor').existsSync();
30
+ final interactorFiles =
31
+ Directory('${featureDir.path}/interactor').existsSync()
32
+ ? Directory('${featureDir.path}/interactor')
33
+ .listSync()
34
+ .whereType<File>()
35
+ .where((f) => f.path.endsWith('.dart'))
36
+ .toList()
37
+ : <File>[];
38
+
39
+ // Collect page and component files
40
+ for (final entity in featureDir.listSync(recursive: true)) {
41
+ if (entity is File && entity.path.endsWith('.dart')) {
42
+ final rel = entity.path
43
+ .replaceFirst(projectRoot, '')
44
+ .replaceFirst(RegExp(r'^/'), '');
45
+ if (entity.path.endsWith('_page.dart')) {
46
+ pages.add(_readDoc(entity, rel));
47
+ } else if (rel.contains('/components/')) {
48
+ components.add(_readDoc(entity, rel));
49
+ }
50
+ }
51
+ }
52
+
53
+ // Derive feature goal from the first page doc (if any)
54
+ final featureGoal =
55
+ pages.isNotEmpty && pages.first.description != null
56
+ ? pages.first.description!
57
+ : '';
58
+
59
+ final b =
60
+ StringBuffer()
61
+ ..writeln('## $name')
62
+ ..writeln('**Path**: lib/src/ui/$name\n')
63
+ ..writeln('### 1. Description')
64
+ ..writeln('Goal: ${featureGoal.isEmpty ? "" : featureGoal}')
65
+ ..writeln('Features:')
66
+ ..writeln('- ')
67
+ ..writeln('\n### 2. UI Structure')
68
+ ..writeln('- Screens:')
69
+ ..writeAll(
70
+ pages.map(
71
+ (p) =>
72
+ ' - ${p.symbolName} — ${p.relPath}\n${p.description != null && p.description!.isNotEmpty ? ' - ${p.description!}\n' : ''}',
73
+ ),
74
+ )
75
+ ..writeln('- Components:')
76
+ ..writeAll(
77
+ components.map(
78
+ (c) =>
79
+ ' - ${c.symbolName} — ${c.relPath}\n${c.description != null && c.description!.isNotEmpty ? ' - ${c.description!}\n' : ''}',
80
+ ),
81
+ )
82
+ ..writeln('\n### 3. User Flow & Logic')
83
+ ..writeln('1) ')
84
+ ..writeln('2) ')
85
+ ..writeln('\n### 4. Key Dependencies')
86
+ ..writeln(hasInteractor ? '- interactor/*' : '- (none)')
87
+ ..writeAll(
88
+ interactorFiles.map(
89
+ (f) =>
90
+ ' - ${f.path.replaceFirst(projectRoot, '').replaceFirst(RegExp(r'^/'), '')}\n',
91
+ ),
92
+ )
93
+ ..writeln(hasBinding ? '- binding/*' : '- (none)')
94
+ ..writeln('\n### 5. Notes & Known Issues')
95
+ ..writeln('- ')
96
+ ..writeln('');
97
+
98
+ sections.add(b.toString());
99
+ }
100
+
101
+ final out =
102
+ StringBuffer()
103
+ ..writeln('# UI Workflow (AUTO-GENERATED)\n')
104
+ ..writeln(
105
+ '> Do not edit manually. Run: dart run tool/generate_ui_workflow_spec.dart\n',
106
+ )
107
+ ..writeAll(sections);
108
+
109
+ outputFile.writeAsStringSync(out.toString());
110
+ stdout.writeln('Generated ${outputFile.path}');
111
+ }
112
+
113
+ class _FileDoc {
114
+ final String relPath;
115
+ final String symbolName;
116
+ final String? description;
117
+ _FileDoc(this.relPath, this.symbolName, this.description);
118
+ }
119
+
120
+ _FileDoc _readDoc(File file, String relPath) {
121
+ final lines = file.readAsLinesSync();
122
+ String? symbolName;
123
+ String? description;
124
+
125
+ // Find first class or widget name
126
+ for (var i = 0; i < lines.length; i++) {
127
+ final line = lines[i].trim();
128
+ final classMatch = RegExp(r'^class\s+([A-Za-z0-9_]+)').firstMatch(line);
129
+ if (classMatch != null) {
130
+ symbolName = classMatch.group(1)!;
131
+ // Collect leading doc comments (/// ...) before class
132
+ final docLines = <String>[];
133
+ var j = i - 1;
134
+ while (j >= 0) {
135
+ final prev = lines[j].trim();
136
+ if (prev.startsWith('///')) {
137
+ docLines.insert(0, prev.replaceFirst('///', '').trim());
138
+ j--;
139
+ continue;
140
+ }
141
+ // Stop at non-doc line
142
+ break;
143
+ }
144
+ if (docLines.isNotEmpty) {
145
+ description = docLines.join(' ');
146
+ }
147
+ break;
148
+ }
149
+ }
150
+
151
+ // Fallback: top-of-file doc block
152
+ if (description == null) {
153
+ final docLines = <String>[];
154
+ for (final l in lines) {
155
+ final t = l.trim();
156
+ if (t.startsWith('///')) {
157
+ docLines.add(t.replaceFirst('///', '').trim());
158
+ } else if (t.isEmpty || t.startsWith('//')) {
159
+ // continue scanning
160
+ continue;
161
+ } else {
162
+ break;
163
+ }
164
+ }
165
+ if (docLines.isNotEmpty) {
166
+ description = docLines.join(' ');
167
+ }
168
+ }
169
+
170
+ // Fallback symbol name to file basename
171
+ symbolName ??= relPath.split('/').last.replaceAll('.dart', '');
172
+ return _FileDoc(relPath, symbolName, description);
173
+ }
@@ -0,0 +1,40 @@
1
+ import 'dart:io';
2
+
3
+ void main() {
4
+ final code1 = _runWithFallback('tool/generate_model_registry.dart');
5
+ if (code1 != 0) exit(code1);
6
+ final code2 = _runWithFallback('tool/generate_ui_workflow_spec.dart');
7
+ if (code2 != 0) exit(code2);
8
+ }
9
+
10
+ int _runWithFallback(String scriptPath) {
11
+ const runners = <List<String>>[
12
+ <String>['dart', 'run'],
13
+ <String>['fvm', 'dart', 'run'],
14
+ <String>['flutter', 'pub', 'run'],
15
+ ];
16
+
17
+ ProcessResult? lastResult;
18
+ for (final runner in runners) {
19
+ final command = runner.first;
20
+ final args = <String>[...runner.sublist(1), scriptPath];
21
+ final result = Process.runSync(command, args, runInShell: true);
22
+ stdout.write(result.stdout);
23
+ stderr.write(result.stderr);
24
+ if (result.exitCode == 0) {
25
+ return 0;
26
+ }
27
+ if (!_isCommandNotFound(result)) {
28
+ return result.exitCode;
29
+ }
30
+ lastResult = result;
31
+ }
32
+
33
+ return lastResult?.exitCode ?? 1;
34
+ }
35
+
36
+ bool _isCommandNotFound(ProcessResult result) {
37
+ if (result.exitCode == 127) return true;
38
+ final stderrText = '${result.stderr}'.toLowerCase();
39
+ return stderrText.contains('command not found') || stderrText.contains('is not recognized');
40
+ }