optikit 1.1.1 → 1.2.0

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.
Files changed (79) hide show
  1. package/CHANGELOG.md +23 -2
  2. package/CLAUDE.md +239 -0
  3. package/CODE_QUALITY.md +398 -0
  4. package/ENHANCEMENTS.md +310 -0
  5. package/FEATURE_ENHANCEMENTS.md +435 -0
  6. package/README.md +46 -13
  7. package/SAFETY_FEATURES.md +396 -0
  8. package/USAGE.md +225 -41
  9. package/VERSION_MANAGEMENT.md +438 -0
  10. package/dist/cli.js +116 -7
  11. package/dist/commands/build/releases.js +57 -0
  12. package/dist/commands/buildReleases.js +48 -74
  13. package/dist/commands/clean/flutter.js +51 -0
  14. package/dist/commands/clean/ios.js +109 -0
  15. package/dist/commands/cleanProject.js +34 -4
  16. package/dist/commands/cleanProjectIos.js +17 -7
  17. package/dist/commands/config/init.js +54 -0
  18. package/dist/commands/config/rollback.js +161 -0
  19. package/dist/commands/generateModule.js +39 -11
  20. package/dist/commands/init.js +54 -0
  21. package/dist/commands/openProject.js +17 -0
  22. package/dist/commands/project/devices.js +188 -0
  23. package/dist/commands/project/generate.js +143 -0
  24. package/dist/commands/project/open.js +63 -0
  25. package/dist/commands/project/setup.js +46 -0
  26. package/dist/commands/rollback.js +161 -0
  27. package/dist/commands/setupVSCode.js +27 -21
  28. package/dist/commands/updateVersions.js +13 -1
  29. package/dist/commands/version/bump.js +161 -0
  30. package/dist/commands/version/update.js +91 -0
  31. package/dist/commands/version.js +161 -0
  32. package/dist/constants.js +131 -0
  33. package/dist/utils/backupHelpers.js +88 -0
  34. package/dist/utils/buildHelpers.js +55 -0
  35. package/dist/utils/commandHelpers.js +51 -0
  36. package/dist/utils/configHelpers.js +80 -0
  37. package/dist/utils/dryRunHelpers.js +103 -0
  38. package/dist/utils/fileHelpers.js +2 -1
  39. package/dist/utils/helpers/build.js +55 -0
  40. package/dist/utils/helpers/dryRun.js +103 -0
  41. package/dist/utils/helpers/file.js +24 -0
  42. package/dist/utils/helpers/string.js +3 -0
  43. package/dist/utils/helpers/version.js +80 -0
  44. package/dist/utils/services/backup.js +88 -0
  45. package/dist/utils/services/command.js +51 -0
  46. package/dist/utils/services/config.js +80 -0
  47. package/dist/utils/services/exec.js +132 -0
  48. package/dist/utils/services/logger.js +15 -0
  49. package/dist/utils/validationHelpers.js +101 -0
  50. package/dist/utils/validators/validation.js +101 -0
  51. package/dist/utils/versionHelpers.js +80 -0
  52. package/package.json +1 -1
  53. package/src/cli.ts +165 -7
  54. package/src/commands/build/releases.ts +79 -0
  55. package/src/commands/clean/flutter.ts +58 -0
  56. package/src/commands/{cleanProjectIos.ts → clean/ios.ts} +19 -10
  57. package/src/commands/config/init.ts +63 -0
  58. package/src/commands/config/rollback.ts +200 -0
  59. package/src/commands/project/devices.ts +246 -0
  60. package/src/commands/{generateModule.ts → project/generate.ts} +47 -17
  61. package/src/commands/{openProject.ts → project/open.ts} +26 -5
  62. package/src/commands/project/setup.ts +50 -0
  63. package/src/commands/version/bump.ts +202 -0
  64. package/src/commands/{updateVersions.ts → version/update.ts} +22 -6
  65. package/src/constants.ts +144 -0
  66. package/src/utils/helpers/build.ts +80 -0
  67. package/src/utils/helpers/dryRun.ts +124 -0
  68. package/src/utils/{fileHelpers.ts → helpers/file.ts} +3 -2
  69. package/src/utils/helpers/version.ts +109 -0
  70. package/src/utils/services/backup.ts +109 -0
  71. package/src/utils/services/command.ts +76 -0
  72. package/src/utils/services/config.ts +106 -0
  73. package/src/utils/{execHelpers.ts → services/exec.ts} +1 -1
  74. package/src/utils/validators/validation.ts +122 -0
  75. package/src/commands/buildReleases.ts +0 -102
  76. package/src/commands/cleanProject.ts +0 -25
  77. package/src/commands/setupVSCode.ts +0 -44
  78. /package/src/utils/{stringHelpers.ts → helpers/string.ts} +0 -0
  79. /package/src/utils/{loggerHelpers.ts → services/logger.ts} +0 -0
