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 +2 -1
- package/package.json +1 -1
- package/src/cli.js +68 -4
- package/templates/shared/tool/generate_model_registry.dart +111 -0
- package/templates/shared/tool/generate_ui_workflow_spec.dart +173 -0
- package/templates/shared/tool/update_specs.dart +40 -0
- package/templates/shared/tools/jsx2flutter/convert.mjs +318 -0
- package/templates/shared/tools/jsx2flutter/icons_map.json +16 -0
- package/templates/shared/tools/jsx2flutter/jsx2flutter.mjs +1997 -0
- package/templates/shared/tools/jsx2flutter/package-lock.json +288 -0
- package/templates/shared/tools/jsx2flutter/package.json +19 -0
- package/templates/shared/vscode/tasks.json +121 -0
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
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
|
|
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
|
+
}
|