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,55 @@
|
|
|
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
|
+
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,103 @@
|
|
|
1
|
+
import { LoggerHelpers } from "../services/logger.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
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { capitalize } from "./string.js";
|
|
4
|
+
export { createDirectories, writeFile, getClassName };
|
|
5
|
+
function createDirectories(modulePath, directories) {
|
|
6
|
+
directories.forEach((dir) => {
|
|
7
|
+
const dirPath = path.join(modulePath, dir);
|
|
8
|
+
if (!fs.existsSync(dirPath)) {
|
|
9
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
function writeFile(filePath, content) {
|
|
14
|
+
fs.writeFileSync(filePath, content);
|
|
15
|
+
}
|
|
16
|
+
function getClassName(moduleName, type) {
|
|
17
|
+
// Split module name by underscores and capitalize each part
|
|
18
|
+
const defineItems = moduleName.split("_").filter(item => item.length > 0);
|
|
19
|
+
let className = "";
|
|
20
|
+
defineItems.forEach((item) => {
|
|
21
|
+
className += capitalize(item);
|
|
22
|
+
});
|
|
23
|
+
return className;
|
|
24
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
export { parseVersion, incrementVersion, getCurrentVersion };
|
|
4
|
+
/**
|
|
5
|
+
* Parses a version string in format "X.Y.Z+B"
|
|
6
|
+
* @param versionString - Version string (e.g., "1.2.3+45")
|
|
7
|
+
* @returns Parsed version info
|
|
8
|
+
*/
|
|
9
|
+
function parseVersion(versionString) {
|
|
10
|
+
const match = versionString.match(/^(\d+)\.(\d+)\.(\d+)\+(\d+)$/);
|
|
11
|
+
if (!match) {
|
|
12
|
+
throw new Error(`Invalid version format: ${versionString}. Expected format: X.Y.Z+B (e.g., 1.2.3+45)`);
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
major: parseInt(match[1], 10),
|
|
16
|
+
minor: parseInt(match[2], 10),
|
|
17
|
+
patch: parseInt(match[3], 10),
|
|
18
|
+
buildNumber: parseInt(match[4], 10),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Increments version based on type
|
|
23
|
+
* @param current - Current version info
|
|
24
|
+
* @param type - Type of increment (major, minor, patch)
|
|
25
|
+
* @param resetIosBuildNumber - Whether to reset iOS build number to 1
|
|
26
|
+
* @returns New version info
|
|
27
|
+
*/
|
|
28
|
+
function incrementVersion(current, type, resetIosBuildNumber = false) {
|
|
29
|
+
const newVersion = { ...current };
|
|
30
|
+
switch (type) {
|
|
31
|
+
case 'major':
|
|
32
|
+
newVersion.major += 1;
|
|
33
|
+
newVersion.minor = 0;
|
|
34
|
+
newVersion.patch = 0;
|
|
35
|
+
newVersion.buildNumber += 1;
|
|
36
|
+
break;
|
|
37
|
+
case 'minor':
|
|
38
|
+
newVersion.minor += 1;
|
|
39
|
+
newVersion.patch = 0;
|
|
40
|
+
newVersion.buildNumber += 1;
|
|
41
|
+
break;
|
|
42
|
+
case 'patch':
|
|
43
|
+
newVersion.patch += 1;
|
|
44
|
+
newVersion.buildNumber += 1;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
return newVersion;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Gets current version from pubspec.yaml
|
|
51
|
+
* @returns Current version info
|
|
52
|
+
*/
|
|
53
|
+
function getCurrentVersion() {
|
|
54
|
+
const pubspecPath = path.join(process.cwd(), "pubspec.yaml");
|
|
55
|
+
if (!fs.existsSync(pubspecPath)) {
|
|
56
|
+
throw new Error("pubspec.yaml not found. Are you in a Flutter project?");
|
|
57
|
+
}
|
|
58
|
+
const pubspecContent = fs.readFileSync(pubspecPath, "utf8");
|
|
59
|
+
const versionMatch = pubspecContent.match(/version:\s*(\d+\.\d+\.\d+\+\d+)/);
|
|
60
|
+
if (!versionMatch) {
|
|
61
|
+
throw new Error("No version found in pubspec.yaml");
|
|
62
|
+
}
|
|
63
|
+
return parseVersion(versionMatch[1]);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Formats version info to string
|
|
67
|
+
* @param version - Version info
|
|
68
|
+
* @returns Formatted version string (e.g., "1.2.3+45")
|
|
69
|
+
*/
|
|
70
|
+
export function formatVersion(version) {
|
|
71
|
+
return `${version.major}.${version.minor}.${version.patch}+${version.buildNumber}`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Gets the next build number from current version
|
|
75
|
+
* @returns Next build number
|
|
76
|
+
*/
|
|
77
|
+
export function getNextBuildNumber() {
|
|
78
|
+
const current = getCurrentVersion();
|
|
79
|
+
return current.buildNumber + 1;
|
|
80
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { LoggerHelpers } from "./logger.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,51 @@
|
|
|
1
|
+
import { validateFlutterProject, validateFlutterSdk, validateIosProject, validateAndroidProject } from "../validators/validation.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 "./logger.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,132 @@
|
|
|
1
|
+
import { exec, spawn } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { LoggerHelpers } from "./logger.js";
|
|
5
|
+
export const iosDirectory = path.join(process.cwd(), "ios");
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
export function handleExecResult(err, stdout, stderr) {
|
|
8
|
+
if (err) {
|
|
9
|
+
LoggerHelpers.error(`Error: ${err.message}`);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (stderr) {
|
|
13
|
+
LoggerHelpers.warning(`stderr: ${stderr}`);
|
|
14
|
+
}
|
|
15
|
+
if (stdout) {
|
|
16
|
+
LoggerHelpers.success(`stdout: ${stdout}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function execCommand(command) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const fullCommand = `${command}`;
|
|
22
|
+
const process = spawn(fullCommand, { shell: true });
|
|
23
|
+
let output = "";
|
|
24
|
+
let lastLogLine = "";
|
|
25
|
+
process.stdout.on("data", (data) => {
|
|
26
|
+
const dataString = data.toString().trim();
|
|
27
|
+
output += dataString;
|
|
28
|
+
if (dataString) {
|
|
29
|
+
if ((dataString.endsWith("ms") || dataString.endsWith("s")) &&
|
|
30
|
+
lastLogLine) {
|
|
31
|
+
lastLogLine += ` ${dataString}`;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
if (lastLogLine) {
|
|
35
|
+
LoggerHelpers.success(lastLogLine);
|
|
36
|
+
}
|
|
37
|
+
lastLogLine = dataString;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
process.stderr.on("data", (data) => {
|
|
42
|
+
LoggerHelpers.error(data.toString());
|
|
43
|
+
});
|
|
44
|
+
process.on("close", (code) => {
|
|
45
|
+
if (lastLogLine) {
|
|
46
|
+
LoggerHelpers.success(lastLogLine);
|
|
47
|
+
}
|
|
48
|
+
if (code !== 0) {
|
|
49
|
+
reject(new Error(`Command failed with exit code ${code}`));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
resolve(output);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
process.on("error", (err) => {
|
|
56
|
+
reject(new Error(`Failed to start subprocess: ${err.message}`));
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export async function execInIos(command) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const fullCommand = `cd "${iosDirectory}" && ${command}`;
|
|
63
|
+
const process = spawn(fullCommand, { shell: true, timeout: 600000 });
|
|
64
|
+
let output = "";
|
|
65
|
+
let lastLogLine = "";
|
|
66
|
+
process.stdout.on("data", (data) => {
|
|
67
|
+
const dataString = data.toString().trim();
|
|
68
|
+
output += dataString;
|
|
69
|
+
if (dataString) {
|
|
70
|
+
if ((dataString.endsWith("ms") || dataString.endsWith("s")) &&
|
|
71
|
+
lastLogLine) {
|
|
72
|
+
lastLogLine += ` ${dataString}`;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
if (lastLogLine) {
|
|
76
|
+
LoggerHelpers.success(lastLogLine);
|
|
77
|
+
}
|
|
78
|
+
lastLogLine = dataString;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
process.stderr.on("data", (data) => {
|
|
83
|
+
LoggerHelpers.error(data.toString());
|
|
84
|
+
});
|
|
85
|
+
process.on("close", (code) => {
|
|
86
|
+
if (lastLogLine) {
|
|
87
|
+
LoggerHelpers.success(lastLogLine);
|
|
88
|
+
}
|
|
89
|
+
if (code !== 0) {
|
|
90
|
+
reject(new Error(`Command failed with exit code ${code}`));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
resolve(output);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
process.on("error", (err) => {
|
|
97
|
+
reject(new Error(`Failed to start subprocess: ${err.message}`));
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
export async function execInIosWithRetry(command, retries = 3, delay = 5000) {
|
|
102
|
+
let attempts = 0;
|
|
103
|
+
let lastError;
|
|
104
|
+
while (attempts < retries) {
|
|
105
|
+
try {
|
|
106
|
+
const result = await execInIos(command);
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
attempts++;
|
|
111
|
+
lastError = error;
|
|
112
|
+
if (error instanceof Error) {
|
|
113
|
+
LoggerHelpers.error(`Attempt ${attempts} failed. Error: ${error.message}`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
LoggerHelpers.error(`Attempt ${attempts} failed. Unknown error: ${JSON.stringify(error)}`);
|
|
117
|
+
}
|
|
118
|
+
if (attempts >= retries) {
|
|
119
|
+
if (lastError instanceof Error) {
|
|
120
|
+
LoggerHelpers.error(`Command failed after ${retries} attempts: ${lastError.message}`);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
LoggerHelpers.error(`Command failed after ${retries} attempts: Unknown error`);
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`Command failed after ${retries} attempts: ${lastError instanceof Error ? lastError.message : "Unknown error"}`);
|
|
126
|
+
}
|
|
127
|
+
LoggerHelpers.error(`Retrying in ${delay / 1000} seconds...`);
|
|
128
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw lastError;
|
|
132
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
export class LoggerHelpers {
|
|
3
|
+
static success(message) {
|
|
4
|
+
console.log(chalk.green(message));
|
|
5
|
+
}
|
|
6
|
+
static error(message) {
|
|
7
|
+
console.log(chalk.red(message));
|
|
8
|
+
}
|
|
9
|
+
static warning(message) {
|
|
10
|
+
console.log(chalk.yellow(message));
|
|
11
|
+
}
|
|
12
|
+
static info(message) {
|
|
13
|
+
console.log(chalk.hex('#add8e6')(message));
|
|
14
|
+
}
|
|
15
|
+
}
|