@@ -0,0 +1,202 @@
1
+ import { validateFlutterProject } from "../../utils/validators/validation.js";
2
+ import { LoggerHelpers } from "../../utils/services/logger.js";
3
+ import {
4
+ getCurrentVersion,
5
+ incrementVersion,
6
+ formatVersion,
7
+ getNextBuildNumber
8
+ } from "../../utils/helpers/version.js";
9
+ import { updateFlutterVersion } from "./update.js";
10
+ import chalk from "chalk";
11
+
12
+ export {
13
+ bumpVersion,
14
+ bumpIosBuildOnly,
15
+ bumpAndroidBuildOnly,
16
+ showCurrentVersion
17
+ };
18
+
19
+ /**
20
+ * Bumps version with semantic versioning (major, minor, patch)
21
+ * Android build number increments with version
22
+ * iOS build number resets to 1 on version change
23
+ *
24
+ * @param type - Type of version bump (major, minor, patch)
25
+ */
26
+ async function bumpVersion(
27
+ type: 'major' | 'minor' | 'patch'
28
+ ): Promise<void> {
29
+ // Pre-flight validation
30
+ if (!validateFlutterProject()) {
31
+ process.exit(1);
32
+ }
33
+
34
+ try {
35
+ const current = getCurrentVersion();
36
+ const newVersion = incrementVersion(current, type);
37
+
38
+ LoggerHelpers.info(`Current version: ${formatVersion(current)}`);
39
+ LoggerHelpers.info(`Bumping ${type} version...`);
40
+
41
+ console.log(chalk.cyan("\nVersion changes:"));
42
+ console.log(chalk.gray(" Old:"), chalk.white(formatVersion(current)));
43
+ console.log(chalk.gray(" New:"), chalk.green.bold(formatVersion(newVersion)));
44
+
45
+ console.log(chalk.cyan("\nBuild number strategy:"));
46
+ console.log(chalk.gray(" Android:"), chalk.white(`${current.buildNumber} → ${newVersion.buildNumber} (incremented)`));
47
+ console.log(chalk.gray(" iOS:"), chalk.white(`${current.buildNumber} → 1 (reset for new version)`));
48
+ console.log();
49
+
50
+ // Update with new version
51
+ // Android uses the incremented build number from version
52
+ // iOS gets reset to 1 for new version releases
53
+ await updateFlutterVersion(
54
+ `${newVersion.major}.${newVersion.minor}.${newVersion.patch}`,
55
+ newVersion.buildNumber.toString(),
56
+ "1" // iOS always starts at 1 for new versions
57
+ );
58
+
59
+ LoggerHelpers.success(`Version bumped to ${formatVersion(newVersion)}`);
60
+ console.log(chalk.gray("\nAndroid build:"), chalk.white(newVersion.buildNumber));
61
+ console.log(chalk.gray("iOS build:"), chalk.white("1"));
62
+
63
+ } catch (error) {
64
+ if (error instanceof Error) {
65
+ LoggerHelpers.error(`Error bumping version: ${error.message}`);
66
+ } else {
67
+ LoggerHelpers.error(`Error bumping version: ${error}`);
68
+ }
69
+ process.exit(1);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Increments ONLY iOS build number (for TestFlight builds)
75
+ * Keeps version and Android build number unchanged
76
+ * Perfect for uploading new iOS builds without changing app version
77
+ *
78
+ * Example: 1.0.2+45 (iOS: 45) → 1.0.2+45 (iOS: 46)
79
+ */
80
+ async function bumpIosBuildOnly(): Promise<void> {
81
+ // Pre-flight validation
82
+ if (!validateFlutterProject()) {
83
+ process.exit(1);
84
+ }
85
+
86
+ try {
87
+ const current = getCurrentVersion();
88
+ const currentVersionString = `${current.major}.${current.minor}.${current.patch}`;
89
+
90
+ // iOS build number increments from current Android build number
91
+ const nextIosBuild = current.buildNumber + 1;
92
+
93
+ LoggerHelpers.info(`Current version: ${formatVersion(current)}`);
94
+ LoggerHelpers.info("Incrementing iOS build number only (for TestFlight)...");
95
+
96
+ console.log(chalk.cyan("\nBuild number changes:"));
97
+ console.log(chalk.gray(" Version:"), chalk.white(`${currentVersionString} (unchanged)`));
98
+ console.log(chalk.gray(" Android:"), chalk.white(`${current.buildNumber} (unchanged)`));
99
+ console.log(chalk.gray(" iOS:"), chalk.white(`${current.buildNumber} → ${nextIosBuild}`), chalk.green("(incremented)"));
100
+ console.log();
101
+
102
+ // Update only iOS build number
103
+ await updateFlutterVersion(
104
+ currentVersionString,
105
+ "", // Empty string means don't update Android
106
+ nextIosBuild.toString()
107
+ );
108
+
109
+ LoggerHelpers.success(`iOS build number incremented to ${nextIosBuild}`);
110
+ console.log(chalk.gray("\nResult:"), chalk.white(`${currentVersionString}+${current.buildNumber} (iOS: ${nextIosBuild})`));
111
+ console.log(chalk.gray("Use this for:"), chalk.white("TestFlight uploads without version changes"));
112
+
113
+ } catch (error) {
114
+ if (error instanceof Error) {
115
+ LoggerHelpers.error(`Error incrementing iOS build: ${error.message}`);
116
+ } else {
117
+ LoggerHelpers.error(`Error incrementing iOS build: ${error}`);
118
+ }
119
+ process.exit(1);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Increments ONLY Android build number
125
+ * Keeps version and iOS build number unchanged
126
+ */
127
+ async function bumpAndroidBuildOnly(): Promise<void> {
128
+ // Pre-flight validation
129
+ if (!validateFlutterProject()) {
130
+ process.exit(1);
131
+ }
132
+
133
+ try {
134
+ const current = getCurrentVersion();
135
+ const currentVersionString = `${current.major}.${current.minor}.${current.patch}`;
136
+ const nextAndroidBuild = current.buildNumber + 1;
137
+
138
+ LoggerHelpers.info(`Current version: ${formatVersion(current)}`);
139
+ LoggerHelpers.info("Incrementing Android build number only...");
140
+
141
+ console.log(chalk.cyan("\nBuild number changes:"));
142
+ console.log(chalk.gray(" Version:"), chalk.white(`${currentVersionString} (unchanged)`));
143
+ console.log(chalk.gray(" Android:"), chalk.white(`${current.buildNumber} → ${nextAndroidBuild}`), chalk.green("(incremented)"));
144
+ console.log(chalk.gray(" iOS:"), chalk.white("(unchanged)"));
145
+ console.log();
146
+
147
+ // Update only Android build number
148
+ await updateFlutterVersion(
149
+ currentVersionString,
150
+ nextAndroidBuild.toString(),
151
+ "" // Empty string means don't update iOS
152
+ );
153
+
154
+ LoggerHelpers.success(`Android build number incremented to ${nextAndroidBuild}`);
155
+
156
+ } catch (error) {
157
+ if (error instanceof Error) {
158
+ LoggerHelpers.error(`Error incrementing Android build: ${error.message}`);
159
+ } else {
160
+ LoggerHelpers.error(`Error incrementing Android build: ${error}`);
161
+ }
162
+ process.exit(1);
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Shows current version information
168
+ */
169
+ async function showCurrentVersion(): Promise<void> {
170
+ try {
171
+ const current = getCurrentVersion();
172
+ const versionString = formatVersion(current);
173
+
174
+ console.log(chalk.bold("\n📱 Current Version Information\n"));
175
+ console.log(chalk.cyan("Version:"), chalk.white.bold(versionString));
176
+ console.log(chalk.gray(" Major:"), chalk.white(current.major));
177
+ console.log(chalk.gray(" Minor:"), chalk.white(current.minor));
178
+ console.log(chalk.gray(" Patch:"), chalk.white(current.patch));
179
+ console.log(chalk.gray(" Build:"), chalk.white(current.buildNumber));
180
+ console.log();
181
+
182
+ } catch (error) {
183
+ if (error instanceof Error) {
184
+ LoggerHelpers.error(`Error reading version: ${error.message}`);
185
+ } else {
186
+ LoggerHelpers.error(`Error reading version: ${error}`);
187
+ }
188
+ process.exit(1);
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Helper to get next iOS build number
194
+ * In the future, this could read from Info.plist or project.pbxproj
195
+ * For now, we'll use a simple increment
196
+ */
197
+ async function getNextIosBuildNumber(): Promise<number> {
198
+ // TODO: Read actual iOS build number from Info.plist or project.pbxproj
199
+ // For now, we'll just increment based on timestamp or simple counter
200
+ const current = getCurrentVersion();
201
+ return current.buildNumber + 1;
202
+ }
@@ -1,7 +1,9 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { execInIos } from "../utils/execHelpers.js";
4
- import { LoggerHelpers } from "../utils/loggerHelpers.js";
3
+ import { execInIos } from "../../utils/services/exec.js";
4
+ import { LoggerHelpers } from "../../utils/services/logger.js";
5
+ import { validateFlutterProject } from "../../utils/validators/validation.js";
6
+ import { createBackup } from "../../utils/services/backup.js";
5
7
 
6
8
  const currentDir = process.cwd();
7
9
 
@@ -12,6 +14,11 @@ async function updateFlutterVersion(
12
14
  build: string,
13
15
  iosBuild: string
14
16
  ) {
17
+ // Pre-flight validation
18
+ if (!validateFlutterProject()) {
19
+ process.exit(1);
20
+ }
21
+
15
22
  try {
16
23
  // Always update the version since it is required.
17
24
  LoggerHelpers.info(`Starting update for version ${version}...`);
@@ -43,7 +50,7 @@ async function updateFlutterVersion(
43
50
  error instanceof Error ? error.message : String(error)
44
51
  }`
45
52
  );
46
- throw error;
53
+ process.exit(1);
47
54
  }
48
55
  }
49
56
 
@@ -55,6 +62,9 @@ async function updatePubspecVersionAndBuild(version: string, build: string) {
55
62
  throw new Error(`pubspec.yaml not found at ${pubspecPath}`);
56
63
  }
57
64
 
65
+ // Create backup before modification
66
+ createBackup(pubspecPath);
67
+
58
68
  const pubspecContent = fs.readFileSync(pubspecPath, "utf8");
59
69
 
60
70
  const updatedPubspec = pubspecContent.replace(
@@ -80,13 +90,16 @@ async function updatePubspecVersionAndBuild(version: string, build: string) {
80
90
  async function updateIosVersionAndBuild(version: string, iosBuild: string) {
81
91
  try {
82
92
  const currentDir = process.cwd();
83
-
93
+
84
94
  const projectPbxProjPath = path.join(currentDir, "ios/Runner.xcodeproj/project.pbxproj");
85
-
95
+
86
96
  if (!fs.existsSync(projectPbxProjPath)) {
87
97
  throw new Error(`project.pbxproj not found at ${projectPbxProjPath}`);
88
98
  }
89
-
99
+
100
+ // Create backup before modification
101
+ createBackup(projectPbxProjPath);
102
+
90
103
  let projectContent = fs.readFileSync(projectPbxProjPath, 'utf8');
91
104
 
92
105
  projectContent = projectContent
@@ -109,6 +122,9 @@ async function updateIosVersionAndBuild(version: string, iosBuild: string) {
109
122
  throw new Error(`Info.plist not found at ${infoPlistPath}`);
110
123
  }
111
124
 
125
+ // Create backup before modification
126
+ createBackup(infoPlistPath);
127
+
112
128
  const infoPlistContent = fs.readFileSync(infoPlistPath, "utf8");
113
129
 
114
130
  const updatedPlist = infoPlistContent
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Application-wide constants
3
+ */
4
+
5
+ // Build configurations
6
+ export const BUILD_CONFIGS = {
7
+ APK: {
8
+ outputPath: "build/app/outputs/symbols",
9
+ flags: ["--release", "--obfuscate"],
10
+ },
11
+ BUNDLE: {
12
+ outputPath: "build/app/outputs/symbols",
13
+ flags: ["--release", "--obfuscate"],
14
+ },
15
+ IOS: {
16
+ flags: ["--release"],
17
+ },
18
+ IPA: {
19
+ flags: ["--release"],
20
+ },
21
+ } as const;
22
+
23
+ // Project structure
24
+ export const PROJECT_PATHS = {
25
+ PUBSPEC: "pubspec.yaml",
26
+ PUBSPEC_LOCK: "pubspec.lock",
27
+ IOS_DIR: "ios",
28
+ ANDROID_DIR: "android",
29
+ LIB_DIR: "lib",
30
+ MODULE_DIR: "lib/module",
31
+ IOS_RUNNER_PROJ: "ios/Runner.xcodeproj",
32
+ IOS_RUNNER_WORKSPACE: "ios/Runner.xcworkspace",
33
+ IOS_PROJECT_PBXPROJ: "ios/Runner.xcodeproj/project.pbxproj",
34
+ IOS_INFO_PLIST: "ios/Runner/Info.plist",
35
+ IOS_PODFILE_LOCK: "ios/Podfile.lock",
36
+ ANDROID_BUILD_GRADLE: "android/build.gradle",
37
+ ANDROID_BUILD_GRADLE_KTS: "android/build.gradle.kts",
38
+ FVM_FLUTTER_SDK: ".fvm/flutter_sdk",
39
+ VSCODE_DIR: ".vscode",
40
+ VSCODE_SETTINGS: ".vscode/settings.json",
41
+ } as const;
42
+
43
+ // Backup configuration
44
+ export const BACKUP_CONFIG = {
45
+ DIR_NAME: ".optikit-backup",
46
+ RETENTION_COUNT: 5,
47
+ } as const;
48
+
49
+ // Module generation
50
+ export const MODULE_STRUCTURE = {
51
+ DIRECTORIES: ["bloc", "event", "state", "screen", "import", "factory"],
52
+ NAME_PATTERN: /^[a-z0-9_]+$/,
53
+ } as const;
54
+
55
+ // Flutter commands
56
+ export const FLUTTER_COMMANDS = {
57
+ CLEAN: "flutter clean",
58
+ PUB_GET: "flutter pub get",
59
+ BUILD_APK: "flutter build apk",
60
+ BUILD_BUNDLE: "flutter build appbundle",
61
+ BUILD_IOS: "flutter build ios",
62
+ BUILD_IPA: "flutter build ipa",
63
+ PRECACHE_IOS: "flutter precache --ios",
64
+ VERSION: "flutter --version",
65
+ } as const;
66
+
67
+ // FVM commands
68
+ export const FVM_COMMANDS = {
69
+ CLEAN: "fvm flutter clean",
70
+ PUB_GET: "fvm flutter pub get",
71
+ BUILD_APK: "fvm flutter build apk",
72
+ BUILD_BUNDLE: "fvm flutter build appbundle",
73
+ BUILD_IOS: "fvm flutter build ios",
74
+ BUILD_IPA: "fvm flutter build ipa",
75
+ PRECACHE_IOS: "fvm flutter precache --ios",
76
+ VERSION: "fvm --version",
77
+ } as const;
78
+
79
+ // iOS commands
80
+ export const IOS_COMMANDS = {
81
+ POD_DEINTEGRATE: "pod deintegrate",
82
+ POD_INSTALL: "pod install",
83
+ POD_UPDATE: "pod update",
84
+ POD_REPO_UPDATE: "pod repo update",
85
+ POD_CACHE_CLEAN: "pod cache clean --all",
86
+ } as const;
87
+
88
+ // IDE commands
89
+ export const IDE_COMMANDS = {
90
+ XCODE: "open ios/Runner.xcworkspace",
91
+ ANDROID_STUDIO: {
92
+ DARWIN: "open -a 'Android Studio' android",
93
+ WIN32: "start android",
94
+ LINUX: "xdg-open android",
95
+ },
96
+ } as const;
97
+
98
+ // VSCode settings template
99
+ export const VSCODE_SETTINGS_TEMPLATE = {
100
+ "dart.flutterSdkPath": ".fvm/flutter_sdk",
101
+ "editor.formatOnSave": true,
102
+ "dart.previewFlutterUiGuides": true,
103
+ "files.exclude": {
104
+ "**/.git": true,
105
+ "**/.DS_Store": true,
106
+ "**/node_modules": true,
107
+ "**/build": true,
108
+ },
109
+ } as const;
110
+
111
+ // Retry configuration
112
+ export const RETRY_CONFIG = {
113
+ DEFAULT_ATTEMPTS: 3,
114
+ DEFAULT_DELAY_MS: 10000,
115
+ IOS_TIMEOUT_MS: 600000,
116
+ } as const;
117
+
118
+ // Help URLs
119
+ export const HELP_URLS = {
120
+ FLUTTER_INSTALL: "https://flutter.dev/docs/get-started/install",
121
+ FVM_INSTALL: "https://fvm.app/docs/getting_started/installation",
122
+ } as const;
123
+
124
+ // Error messages
125
+ export const ERROR_MESSAGES = {
126
+ NOT_FLUTTER_PROJECT: "Not a Flutter project: pubspec.yaml not found.",
127
+ NO_FLUTTER_REFERENCE: "Not a Flutter project: pubspec.yaml does not reference Flutter SDK.",
128
+ FVM_NOT_FOUND: "FVM Flutter SDK not found at .fvm/flutter_sdk",
129
+ FLUTTER_NOT_FOUND: "Flutter SDK not found.",
130
+ IOS_PROJECT_NOT_FOUND: "iOS project directory not found.",
131
+ ANDROID_PROJECT_NOT_FOUND: "Android project directory not found.",
132
+ NO_XCODE_PROJECT: "No Xcode project or workspace found in ios/ directory.",
133
+ NO_BUILD_GRADLE: "No build.gradle found in android/ directory.",
134
+ MODULE_NAME_EMPTY: "Module name cannot be empty.",
135
+ MODULE_NAME_INVALID: "Module name must contain only lowercase letters, numbers, and underscores.",
136
+ } as const;
137
+
138
+ // Info messages
139
+ export const INFO_MESSAGES = {
140
+ RUN_FROM_PROJECT_ROOT: "Please run this command from the root of a Flutter project.",
141
+ ADD_IOS_SUPPORT: "Run 'flutter create .' to add iOS support.",
142
+ ADD_ANDROID_SUPPORT: "Run 'flutter create .' to add Android support.",
143
+ INSTALL_FVM_OR_DISABLE: "Run 'fvm install' or use --disable-fvm flag.",
144
+ } as const;
@@ -0,0 +1,80 @@
1
+ import { execCommand } from "../services/exec.js";
2
+ import { LoggerHelpers } from "../services/logger.js";
3
+ import { validateFlutterProject, validateFlutterSdk, validateIosProject, validateAndroidProject } from "../validators/validation.js";
4
+
5
+ export { executeBuild, BuildConfig };
6
+
7
+ /**
8
+ * Build configuration
9
+ */
10
+ interface BuildConfig {
11
+ /** Build type name (e.g., "APK", "Bundle", "iOS", "IPA") */
12
+ type: string;
13
+ /** Base Flutter command (e.g., "flutter build apk") */
14
+ command: string;
15
+ /** Additional build flags */
16
+ flags?: string[];
17
+ /** Whether to validate iOS project */
18
+ requireIos?: boolean;
19
+ /** Whether to validate Android project */
20
+ requireAndroid?: boolean;
21
+ }
22
+
23
+ /**
24
+ * Executes a Flutter build with common validation and error handling
25
+ *
26
+ * @param config - Build configuration
27
+ * @param noFvm - Whether to disable FVM usage
28
+ *
29
+ * @example
30
+ * await executeBuild({
31
+ * type: "APK",
32
+ * command: "flutter build apk",
33
+ * flags: ["--release", "--obfuscate", "--split-debug-info=build/app/outputs/symbols"],
34
+ * requireAndroid: true
35
+ * }, false);
36
+ */
37
+ async function executeBuild(config: BuildConfig, noFvm: boolean): Promise<void> {
38
+ const { type, command, flags = [], requireIos = false, requireAndroid = false } = config;
39
+
40
+ LoggerHelpers.info(
41
+ noFvm
42
+ ? `Building Flutter ${type} without FVM...`
43
+ : `Building Flutter ${type} with FVM...`
44
+ );
45
+
46
+ // Pre-flight validation
47
+ if (!validateFlutterProject()) {
48
+ process.exit(1);
49
+ }
50
+
51
+ if (!(await validateFlutterSdk(!noFvm))) {
52
+ process.exit(1);
53
+ }
54
+
55
+ if (requireIos && !validateIosProject()) {
56
+ process.exit(1);
57
+ }
58
+
59
+ if (requireAndroid && !validateAndroidProject()) {
60
+ process.exit(1);
61
+ }
62
+
63
+ // Build the full command
64
+ const baseCommand = noFvm ? command : command.replace(/^flutter\s/, "fvm flutter ");
65
+ const fullCommand = flags.length > 0
66
+ ? `${baseCommand} ${flags.join(" ")}`
67
+ : baseCommand;
68
+
69
+ try {
70
+ await execCommand(fullCommand);
71
+ LoggerHelpers.success(`Flutter ${type} build successful.`);
72
+ } catch (error) {
73
+ if (error instanceof Error) {
74
+ LoggerHelpers.error(`Error during ${type} build: ${error.message}`);
75
+ } else {
76
+ LoggerHelpers.error(`Error during ${type} build: ${error}`);
77
+ }
78
+ process.exit(1);
79
+ }
80
+ }
@@ -0,0 +1,124 @@
1
+ import { LoggerHelpers } from "../services/logger.js";
2
+ import chalk from "chalk";
3
+
4
+ export { DryRunManager, isDryRunMode, setDryRunMode };
5
+
6
+ /**
7
+ * Global dry-run state
8
+ */
9
+ let dryRunEnabled = false;
10
+
11
+ /**
12
+ * Check if dry-run mode is enabled
13
+ */
14
+ function isDryRunMode(): boolean {
15
+ return dryRunEnabled;
16
+ }
17
+
18
+ /**
19
+ * Set dry-run mode
20
+ */
21
+ function setDryRunMode(enabled: boolean): void {
22
+ dryRunEnabled = enabled;
23
+ if (enabled) {
24
+ LoggerHelpers.info(chalk.yellow("🔍 DRY-RUN MODE ENABLED - No commands will be executed"));
25
+ console.log(chalk.gray("Commands will be displayed but not run\n"));
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Dry-run manager for tracking and displaying operations
31
+ */
32
+ class DryRunManager {
33
+ private operations: Array<{
34
+ type: string;
35
+ description: string;
36
+ command?: string;
37
+ details?: string;
38
+ }> = [];
39
+
40
+ /**
41
+ * Log a command that would be executed
42
+ */
43
+ logCommand(description: string, command: string, details?: string): void {
44
+ if (!isDryRunMode()) return;
45
+
46
+ this.operations.push({
47
+ type: "command",
48
+ description,
49
+ command,
50
+ details,
51
+ });
52
+
53
+ console.log(chalk.cyan("→"), chalk.bold(description));
54
+ console.log(chalk.gray(" Command:"), chalk.white(command));
55
+ if (details) {
56
+ console.log(chalk.gray(" Details:"), details);
57
+ }
58
+ console.log();
59
+ }
60
+
61
+ /**
62
+ * Log a file operation that would be performed
63
+ */
64
+ logFileOperation(operation: string, filePath: string, details?: string): void {
65
+ if (!isDryRunMode()) return;
66
+
67
+ this.operations.push({
68
+ type: "file",
69
+ description: `${operation}: ${filePath}`,
70
+ details,
71
+ });
72
+
73
+ console.log(chalk.cyan("→"), chalk.bold(operation));
74
+ console.log(chalk.gray(" File:"), chalk.white(filePath));
75
+ if (details) {
76
+ console.log(chalk.gray(" Details:"), details);
77
+ }
78
+ console.log();
79
+ }
80
+
81
+ /**
82
+ * Log a validation check
83
+ */
84
+ logValidation(check: string, result: boolean, message?: string): void {
85
+ if (!isDryRunMode()) return;
86
+
87
+ const icon = result ? chalk.green("✓") : chalk.red("✗");
88
+ const status = result ? chalk.green("PASS") : chalk.red("FAIL");
89
+
90
+ console.log(icon, chalk.bold(check), status);
91
+ if (message) {
92
+ console.log(chalk.gray(" "), message);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Display summary of dry-run operations
98
+ */
99
+ displaySummary(): void {
100
+ if (!isDryRunMode() || this.operations.length === 0) return;
101
+
102
+ console.log(chalk.yellow("\n" + "=".repeat(60)));
103
+ console.log(chalk.yellow.bold("DRY-RUN SUMMARY"));
104
+ console.log(chalk.yellow("=".repeat(60)));
105
+
106
+ const commands = this.operations.filter((op) => op.type === "command");
107
+ const files = this.operations.filter((op) => op.type === "file");
108
+
109
+ console.log(chalk.white(`\nTotal operations: ${this.operations.length}`));
110
+ console.log(chalk.white(` Commands: ${commands.length}`));
111
+ console.log(chalk.white(` File operations: ${files.length}`));
112
+
113
+ console.log(chalk.yellow("\n" + "=".repeat(60)));
114
+ console.log(chalk.gray("No actual changes were made to your system."));
115
+ console.log(chalk.gray("Run without --dry-run to execute these operations.\n"));
116
+ }
117
+
118
+ /**
119
+ * Reset the operations log
120
+ */
121
+ reset(): void {
122
+ this.operations = [];
123
+ }
124
+ }
@@ -1,6 +1,6 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { capitalize } from "./stringHelpers.js";
3
+ import { capitalize } from "./string.js";
4
4
 
5
5
  export { createDirectories, writeFile, getClassName };
6
6
 
@@ -18,7 +18,8 @@ function writeFile(filePath: string, content: string) {
18
18
  }
19
19
 
20
20
  function getClassName(moduleName: string, type: string): string {
21
- const defineItems = moduleName.replace(type, "").split("_");
21
+ // Split module name by underscores and capitalize each part
22
+ const defineItems = moduleName.split("_").filter(item => item.length > 0);
22
23
  let className = "";
23
24
  defineItems.forEach((item) => {
24
25
  className += capitalize(item);