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,161 @@
1
+ import { validateFlutterProject } from "../utils/validationHelpers.js";
2
+ import { LoggerHelpers } from "../utils/loggerHelpers.js";
3
+ import { getCurrentVersion, incrementVersion, formatVersion } from "../utils/versionHelpers.js";
4
+ import { updateFlutterVersion } from "./updateVersions.js";
5
+ import chalk from "chalk";
6
+ export { bumpVersion, bumpIosBuildOnly, bumpAndroidBuildOnly, showCurrentVersion };
7
+ /**
8
+ * Bumps version with semantic versioning (major, minor, patch)
9
+ * Android build number increments with version
10
+ * iOS build number resets to 1 on version change
11
+ *
12
+ * @param type - Type of version bump (major, minor, patch)
13
+ */
14
+ async function bumpVersion(type) {
15
+ // Pre-flight validation
16
+ if (!validateFlutterProject()) {
17
+ process.exit(1);
18
+ }
19
+ try {
20
+ const current = getCurrentVersion();
21
+ const newVersion = incrementVersion(current, type);
22
+ LoggerHelpers.info(`Current version: ${formatVersion(current)}`);
23
+ LoggerHelpers.info(`Bumping ${type} version...`);
24
+ console.log(chalk.cyan("\nVersion changes:"));
25
+ console.log(chalk.gray(" Old:"), chalk.white(formatVersion(current)));
26
+ console.log(chalk.gray(" New:"), chalk.green.bold(formatVersion(newVersion)));
27
+ console.log(chalk.cyan("\nBuild number strategy:"));
28
+ console.log(chalk.gray(" Android:"), chalk.white(`${current.buildNumber} → ${newVersion.buildNumber} (incremented)`));
29
+ console.log(chalk.gray(" iOS:"), chalk.white(`${current.buildNumber} → 1 (reset for new version)`));
30
+ console.log();
31
+ // Update with new version
32
+ // Android uses the incremented build number from version
33
+ // iOS gets reset to 1 for new version releases
34
+ await updateFlutterVersion(`${newVersion.major}.${newVersion.minor}.${newVersion.patch}`, newVersion.buildNumber.toString(), "1" // iOS always starts at 1 for new versions
35
+ );
36
+ LoggerHelpers.success(`Version bumped to ${formatVersion(newVersion)}`);
37
+ console.log(chalk.gray("\nAndroid build:"), chalk.white(newVersion.buildNumber));
38
+ console.log(chalk.gray("iOS build:"), chalk.white("1"));
39
+ }
40
+ catch (error) {
41
+ if (error instanceof Error) {
42
+ LoggerHelpers.error(`Error bumping version: ${error.message}`);
43
+ }
44
+ else {
45
+ LoggerHelpers.error(`Error bumping version: ${error}`);
46
+ }
47
+ process.exit(1);
48
+ }
49
+ }
50
+ /**
51
+ * Increments ONLY iOS build number (for TestFlight builds)
52
+ * Keeps version and Android build number unchanged
53
+ * Perfect for uploading new iOS builds without changing app version
54
+ *
55
+ * Example: 1.0.2+45 (iOS: 45) → 1.0.2+45 (iOS: 46)
56
+ */
57
+ async function bumpIosBuildOnly() {
58
+ // Pre-flight validation
59
+ if (!validateFlutterProject()) {
60
+ process.exit(1);
61
+ }
62
+ try {
63
+ const current = getCurrentVersion();
64
+ const currentVersionString = `${current.major}.${current.minor}.${current.patch}`;
65
+ // iOS build number increments from current Android build number
66
+ const nextIosBuild = current.buildNumber + 1;
67
+ LoggerHelpers.info(`Current version: ${formatVersion(current)}`);
68
+ LoggerHelpers.info("Incrementing iOS build number only (for TestFlight)...");
69
+ console.log(chalk.cyan("\nBuild number changes:"));
70
+ console.log(chalk.gray(" Version:"), chalk.white(`${currentVersionString} (unchanged)`));
71
+ console.log(chalk.gray(" Android:"), chalk.white(`${current.buildNumber} (unchanged)`));
72
+ console.log(chalk.gray(" iOS:"), chalk.white(`${current.buildNumber} → ${nextIosBuild}`), chalk.green("(incremented)"));
73
+ console.log();
74
+ // Update only iOS build number
75
+ await updateFlutterVersion(currentVersionString, "", // Empty string means don't update Android
76
+ nextIosBuild.toString());
77
+ LoggerHelpers.success(`iOS build number incremented to ${nextIosBuild}`);
78
+ console.log(chalk.gray("\nResult:"), chalk.white(`${currentVersionString}+${current.buildNumber} (iOS: ${nextIosBuild})`));
79
+ console.log(chalk.gray("Use this for:"), chalk.white("TestFlight uploads without version changes"));
80
+ }
81
+ catch (error) {
82
+ if (error instanceof Error) {
83
+ LoggerHelpers.error(`Error incrementing iOS build: ${error.message}`);
84
+ }
85
+ else {
86
+ LoggerHelpers.error(`Error incrementing iOS build: ${error}`);
87
+ }
88
+ process.exit(1);
89
+ }
90
+ }
91
+ /**
92
+ * Increments ONLY Android build number
93
+ * Keeps version and iOS build number unchanged
94
+ */
95
+ async function bumpAndroidBuildOnly() {
96
+ // Pre-flight validation
97
+ if (!validateFlutterProject()) {
98
+ process.exit(1);
99
+ }
100
+ try {
101
+ const current = getCurrentVersion();
102
+ const currentVersionString = `${current.major}.${current.minor}.${current.patch}`;
103
+ const nextAndroidBuild = current.buildNumber + 1;
104
+ LoggerHelpers.info(`Current version: ${formatVersion(current)}`);
105
+ LoggerHelpers.info("Incrementing Android build number only...");
106
+ console.log(chalk.cyan("\nBuild number changes:"));
107
+ console.log(chalk.gray(" Version:"), chalk.white(`${currentVersionString} (unchanged)`));
108
+ console.log(chalk.gray(" Android:"), chalk.white(`${current.buildNumber} → ${nextAndroidBuild}`), chalk.green("(incremented)"));
109
+ console.log(chalk.gray(" iOS:"), chalk.white("(unchanged)"));
110
+ console.log();
111
+ // Update only Android build number
112
+ await updateFlutterVersion(currentVersionString, nextAndroidBuild.toString(), "" // Empty string means don't update iOS
113
+ );
114
+ LoggerHelpers.success(`Android build number incremented to ${nextAndroidBuild}`);
115
+ }
116
+ catch (error) {
117
+ if (error instanceof Error) {
118
+ LoggerHelpers.error(`Error incrementing Android build: ${error.message}`);
119
+ }
120
+ else {
121
+ LoggerHelpers.error(`Error incrementing Android build: ${error}`);
122
+ }
123
+ process.exit(1);
124
+ }
125
+ }
126
+ /**
127
+ * Shows current version information
128
+ */
129
+ async function showCurrentVersion() {
130
+ try {
131
+ const current = getCurrentVersion();
132
+ const versionString = formatVersion(current);
133
+ console.log(chalk.bold("\n📱 Current Version Information\n"));
134
+ console.log(chalk.cyan("Version:"), chalk.white.bold(versionString));
135
+ console.log(chalk.gray(" Major:"), chalk.white(current.major));
136
+ console.log(chalk.gray(" Minor:"), chalk.white(current.minor));
137
+ console.log(chalk.gray(" Patch:"), chalk.white(current.patch));
138
+ console.log(chalk.gray(" Build:"), chalk.white(current.buildNumber));
139
+ console.log();
140
+ }
141
+ catch (error) {
142
+ if (error instanceof Error) {
143
+ LoggerHelpers.error(`Error reading version: ${error.message}`);
144
+ }
145
+ else {
146
+ LoggerHelpers.error(`Error reading version: ${error}`);
147
+ }
148
+ process.exit(1);
149
+ }
150
+ }
151
+ /**
152
+ * Helper to get next iOS build number
153
+ * In the future, this could read from Info.plist or project.pbxproj
154
+ * For now, we'll use a simple increment
155
+ */
156
+ async function getNextIosBuildNumber() {
157
+ // TODO: Read actual iOS build number from Info.plist or project.pbxproj
158
+ // For now, we'll just increment based on timestamp or simple counter
159
+ const current = getCurrentVersion();
160
+ return current.buildNumber + 1;
161
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Application-wide constants
3
+ */
4
+ // Build configurations
5
+ export const BUILD_CONFIGS = {
6
+ APK: {
7
+ outputPath: "build/app/outputs/symbols",
8
+ flags: ["--release", "--obfuscate"],
9
+ },
10
+ BUNDLE: {
11
+ outputPath: "build/app/outputs/symbols",
12
+ flags: ["--release", "--obfuscate"],
13
+ },
14
+ IOS: {
15
+ flags: ["--release"],
16
+ },
17
+ IPA: {
18
+ flags: ["--release"],
19
+ },
20
+ };
21
+ // Project structure
22
+ export const PROJECT_PATHS = {
23
+ PUBSPEC: "pubspec.yaml",
24
+ PUBSPEC_LOCK: "pubspec.lock",
25
+ IOS_DIR: "ios",
26
+ ANDROID_DIR: "android",
27
+ LIB_DIR: "lib",
28
+ MODULE_DIR: "lib/module",
29
+ IOS_RUNNER_PROJ: "ios/Runner.xcodeproj",
30
+ IOS_RUNNER_WORKSPACE: "ios/Runner.xcworkspace",
31
+ IOS_PROJECT_PBXPROJ: "ios/Runner.xcodeproj/project.pbxproj",
32
+ IOS_INFO_PLIST: "ios/Runner/Info.plist",
33
+ IOS_PODFILE_LOCK: "ios/Podfile.lock",
34
+ ANDROID_BUILD_GRADLE: "android/build.gradle",
35
+ ANDROID_BUILD_GRADLE_KTS: "android/build.gradle.kts",
36
+ FVM_FLUTTER_SDK: ".fvm/flutter_sdk",
37
+ VSCODE_DIR: ".vscode",
38
+ VSCODE_SETTINGS: ".vscode/settings.json",
39
+ };
40
+ // Backup configuration
41
+ export const BACKUP_CONFIG = {
42
+ DIR_NAME: ".optikit-backup",
43
+ RETENTION_COUNT: 5,
44
+ };
45
+ // Module generation
46
+ export const MODULE_STRUCTURE = {
47
+ DIRECTORIES: ["bloc", "event", "state", "screen", "import", "factory"],
48
+ NAME_PATTERN: /^[a-z0-9_]+$/,
49
+ };
50
+ // Flutter commands
51
+ export const FLUTTER_COMMANDS = {
52
+ CLEAN: "flutter clean",
53
+ PUB_GET: "flutter pub get",
54
+ BUILD_APK: "flutter build apk",
55
+ BUILD_BUNDLE: "flutter build appbundle",
56
+ BUILD_IOS: "flutter build ios",
57
+ BUILD_IPA: "flutter build ipa",
58
+ PRECACHE_IOS: "flutter precache --ios",
59
+ VERSION: "flutter --version",
60
+ };
61
+ // FVM commands
62
+ export const FVM_COMMANDS = {
63
+ CLEAN: "fvm flutter clean",
64
+ PUB_GET: "fvm flutter pub get",
65
+ BUILD_APK: "fvm flutter build apk",
66
+ BUILD_BUNDLE: "fvm flutter build appbundle",
67
+ BUILD_IOS: "fvm flutter build ios",
68
+ BUILD_IPA: "fvm flutter build ipa",
69
+ PRECACHE_IOS: "fvm flutter precache --ios",
70
+ VERSION: "fvm --version",
71
+ };
72
+ // iOS commands
73
+ export const IOS_COMMANDS = {
74
+ POD_DEINTEGRATE: "pod deintegrate",
75
+ POD_INSTALL: "pod install",
76
+ POD_UPDATE: "pod update",
77
+ POD_REPO_UPDATE: "pod repo update",
78
+ POD_CACHE_CLEAN: "pod cache clean --all",
79
+ };
80
+ // IDE commands
81
+ export const IDE_COMMANDS = {
82
+ XCODE: "open ios/Runner.xcworkspace",
83
+ ANDROID_STUDIO: {
84
+ DARWIN: "open -a 'Android Studio' android",
85
+ WIN32: "start android",
86
+ LINUX: "xdg-open android",
87
+ },
88
+ };
89
+ // VSCode settings template
90
+ export const VSCODE_SETTINGS_TEMPLATE = {
91
+ "dart.flutterSdkPath": ".fvm/flutter_sdk",
92
+ "editor.formatOnSave": true,
93
+ "dart.previewFlutterUiGuides": true,
94
+ "files.exclude": {
95
+ "**/.git": true,
96
+ "**/.DS_Store": true,
97
+ "**/node_modules": true,
98
+ "**/build": true,
99
+ },
100
+ };
101
+ // Retry configuration
102
+ export const RETRY_CONFIG = {
103
+ DEFAULT_ATTEMPTS: 3,
104
+ DEFAULT_DELAY_MS: 10000,
105
+ IOS_TIMEOUT_MS: 600000,
106
+ };
107
+ // Help URLs
108
+ export const HELP_URLS = {
109
+ FLUTTER_INSTALL: "https://flutter.dev/docs/get-started/install",
110
+ FVM_INSTALL: "https://fvm.app/docs/getting_started/installation",
111
+ };
112
+ // Error messages
113
+ export const ERROR_MESSAGES = {
114
+ NOT_FLUTTER_PROJECT: "Not a Flutter project: pubspec.yaml not found.",
115
+ NO_FLUTTER_REFERENCE: "Not a Flutter project: pubspec.yaml does not reference Flutter SDK.",
116
+ FVM_NOT_FOUND: "FVM Flutter SDK not found at .fvm/flutter_sdk",
117
+ FLUTTER_NOT_FOUND: "Flutter SDK not found.",
118
+ IOS_PROJECT_NOT_FOUND: "iOS project directory not found.",
119
+ ANDROID_PROJECT_NOT_FOUND: "Android project directory not found.",
120
+ NO_XCODE_PROJECT: "No Xcode project or workspace found in ios/ directory.",
121
+ NO_BUILD_GRADLE: "No build.gradle found in android/ directory.",
122
+ MODULE_NAME_EMPTY: "Module name cannot be empty.",
123
+ MODULE_NAME_INVALID: "Module name must contain only lowercase letters, numbers, and underscores.",
124
+ };
125
+ // Info messages
126
+ export const INFO_MESSAGES = {
127
+ RUN_FROM_PROJECT_ROOT: "Please run this command from the root of a Flutter project.",
128
+ ADD_IOS_SUPPORT: "Run 'flutter create .' to add iOS support.",
129
+ ADD_ANDROID_SUPPORT: "Run 'flutter create .' to add Android support.",
130
+ INSTALL_FVM_OR_DISABLE: "Run 'fvm install' or use --disable-fvm flag.",
131
+ };
@@ -0,0 +1,88 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { LoggerHelpers } from "./loggerHelpers.js";
4
+ export { createBackup, restoreBackup, cleanupBackups, getBackupPath };
5
+ /**
6
+ * Creates a backup of a file with timestamp
7
+ * Returns the backup path if successful, null otherwise
8
+ */
9
+ function createBackup(filePath) {
10
+ try {
11
+ if (!fs.existsSync(filePath)) {
12
+ LoggerHelpers.warning(`File does not exist, skipping backup: ${filePath}`);
13
+ return null;
14
+ }
15
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
16
+ const parsedPath = path.parse(filePath);
17
+ const backupPath = path.join(parsedPath.dir, `.optikit-backup`, `${parsedPath.name}_${timestamp}${parsedPath.ext}`);
18
+ // Create backup directory if it doesn't exist
19
+ const backupDir = path.dirname(backupPath);
20
+ if (!fs.existsSync(backupDir)) {
21
+ fs.mkdirSync(backupDir, { recursive: true });
22
+ }
23
+ // Copy file to backup location
24
+ fs.copyFileSync(filePath, backupPath);
25
+ LoggerHelpers.info(`Backup created: ${backupPath}`);
26
+ return backupPath;
27
+ }
28
+ catch (error) {
29
+ LoggerHelpers.error(`Failed to create backup: ${error instanceof Error ? error.message : error}`);
30
+ return null;
31
+ }
32
+ }
33
+ /**
34
+ * Restores a file from backup
35
+ */
36
+ function restoreBackup(originalPath, backupPath) {
37
+ try {
38
+ if (!fs.existsSync(backupPath)) {
39
+ LoggerHelpers.error(`Backup file not found: ${backupPath}`);
40
+ return false;
41
+ }
42
+ fs.copyFileSync(backupPath, originalPath);
43
+ LoggerHelpers.success(`Restored from backup: ${originalPath}`);
44
+ return true;
45
+ }
46
+ catch (error) {
47
+ LoggerHelpers.error(`Failed to restore backup: ${error instanceof Error ? error.message : error}`);
48
+ return false;
49
+ }
50
+ }
51
+ /**
52
+ * Cleans up old backups (keeps only the most recent N backups)
53
+ */
54
+ function cleanupBackups(directory, keepCount = 5) {
55
+ try {
56
+ const backupDir = path.join(directory, ".optikit-backup");
57
+ if (!fs.existsSync(backupDir)) {
58
+ return;
59
+ }
60
+ const files = fs.readdirSync(backupDir);
61
+ // Sort by modification time (newest first)
62
+ const sortedFiles = files
63
+ .map((file) => ({
64
+ name: file,
65
+ path: path.join(backupDir, file),
66
+ mtime: fs.statSync(path.join(backupDir, file)).mtime.getTime(),
67
+ }))
68
+ .sort((a, b) => b.mtime - a.mtime);
69
+ // Delete old backups beyond keepCount
70
+ if (sortedFiles.length > keepCount) {
71
+ const filesToDelete = sortedFiles.slice(keepCount);
72
+ filesToDelete.forEach((file) => {
73
+ fs.unlinkSync(file.path);
74
+ LoggerHelpers.info(`Cleaned up old backup: ${file.name}`);
75
+ });
76
+ }
77
+ }
78
+ catch (error) {
79
+ LoggerHelpers.warning(`Failed to cleanup backups: ${error instanceof Error ? error.message : error}`);
80
+ }
81
+ }
82
+ /**
83
+ * Gets the backup directory path for a given file
84
+ */
85
+ function getBackupPath(filePath) {
86
+ const parsedPath = path.parse(filePath);
87
+ return path.join(parsedPath.dir, ".optikit-backup");
88
+ }
@@ -0,0 +1,55 @@
1
+ import { execCommand } from "./execHelpers.js";
2
+ import { LoggerHelpers } from "./loggerHelpers.js";
3
+ import { validateFlutterProject, validateFlutterSdk, validateIosProject, validateAndroidProject } from "./validationHelpers.js";
4
+ export { executeBuild };
5
+ /**
6
+ * Executes a Flutter build with common validation and error handling
7
+ *
8
+ * @param config - Build configuration
9
+ * @param noFvm - Whether to disable FVM usage
10
+ *
11
+ * @example
12
+ * await executeBuild({
13
+ * type: "APK",
14
+ * command: "flutter build apk",
15
+ * flags: ["--release", "--obfuscate", "--split-debug-info=build/app/outputs/symbols"],
16
+ * requireAndroid: true
17
+ * }, false);
18
+ */
19
+ async function executeBuild(config, noFvm) {
20
+ const { type, command, flags = [], requireIos = false, requireAndroid = false } = config;
21
+ LoggerHelpers.info(noFvm
22
+ ? `Building Flutter ${type} without FVM...`
23
+ : `Building Flutter ${type} with FVM...`);
24
+ // Pre-flight validation
25
+ if (!validateFlutterProject()) {
26
+ process.exit(1);
27
+ }
28
+ if (!(await validateFlutterSdk(!noFvm))) {
29
+ process.exit(1);
30
+ }
31
+ if (requireIos && !validateIosProject()) {
32
+ process.exit(1);
33
+ }
34
+ if (requireAndroid && !validateAndroidProject()) {
35
+ process.exit(1);
36
+ }
37
+ // Build the full command
38
+ const baseCommand = noFvm ? command : command.replace(/^flutter\s/, "fvm flutter ");
39
+ const fullCommand = flags.length > 0
40
+ ? `${baseCommand} ${flags.join(" ")}`
41
+ : baseCommand;
42
+ try {
43
+ await execCommand(fullCommand);
44
+ LoggerHelpers.success(`Flutter ${type} build successful.`);
45
+ }
46
+ catch (error) {
47
+ if (error instanceof Error) {
48
+ LoggerHelpers.error(`Error during ${type} build: ${error.message}`);
49
+ }
50
+ else {
51
+ LoggerHelpers.error(`Error during ${type} build: ${error}`);
52
+ }
53
+ process.exit(1);
54
+ }
55
+ }
@@ -0,0 +1,51 @@
1
+ import { validateFlutterProject, validateFlutterSdk, validateIosProject, validateAndroidProject } from "./validationHelpers.js";
2
+ export { validateBuildEnvironment, getFlutterCommand };
3
+ /**
4
+ * Performs common validation checks for commands
5
+ * Exits with code 1 if any validation fails
6
+ *
7
+ * @param options - Validation options
8
+ * @returns true if all validations pass (won't return false, exits instead)
9
+ */
10
+ function validateBuildEnvironment(options) {
11
+ const { requireFlutterProject = true, requireFlutterSdk = false, requireIosProject = false, requireAndroidProject = false, useFvm = false, } = options;
12
+ // Flutter project validation
13
+ if (requireFlutterProject && !validateFlutterProject()) {
14
+ process.exit(1);
15
+ }
16
+ // Flutter SDK validation (async)
17
+ if (requireFlutterSdk) {
18
+ validateFlutterSdk(useFvm).then((isValid) => {
19
+ if (!isValid) {
20
+ process.exit(1);
21
+ }
22
+ });
23
+ }
24
+ // iOS project validation
25
+ if (requireIosProject && !validateIosProject()) {
26
+ process.exit(1);
27
+ }
28
+ // Android project validation
29
+ if (requireAndroidProject && !validateAndroidProject()) {
30
+ process.exit(1);
31
+ }
32
+ return true;
33
+ }
34
+ /**
35
+ * Gets the appropriate Flutter command based on FVM usage
36
+ *
37
+ * @param baseCommand - The base Flutter command (e.g., "flutter clean")
38
+ * @param useFvm - Whether to use FVM
39
+ * @returns The command string with or without FVM prefix
40
+ *
41
+ * @example
42
+ * getFlutterCommand("flutter clean", true) // "fvm flutter clean"
43
+ * getFlutterCommand("flutter pub get", false) // "flutter pub get"
44
+ */
45
+ function getFlutterCommand(baseCommand, useFvm) {
46
+ if (useFvm) {
47
+ // Replace "flutter" with "fvm flutter"
48
+ return baseCommand.replace(/^flutter\s/, "fvm flutter ");
49
+ }
50
+ return baseCommand;
51
+ }
@@ -0,0 +1,80 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { LoggerHelpers } from "./loggerHelpers.js";
4
+ export { loadConfig, saveConfig, getConfigPath };
5
+ /**
6
+ * Default configuration
7
+ */
8
+ const DEFAULT_CONFIG = {
9
+ backupRetentionCount: 5,
10
+ useFvmByDefault: false,
11
+ autoBackup: true,
12
+ verbose: false,
13
+ };
14
+ /**
15
+ * Gets the config file path
16
+ * Checks multiple locations in priority order:
17
+ * 1. .optikitrc in current directory
18
+ * 2. .optikitrc in home directory
19
+ *
20
+ * @returns Config file path or null if not found
21
+ */
22
+ function getConfigPath() {
23
+ const cwd = process.cwd();
24
+ const home = process.env.HOME || process.env.USERPROFILE || "";
25
+ const possiblePaths = [
26
+ path.join(cwd, ".optikitrc"),
27
+ path.join(cwd, ".optikitrc.json"),
28
+ path.join(home, ".optikitrc"),
29
+ path.join(home, ".optikitrc.json"),
30
+ ];
31
+ for (const configPath of possiblePaths) {
32
+ if (fs.existsSync(configPath)) {
33
+ return configPath;
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+ /**
39
+ * Loads configuration from .optikitrc file
40
+ * Falls back to default configuration if file doesn't exist
41
+ *
42
+ * @returns Merged configuration (defaults + user config)
43
+ */
44
+ function loadConfig() {
45
+ const configPath = getConfigPath();
46
+ if (!configPath) {
47
+ return { ...DEFAULT_CONFIG };
48
+ }
49
+ try {
50
+ const fileContent = fs.readFileSync(configPath, "utf8");
51
+ const userConfig = JSON.parse(fileContent);
52
+ // Merge with defaults
53
+ return {
54
+ ...DEFAULT_CONFIG,
55
+ ...userConfig,
56
+ };
57
+ }
58
+ catch (error) {
59
+ LoggerHelpers.warning(`Failed to load config from ${configPath}, using defaults.`);
60
+ return { ...DEFAULT_CONFIG };
61
+ }
62
+ }
63
+ /**
64
+ * Saves configuration to .optikitrc file in current directory
65
+ *
66
+ * @param config - Configuration to save
67
+ * @returns true if successful, false otherwise
68
+ */
69
+ function saveConfig(config) {
70
+ const configPath = path.join(process.cwd(), ".optikitrc.json");
71
+ try {
72
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
73
+ LoggerHelpers.success(`Configuration saved to ${configPath}`);
74
+ return true;
75
+ }
76
+ catch (error) {
77
+ LoggerHelpers.error(`Failed to save config: ${error instanceof Error ? error.message : error}`);
78
+ return false;
79
+ }
80
+ }
@@ -0,0 +1,103 @@
1
+ import { LoggerHelpers } from "./loggerHelpers.js";
2
+ import chalk from "chalk";
3
+ export { DryRunManager, isDryRunMode, setDryRunMode };
4
+ /**
5
+ * Global dry-run state
6
+ */
7
+ let dryRunEnabled = false;
8
+ /**
9
+ * Check if dry-run mode is enabled
10
+ */
11
+ function isDryRunMode() {
12
+ return dryRunEnabled;
13
+ }
14
+ /**
15
+ * Set dry-run mode
16
+ */
17
+ function setDryRunMode(enabled) {
18
+ dryRunEnabled = enabled;
19
+ if (enabled) {
20
+ LoggerHelpers.info(chalk.yellow("🔍 DRY-RUN MODE ENABLED - No commands will be executed"));
21
+ console.log(chalk.gray("Commands will be displayed but not run\n"));
22
+ }
23
+ }
24
+ /**
25
+ * Dry-run manager for tracking and displaying operations
26
+ */
27
+ class DryRunManager {
28
+ operations = [];
29
+ /**
30
+ * Log a command that would be executed
31
+ */
32
+ logCommand(description, command, details) {
33
+ if (!isDryRunMode())
34
+ return;
35
+ this.operations.push({
36
+ type: "command",
37
+ description,
38
+ command,
39
+ details,
40
+ });
41
+ console.log(chalk.cyan("→"), chalk.bold(description));
42
+ console.log(chalk.gray(" Command:"), chalk.white(command));
43
+ if (details) {
44
+ console.log(chalk.gray(" Details:"), details);
45
+ }
46
+ console.log();
47
+ }
48
+ /**
49
+ * Log a file operation that would be performed
50
+ */
51
+ logFileOperation(operation, filePath, details) {
52
+ if (!isDryRunMode())
53
+ return;
54
+ this.operations.push({
55
+ type: "file",
56
+ description: `${operation}: ${filePath}`,
57
+ details,
58
+ });
59
+ console.log(chalk.cyan("→"), chalk.bold(operation));
60
+ console.log(chalk.gray(" File:"), chalk.white(filePath));
61
+ if (details) {
62
+ console.log(chalk.gray(" Details:"), details);
63
+ }
64
+ console.log();
65
+ }
66
+ /**
67
+ * Log a validation check
68
+ */
69
+ logValidation(check, result, message) {
70
+ if (!isDryRunMode())
71
+ return;
72
+ const icon = result ? chalk.green("✓") : chalk.red("✗");
73
+ const status = result ? chalk.green("PASS") : chalk.red("FAIL");
74
+ console.log(icon, chalk.bold(check), status);
75
+ if (message) {
76
+ console.log(chalk.gray(" "), message);
77
+ }
78
+ }
79
+ /**
80
+ * Display summary of dry-run operations
81
+ */
82
+ displaySummary() {
83
+ if (!isDryRunMode() || this.operations.length === 0)
84
+ return;
85
+ console.log(chalk.yellow("\n" + "=".repeat(60)));
86
+ console.log(chalk.yellow.bold("DRY-RUN SUMMARY"));
87
+ console.log(chalk.yellow("=".repeat(60)));
88
+ const commands = this.operations.filter((op) => op.type === "command");
89
+ const files = this.operations.filter((op) => op.type === "file");
90
+ console.log(chalk.white(`\nTotal operations: ${this.operations.length}`));
91
+ console.log(chalk.white(` Commands: ${commands.length}`));
92
+ console.log(chalk.white(` File operations: ${files.length}`));
93
+ console.log(chalk.yellow("\n" + "=".repeat(60)));
94
+ console.log(chalk.gray("No actual changes were made to your system."));
95
+ console.log(chalk.gray("Run without --dry-run to execute these operations.\n"));
96
+ }
97
+ /**
98
+ * Reset the operations log
99
+ */
100
+ reset() {
101
+ this.operations = [];
102
+ }
103
+ }
@@ -14,7 +14,8 @@ function writeFile(filePath, content) {
14
14
  fs.writeFileSync(filePath, content);
15
15
  }
16
16
  function getClassName(moduleName, type) {
17
- const defineItems = moduleName.replace(type, "").split("_");
17
+ // Split module name by underscores and capitalize each part
18
+ const defineItems = moduleName.split("_").filter(item => item.length > 0);
18
19
  let className = "";
19
20
  defineItems.forEach((item) => {
20
21
  className += capitalize(item);