Skip to content

Commit 0394f3d

Browse files
committed
v1.5.0
Add nylo test, nylo clean platform flags, and nylo self-update commands.
1 parent 9ee3bcf commit 0394f3d

14 files changed

Lines changed: 1494 additions & 10 deletions

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,23 @@
1+
## [1.5.0] - 2026-03-28
2+
3+
### Added
4+
- `nylo self-update` command: updates nylo_installer to the latest version from pub.dev
5+
- Automatic update detection: shows a styled banner after any command when a newer version is available
6+
- Version check caching in `~/.nylo/version_cache.json` with 24-hour TTL
7+
- 2-second HTTP timeout for version checks — network failures are silently ignored
8+
- `nylo test` command: format and run Flutter tests with pretty JSON output, per-test timing, and aggregated pass/fail summary
9+
- `nylo test` flags: `--no-format`, `--filter=<pattern>`, `--coverage`, `--path=<dir>`
10+
- `nylo clean` platform-specific deep cleaning with `--ios`, `--android`, and `--all` flags
11+
- iOS deep clean: removes Pods, .symlinks, Podfile.lock and re-runs `pod install --repo-update`
12+
- Android deep clean: runs `gradlew clean`
13+
- Platform directory validation before attempting platform-specific cleans
14+
- Comprehensive tests for `CleanCommand` and `TestCommand`
15+
16+
### Changed
17+
- `CleanCommand.run()` now accepts optional arguments for flag parsing
18+
- CLI argument parser uses `allowTrailingOptions: false` for correct subcommand argument routing
19+
- Updated help text with all new command flags and usage examples
20+
121
## [1.4.0] - 2026-02-14
222

323
### Added

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ Commands:
1717
new <project_name> Create a new Nylo project
1818
init Set up the Metro CLI alias
1919
clean Run flutter clean and flutter pub get
20+
--ios Deep clean iOS (remove Pods, re-run pod install)
21+
--android Deep clean Android (run gradlew clean)
22+
--all Deep clean both iOS and Android
23+
test Format and run Flutter tests
24+
--no-format Skip formatting before running tests
25+
--filter=<pattern> Filter tests by name
26+
--coverage Collect code coverage
27+
--path=<dir> Test directory path (default: test)
2028
2129
Options:
2230
-h, --help Show usage information
@@ -62,6 +70,32 @@ This runs:
6270
1. `flutter clean` - Removes build artifacts
6371
2. `flutter pub get` - Reinstalls dependencies
6472

73+
Use platform flags for deep cleaning:
74+
75+
```bash
76+
nylo clean --ios # Deep clean iOS (removes Pods, .symlinks, Podfile.lock, re-runs pod install)
77+
nylo clean --android # Deep clean Android (runs gradlew clean)
78+
nylo clean --all # Deep clean both iOS and Android
79+
```
80+
81+
### `nylo test`
82+
83+
Format and run Flutter tests with pretty output:
84+
85+
```bash
86+
nylo test
87+
```
88+
89+
Options:
90+
91+
```bash
92+
nylo test --no-format # Skip formatting before running tests
93+
nylo test --filter "login" # Filter tests by name
94+
nylo test --coverage # Collect code coverage
95+
nylo test --path integration_test # Specify test directory
96+
nylo test --filter "auth" --coverage
97+
```
98+
6599
## Metro CLI
66100

67101
Once Metro is set up via `nylo init`, you can generate files for your Nylo project:

bin/nylo.dart

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import 'package:args/args.dart';
55
import 'package:nylo_installer/src/commands/clean_command.dart';
66
import 'package:nylo_installer/src/commands/init_command.dart';
77
import 'package:nylo_installer/src/commands/new_command.dart';
8+
import 'package:nylo_installer/src/commands/self_update_command.dart';
9+
import 'package:nylo_installer/src/commands/test_command.dart';
810
import 'package:nylo_installer/src/console/console.dart';
911
import 'package:nylo_installer/src/constants.dart';
12+
import 'package:nylo_installer/src/utils/version_checker.dart';
1013

