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.
- package/CHANGELOG.md +23 -2
- package/CLAUDE.md +239 -0
- package/CODE_QUALITY.md +398 -0
- package/ENHANCEMENTS.md +310 -0
- package/FEATURE_ENHANCEMENTS.md +435 -0
- package/README.md +46 -13
- package/SAFETY_FEATURES.md +396 -0
- package/USAGE.md +225 -41
- package/VERSION_MANAGEMENT.md +438 -0
- package/dist/cli.js +116 -7
- package/dist/commands/build/releases.js +57 -0
- package/dist/commands/buildReleases.js +48 -74
- package/dist/commands/clean/flutter.js +51 -0
- package/dist/commands/clean/ios.js +109 -0
- package/dist/commands/cleanProject.js +34 -4
- package/dist/commands/cleanProjectIos.js +17 -7
- package/dist/commands/config/init.js +54 -0
- package/dist/commands/config/rollback.js +161 -0
- package/dist/commands/generateModule.js +39 -11
- package/dist/commands/init.js +54 -0
- package/dist/commands/openProject.js +17 -0
- package/dist/commands/project/devices.js +188 -0
- package/dist/commands/project/generate.js +143 -0
- package/dist/commands/project/open.js +63 -0
- package/dist/commands/project/setup.js +46 -0
- package/dist/commands/rollback.js +161 -0
- package/dist/commands/setupVSCode.js +27 -21
- package/dist/commands/updateVersions.js +13 -1
- package/dist/commands/version/bump.js +161 -0
- package/dist/commands/version/update.js +91 -0
- package/dist/commands/version.js +161 -0
- package/dist/constants.js +131 -0
- package/dist/utils/backupHelpers.js +88 -0
- package/dist/utils/buildHelpers.js +55 -0
- package/dist/utils/commandHelpers.js +51 -0
- package/dist/utils/configHelpers.js +80 -0
- package/dist/utils/dryRunHelpers.js +103 -0
- package/dist/utils/fileHelpers.js +2 -1
- package/dist/utils/helpers/build.js +55 -0
- package/dist/utils/helpers/dryRun.js +103 -0
- package/dist/utils/helpers/file.js +24 -0
- package/dist/utils/helpers/string.js +3 -0
- package/dist/utils/helpers/version.js +80 -0
- package/dist/utils/services/backup.js +88 -0
- package/dist/utils/services/command.js +51 -0
- package/dist/utils/services/config.js +80 -0
- package/dist/utils/services/exec.js +132 -0
- package/dist/utils/services/logger.js +15 -0
- package/dist/utils/validationHelpers.js +101 -0
- package/dist/utils/validators/validation.js +101 -0
- package/dist/utils/versionHelpers.js +80 -0
- package/package.json +1 -1
- package/src/cli.ts +165 -7
- package/src/commands/build/releases.ts +79 -0
- package/src/commands/clean/flutter.ts +58 -0
- package/src/commands/{cleanProjectIos.ts → clean/ios.ts} +19 -10
- package/src/commands/config/init.ts +63 -0
- package/src/commands/config/rollback.ts +200 -0
- package/src/commands/project/devices.ts +246 -0
- package/src/commands/{generateModule.ts → project/generate.ts} +47 -17
- package/src/commands/{openProject.ts → project/open.ts} +26 -5
- package/src/commands/project/setup.ts +50 -0
- package/src/commands/version/bump.ts +202 -0
- package/src/commands/{updateVersions.ts → version/update.ts} +22 -6
- package/src/constants.ts +144 -0
- package/src/utils/helpers/build.ts +80 -0
- package/src/utils/helpers/dryRun.ts +124 -0
- package/src/utils/{fileHelpers.ts → helpers/file.ts} +3 -2
- package/src/utils/helpers/version.ts +109 -0
- package/src/utils/services/backup.ts +109 -0
- package/src/utils/services/command.ts +76 -0
- package/src/utils/services/config.ts +106 -0
- package/src/utils/{execHelpers.ts → services/exec.ts} +1 -1
- package/src/utils/validators/validation.ts +122 -0
- package/src/commands/buildReleases.ts +0 -102
- package/src/commands/cleanProject.ts +0 -25
- package/src/commands/setupVSCode.ts +0 -44
- /package/src/utils/{stringHelpers.ts → helpers/string.ts} +0 -0
- /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
|
-
|
|
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);
|