1114
void main(List<String> arguments) async {
12-
final parser = ArgParser()
15+
final parser = ArgParser(allowTrailingOptions: false)
1316
..addFlag('help',
1417
abbr: 'h', negatable: false, help: 'Show usage information')
1518
..addFlag('version', abbr: 'v', negatable: false, help: 'Show version');
@@ -48,13 +51,31 @@ void main(List<String> arguments) async {
4851
await InitCommand().run();
4952
break;
5053
case 'clean':
51-
await CleanCommand().run();
54+
final cleanArgs =
55+
results.rest.length > 1 ? results.rest.sublist(1) : <String>[];
56+
await CleanCommand().run(cleanArgs);
57+
break;
58+
case 'test':
59+
final testArgs =
60+
results.rest.length > 1 ? results.rest.sublist(1) : <String>[];
61+
await TestCommand().run(testArgs);
62+
break;
63+
case 'self-update':
64+
await SelfUpdateCommand().run();
5265
break;
5366
default:
5467
NyloConsole.writeError('Unknown command: $command');
5568
_printUsage();
5669
exit(1);
5770
}
71+
72+
// Check for updates after command completes (skip for self-update)
73+
if (command != 'self-update') {
74+
final updateInfo = await VersionChecker().checkForUpdate();
75+
if (updateInfo != null) {
76+
VersionChecker.printUpdateBanner(updateInfo);
77+
}
78+
}
5879
} on FormatException catch (e) {
5980
NyloConsole.writeError('Error: ${e.message}');
6081
_printUsage();
@@ -73,6 +94,15 @@ void _printUsage() {
7394
new <project_name> Create a new Nylo project
7495
init Set up the metro CLI alias
7596
clean Run flutter clean and flutter pub get
97+
--ios Deep clean iOS (remove Pods, re-run pod install)
98+
--android Deep clean Android (run gradlew clean)
99+
--all Deep clean both iOS and Android
100+
test Format and run Flutter tests
101+
--no-format Skip formatting before running tests
102+
--filter=<pattern> Filter tests by name
103+
--coverage Collect code coverage
104+
--path=<dir> Test directory path (default: test)
105+
self-update Update nylo to the latest version
76106
77107
Options:
78108
-h, --help Show usage information
@@ -83,6 +113,11 @@ void _printUsage() {
83113
nylo new MyAwesomeApp
84114
nylo init
85115
nylo clean
116+
nylo clean --ios
117+
nylo clean --all
118+
nylo test
119+
nylo test --filter "login" --coverage
120+
nylo self-update
86121
87122
Documentation: ${Constants.docsUrl}
88123
''');

lib/nylo_installer.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ library nylo_installer;
33

44
export 'src/commands/metro_installer.dart';
55
export 'src/commands/new_command.dart';
6+
export 'src/commands/self_update_command.dart';
7+
export 'src/commands/test_command.dart';
8+
export 'src/utils/version_checker.dart';
69
export 'src/console/console.dart';
710
export 'src/constants.dart';
811
export 'src/utils/process_runner.dart';
Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,59 @@
11
import 'dart:io';
2-
2+
import 'package:args/args.dart';
33
import '../console/console.dart';
44
import '../utils/process_runner.dart';
55

66
/// Handles the "nylo clean" command
77
/// Runs flutter clean followed by flutter pub get
8+
/// Supports --ios, --android, and --all flags for platform-specific deep cleaning
89
class CleanCommand {
910
/// Execute the clean command
10-
Future<void> run() async {
11+
Future<void> run([List<String> arguments = const []]) async {
12+
final parser = ArgParser()
13+
..addFlag('ios', negatable: false, help: 'Deep clean iOS')
14+
..addFlag('android', negatable: false, help: 'Deep clean Android')
15+
..addFlag('all', negatable: false, help: 'Deep clean iOS and Android');
16+
17+
final ArgResults results;
18+
try {
19+
results = parser.parse(arguments);
20+
} on FormatException catch (e) {
21+
NyloConsole.writeError(e.message);
22+
exit(1);
23+
}
24+
25+
bool cleanIos = results['ios'] as bool || results['all'] as bool;
26+
bool cleanAndroid = results['android'] as bool || results['all'] as bool;
27+
28+
// Validate platform directories exist
29+
if (cleanIos && !await Directory('ios').exists()) {
30+
NyloConsole.writeWarning('ios/ directory not found. Skipping iOS clean.');
31+
cleanIos = false;
32+
}
33+
if (cleanAndroid && !await Directory('android').exists()) {
34+
NyloConsole.writeWarning(
35+
'android/ directory not found. Skipping Android clean.');
36+
cleanAndroid = false;
37+
}
38+
39+
// Calculate steps dynamically
40+
int totalSteps = 2; // flutter clean + flutter pub get
41+
if (cleanIos) totalSteps += 2; // rm artifacts + pod install
42+
if (cleanAndroid) totalSteps += 1; // gradlew clean
43+
int currentStep = 0;
44+
String stepLabel() {
45+
currentStep++;
46+
return '[$currentStep/$totalSteps]';
47+
}
48+
1149
NyloConsole.writeBanner();
1250
NyloConsole.write('');
1351
NyloConsole.writeInfo('Cleaning project...');
1452
NyloConsole.write('');
1553

16-
// Step 1: Run flutter clean
54+
// Step: Run flutter clean
1755
final cleanSpinner = Spinner('');
18-
cleanSpinner.start('[1/2] Running flutter clean...');
56+
cleanSpinner.start('${stepLabel()} Running flutter clean...');
1957
final cleanResult = await ProcessRunner.run('flutter', ['clean']);
2058
cleanSpinner.stop('flutter clean complete');
2159

@@ -25,9 +63,57 @@ class CleanCommand {
2563
exit(1);
2664
}
2765

28-
// Step 2: Run flutter pub get
66+
// iOS deep clean
67+
if (cleanIos) {
68+
// Step: Remove iOS artifacts
69+
final rmSpinner = Spinner('');
70+
rmSpinner.start('${stepLabel()} Removing iOS build artifacts...');
71+
await _removeIosArtifacts();
72+
rmSpinner.stop('iOS artifacts removed (Pods, .symlinks, Podfile.lock)');
73+
74+
// Step: pod install --repo-update
75+
final podSpinner = Spinner('');
76+
podSpinner.start('${stepLabel()} Running pod install --repo-update...');
77+
if (!Platform.isMacOS) {
78+
podSpinner.stop('pod install skipped (not macOS)');
79+
NyloConsole.writeWarning(
80+
'pod install is only available on macOS. Skipping.');
81+
} else {
82+
final podResult = await ProcessRunner.run(
83+
'pod',
84+
['install', '--repo-update'],
85+
workingDirectory: 'ios',
86+
);
87+
podSpinner.stop('pod install complete');
88+
89+
if (podResult.exitCode != 0) {
90+
NyloConsole.writeWarning('pod install completed with warnings');
91+
NyloConsole.writeWarning(podResult.stderr);
92+
}
93+
}
94+
}
95+
96+
// Android deep clean
97+
if (cleanAndroid) {
98+
final gradleSpinner = Spinner('');
99+
gradleSpinner.start('${stepLabel()} Running gradlew clean...');
100+
final gradlew = Platform.isWindows ? 'gradlew.bat' : './gradlew';
101+
final gradleResult = await ProcessRunner.run(
102+
gradlew,
103+
['clean'],
104+
workingDirectory: 'android',
105+
);
106+
gradleSpinner.stop('gradlew clean complete');
107+
108+
if (gradleResult.exitCode != 0) {
109+
NyloConsole.writeWarning('gradlew clean completed with warnings');
110+
NyloConsole.writeWarning(gradleResult.stderr);
111+
}
112+
}
113+
114+
// Step: Run flutter pub get
29115
final pubGetSpinner = Spinner('');
30-
pubGetSpinner.start('[2/2] Running flutter pub get...');
116+
pubGetSpinner.start('${stepLabel()} Running flutter pub get...');
31117
final pubGetResult = await ProcessRunner.run('flutter', ['pub', 'get']);
32118
pubGetSpinner.stop('flutter pub get complete');
33119

@@ -38,4 +124,26 @@ class CleanCommand {
38124
NyloConsole.write('');
39125
NyloConsole.writeSuccess('Project cleaned successfully!');
40126
}
127+
128+
/// Remove iOS build artifacts (Pods, .symlinks, Podfile.lock)
129+
Future<void> _removeIosArtifacts() async {
130+
final directories = [
131+
Directory('ios/Pods'),
132+
Directory('ios/.symlinks'),
133+
];
134+
final files = [
135+
File('ios/Podfile.lock'),
136+
];
137+
138+
for (final dir in directories) {
139+
if (await dir.exists()) {
140+
await dir.delete(recursive: true);
141+
}
142+
}
143+
for (final file in files) {
144+
if (await file.exists()) {
145+
await file.delete();
146+
}
147+
}
148+
}
41149
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import 'dart:io';
2+
3+
import '../console/console.dart';
4+
import '../constants.dart';
5+
import '../utils/process_runner.dart';
6+
import '../utils/version_checker.dart';
7+
8+
/// Handles the "nylo self-update" command
9+
/// Updates the nylo_installer to the latest version from pub.dev
10+
class SelfUpdateCommand {
11+
/// Execute the self-update command
12+
Future<void> run() async {
13+
NyloConsole.writeInfo('Checking for updates...');
14+
15+
final checker = VersionChecker();
16+
final latestVersion = await checker.fetchLatestVersion();
17+
18+
if (latestVersion == null) {
19+
NyloConsole.writeError(
20+
'Could not check for updates. Please check your internet connection.');
21+
exit(1);
22+
}
23+
24+
if (latestVersion == Constants.version) {
25+
NyloConsole.writeSuccess(
26+
'You\'re already on the latest version (${Constants.version})');
27+
return;
28+
}
29+
30+
NyloConsole.write('');
31+
NyloConsole.writeInfo(
32+
'Updating nylo_installer ${Constants.version} → $latestVersion...');
33+
NyloConsole.write('');
34+
35+
final spinner = Spinner('');
36+
spinner.start('Installing update...');
37+
38+
final result = await ProcessRunner.run(
39+
'dart',
40+
['pub', 'global', 'activate', Constants.packageName],
41+
);
42+
43+
spinner.stop();
44+
45+
if (result.exitCode != 0) {
46+
NyloConsole.writeError('Update failed.');
47+
if (result.stderr.isNotEmpty) {
48+
NyloConsole.writeError(result.stderr);
49+
}
50+
exit(1);
51+
}
52+
53+
NyloConsole.writeSuccess('Updated nylo_installer to $latestVersion');
54+
}
55+
}

0 commit comments

Comments
 (0)