optikit 1.2.5 → 1.3.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 (104) hide show
  1. package/.claude/commands/build.md +57 -0
  2. package/.claude/commands/clean.md +45 -0
  3. package/.claude/commands/generate.md +64 -0
  4. package/.claude/commands/rollback.md +67 -0
  5. package/.claude/commands/run.md +63 -0
  6. package/.claude/commands/setup.md +69 -0
  7. package/.claude/commands/sync-docs.md +148 -0
  8. package/.claude/commands/version.md +63 -0
  9. package/.claude/settings.local.json +28 -0
  10. package/.claude-plugin/marketplace.json +36 -0
  11. package/.claude-plugin/plugin.json +25 -0
  12. package/.history/README_20260325211923.md +268 -0
  13. package/.history/README_20260325212116.md +268 -0
  14. package/.history/README_20260325212234.md +266 -0
  15. package/.history/README_20260325221838.md +321 -0
  16. package/.history/README_20260325221920.md +327 -0
  17. package/.history/README_20260325222245.md +328 -0
  18. package/.history/README_20260325222247.md +328 -0
  19. package/.mcp.json +8 -0
  20. package/CHANGELOG.md +67 -98
  21. package/CLAUDE.md +57 -206
  22. package/OPTIKIT_AGENT.md +398 -0
  23. package/README.md +293 -60
  24. package/dist/cli.js +75 -244
  25. package/dist/commands/build/commands.js +146 -0
  26. package/dist/commands/build/testflight.js +14 -0
  27. package/dist/commands/clean/commands.js +41 -0
  28. package/dist/commands/clean/flutter.js +8 -14
  29. package/dist/commands/clean/ios.js +12 -15
  30. package/dist/commands/config/aliases.js +122 -0
  31. package/dist/commands/config/commands.js +49 -0
  32. package/dist/commands/config/initApp.js +191 -0
  33. package/dist/commands/config/rollback.js +15 -4
  34. package/dist/commands/config/upgrade.js +36 -0
  35. package/dist/commands/mcp/commands.js +21 -0
  36. package/dist/commands/mcp/server.js +27 -0
  37. package/dist/commands/mcp/setup.js +62 -0
  38. package/dist/commands/mcp/tools.js +359 -0
  39. package/dist/commands/project/commands.js +132 -0
  40. package/dist/commands/project/devices.js +10 -26
  41. package/dist/commands/project/doctor.js +58 -0
  42. package/dist/commands/project/generate.js +183 -30
  43. package/dist/commands/project/setup.js +10 -28
  44. package/dist/commands/project/status.js +65 -0
  45. package/dist/commands/version/bump.js +75 -101
  46. package/dist/commands/version/commands.js +63 -0
  47. package/dist/commands/version/update.js +36 -24
  48. package/dist/constants.js +6 -1
  49. package/dist/styles.js +42 -5
  50. package/dist/utils/helpers/error.js +14 -0
  51. package/dist/utils/helpers/file.js +1 -1
  52. package/dist/utils/helpers/version.js +2 -1
  53. package/dist/utils/services/backup.js +12 -1
  54. package/dist/utils/services/command.js +1 -34
  55. package/dist/utils/services/exec.js +76 -101
  56. package/dist/utils/services/logger.js +10 -4
  57. package/dist/utils/validators/validation.js +24 -12
  58. package/docs/INSTALLATION.md +72 -0
  59. package/docs/TROUBLESHOOT.md +140 -0
  60. package/docs/USAGE.md +185 -0
  61. package/docs/VERSION_MANAGEMENT.md +177 -0
  62. package/package.json +7 -11
  63. package/src/cli.ts +82 -371
  64. package/src/commands/build/commands.ts +169 -0
  65. package/src/commands/build/testflight.ts +18 -0
  66. package/src/commands/clean/commands.ts +43 -0
  67. package/src/commands/clean/flutter.ts +9 -13
  68. package/src/commands/clean/ios.ts +13 -13
  69. package/src/commands/config/aliases.ts +150 -0
  70. package/src/commands/config/commands.ts +50 -0
  71. package/src/commands/config/initApp.ts +213 -0
  72. package/src/commands/config/rollback.ts +16 -4
  73. package/src/commands/config/upgrade.ts +40 -0
  74. package/src/commands/mcp/commands.ts +23 -0
  75. package/src/commands/mcp/server.ts +35 -0
  76. package/src/commands/mcp/setup.ts +69 -0
  77. package/src/commands/mcp/tools.ts +365 -0
  78. package/src/commands/project/commands.ts +132 -0
  79. package/src/commands/project/devices.ts +11 -24
  80. package/src/commands/project/doctor.ts +81 -0
  81. package/src/commands/project/generate.ts +211 -32
  82. package/src/commands/project/setup.ts +13 -30
  83. package/src/commands/project/status.ts +72 -0
  84. package/src/commands/version/bump.ts +98 -110
  85. package/src/commands/version/commands.ts +76 -0
  86. package/src/commands/version/update.ts +86 -75
  87. package/src/constants.ts +7 -1
  88. package/src/styles.ts +49 -7
  89. package/src/utils/helpers/error.ts +16 -0
  90. package/src/utils/helpers/file.ts +1 -1
  91. package/src/utils/helpers/version.ts +2 -1
  92. package/src/utils/services/backup.ts +17 -1
  93. package/src/utils/services/command.ts +1 -58
  94. package/src/utils/services/exec.ts +92 -117
  95. package/src/utils/services/logger.ts +12 -4
  96. package/src/utils/validators/validation.ts +24 -12
  97. package/CODE_QUALITY.md +0 -398
  98. package/ENHANCEMENTS.md +0 -310
  99. package/FEATURE_ENHANCEMENTS.md +0 -435
  100. package/INSTALLATION.md +0 -118
  101. package/SAFETY_FEATURES.md +0 -396
  102. package/TROUBLESHOOT.md +0 -60
  103. package/USAGE.md +0 -412
  104. package/VERSION_MANAGEMENT.md +0 -438
@@ -1,24 +1,55 @@
1
+ import { createInterface } from "readline";
1
2
  import { validateFlutterProject } from "../../utils/validators/validation.js";
2
3
  import { LoggerHelpers } from "../../utils/services/logger.js";
3
4
  import { getCurrentVersion, incrementVersion, formatVersion, getCurrentIosBuildNumber } from "../../utils/helpers/version.js";
5
+ import { handleCommandError } from "../../utils/helpers/error.js";
4
6
  import { updateFlutterVersion } from "./update.js";
5
7
  import chalk from "chalk";
6
8
  export { bumpVersion, bumpIosBuildOnly, bumpAndroidBuildOnly, bumpBothBuilds, showCurrentVersion };
7
9
  /**
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)
10
+ * Prompts user for confirmation. Returns true if confirmed.
11
+ * Skips prompt and returns true if skipConfirm is true (--yes flag).
13
12
  */
14
- async function bumpVersion(type) {
15
- // Pre-flight validation
13
+ function confirmAction(message, skipConfirm) {
14
+ if (skipConfirm)
15
+ return Promise.resolve(true);
16
+ return new Promise((resolve) => {
17
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
18
+ rl.question(chalk.yellow(`${message} (y/n): `), (answer) => {
19
+ rl.close();
20
+ resolve(answer.toLowerCase() === "y");
21
+ });
22
+ });
23
+ }
24
+ /**
25
+ * Shows a colored diff preview of what will change.
26
+ */
27
+ function showDiffPreview(oldVersion, newVersion, oldAndroidBuild, newAndroidBuild, oldIosBuild, newIosBuild) {
28
+ console.log(chalk.cyan("\nFile changes preview:"));
29
+ if (newAndroidBuild !== "") {
30
+ console.log(chalk.gray("\n pubspec.yaml:"));
31
+ console.log(chalk.red(` - version: ${oldVersion}+${oldAndroidBuild}`));
32
+ console.log(chalk.green(` + version: ${newVersion}+${newAndroidBuild}`));
33
+ }
34
+ if (newIosBuild !== "") {
35
+ console.log(chalk.gray("\n project.pbxproj:"));
36
+ console.log(chalk.red(` - MARKETING_VERSION = ${oldVersion};`));
37
+ console.log(chalk.green(` + MARKETING_VERSION = ${newVersion};`));
38
+ console.log(chalk.red(` - CURRENT_PROJECT_VERSION = ${oldIosBuild};`));
39
+ console.log(chalk.green(` + CURRENT_PROJECT_VERSION = ${newIosBuild};`));
40
+ }
41
+ console.log();
42
+ }
43
+ async function bumpVersion(type, skipConfirm = false) {
16
44
  if (!validateFlutterProject()) {
17
45
  process.exit(1);
18
46
  }
19
47
  try {
20
48
  const current = getCurrentVersion();
49
+ const currentIosBuild = getCurrentIosBuildNumber();
21
50
  const newVersion = incrementVersion(current, type);
51
+ const currentVersionString = `${current.major}.${current.minor}.${current.patch}`;
52
+ const newVersionString = `${newVersion.major}.${newVersion.minor}.${newVersion.patch}`;
22
53
  LoggerHelpers.info(`Current version: ${formatVersion(current)}`);
23
54
  LoggerHelpers.info(`Bumping ${type} version...`);
24
55
  console.log(chalk.cyan("\nVersion changes:"));
@@ -26,43 +57,29 @@ async function bumpVersion(type) {
26
57
  console.log(chalk.gray(" New:"), chalk.green.bold(formatVersion(newVersion)));
27
58
  console.log(chalk.cyan("\nBuild number strategy:"));
28
59
  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
- );
60
+ console.log(chalk.gray(" iOS:"), chalk.white(`${currentIosBuild} → 1 (reset for new version)`));
61
+ showDiffPreview(currentVersionString, newVersionString, current.buildNumber, newVersion.buildNumber, currentIosBuild, 1);
62
+ const confirmed = await confirmAction("Proceed with version bump?", skipConfirm);
63
+ if (!confirmed) {
64
+ LoggerHelpers.info("Bump cancelled.");
65
+ return;
66
+ }
67
+ await updateFlutterVersion(newVersionString, newVersion.buildNumber.toString(), "1");
36
68
  LoggerHelpers.success(`Version bumped to ${formatVersion(newVersion)}`);
37
69
  console.log(chalk.gray("\nAndroid build:"), chalk.white(newVersion.buildNumber));
38
70
  console.log(chalk.gray("iOS build:"), chalk.white("1"));
39
71
  }
40
72
  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);
73
+ handleCommandError(error, "Error bumping version");
48
74
  }
49
75
  }
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
76
+ async function bumpIosBuildOnly(skipConfirm = false) {
59
77
  if (!validateFlutterProject()) {
60
78
  process.exit(1);
61
79
  }
62
80
  try {
63
81
  const current = getCurrentVersion();
64
82
  const currentVersionString = `${current.major}.${current.minor}.${current.patch}`;
65
- // Read actual iOS build number from project.pbxproj
66
83
  const currentIosBuild = getCurrentIosBuildNumber();
67
84
  const nextIosBuild = currentIosBuild + 1;
68
85
  LoggerHelpers.info(`Current version: ${formatVersion(current)}`);
@@ -71,36 +88,27 @@ async function bumpIosBuildOnly() {
71
88
  console.log(chalk.gray(" Version:"), chalk.white(`${currentVersionString} (unchanged)`));
72
89
  console.log(chalk.gray(" Android:"), chalk.white(`${current.buildNumber} (unchanged)`));
73
90
  console.log(chalk.gray(" iOS:"), chalk.white(`${currentIosBuild} → ${nextIosBuild}`), chalk.green("(incremented)"));
74
- console.log();
75
- // Update only iOS build number
76
- await updateFlutterVersion(currentVersionString, "", // Empty string means don't update Android
77
- nextIosBuild.toString());
91
+ showDiffPreview(currentVersionString, currentVersionString, current.buildNumber, "", currentIosBuild, nextIosBuild);
92
+ const confirmed = await confirmAction("Proceed?", skipConfirm);
93
+ if (!confirmed) {
94
+ LoggerHelpers.info("Bump cancelled.");
95
+ return;
96
+ }
97
+ await updateFlutterVersion(currentVersionString, "", nextIosBuild.toString());
78
98
  LoggerHelpers.success(`iOS build number incremented to ${nextIosBuild}`);
79
- console.log(chalk.gray("\nResult:"), chalk.white(`${currentVersionString}+${current.buildNumber} (iOS: ${nextIosBuild})`));
80
- console.log(chalk.gray("Use this for:"), chalk.white("TestFlight uploads without version changes"));
81
99
  }
82
100
  catch (error) {
83
- if (error instanceof Error) {
84
- LoggerHelpers.error(`Error incrementing iOS build: ${error.message}`);
85
- }
86
- else {
87
- LoggerHelpers.error(`Error incrementing iOS build: ${error}`);
88
- }
89
- process.exit(1);
101
+ handleCommandError(error, "Error incrementing iOS build");
90
102
  }
91
103
  }
92
- /**
93
- * Increments ONLY Android build number
94
- * Keeps version and iOS build number unchanged
95
- */
96
- async function bumpAndroidBuildOnly() {
97
- // Pre-flight validation
104
+ async function bumpAndroidBuildOnly(skipConfirm = false) {
98
105
  if (!validateFlutterProject()) {
99
106
  process.exit(1);
100
107
  }
101
108
  try {
102
109
  const current = getCurrentVersion();
103
110
  const currentVersionString = `${current.major}.${current.minor}.${current.patch}`;
111
+ const currentIosBuild = getCurrentIosBuildNumber();
104
112
  const nextAndroidBuild = current.buildNumber + 1;
105
113
  LoggerHelpers.info(`Current version: ${formatVersion(current)}`);
106
114
  LoggerHelpers.info("Incrementing Android build number only...");
@@ -108,36 +116,26 @@ async function bumpAndroidBuildOnly() {
108
116
  console.log(chalk.gray(" Version:"), chalk.white(`${currentVersionString} (unchanged)`));
109
117
  console.log(chalk.gray(" Android:"), chalk.white(`${current.buildNumber} → ${nextAndroidBuild}`), chalk.green("(incremented)"));
110
118
  console.log(chalk.gray(" iOS:"), chalk.white("(unchanged)"));
111
- console.log();
112
- // Update only Android build number
113
- await updateFlutterVersion(currentVersionString, nextAndroidBuild.toString(), "" // Empty string means don't update iOS
114
- );
119
+ showDiffPreview(currentVersionString, currentVersionString, current.buildNumber, nextAndroidBuild, currentIosBuild, "");
120
+ const confirmed = await confirmAction("Proceed?", skipConfirm);
121
+ if (!confirmed) {
122
+ LoggerHelpers.info("Bump cancelled.");
123
+ return;
124
+ }
125
+ await updateFlutterVersion(currentVersionString, nextAndroidBuild.toString(), "");
115
126
  LoggerHelpers.success(`Android build number incremented to ${nextAndroidBuild}`);
116
127
  }
117
128
  catch (error) {
118
- if (error instanceof Error) {
119
- LoggerHelpers.error(`Error incrementing Android build: ${error.message}`);
120
- }
121
- else {
122
- LoggerHelpers.error(`Error incrementing Android build: ${error}`);
123
- }
124
- process.exit(1);
129
+ handleCommandError(error, "Error incrementing Android build");
125
130
  }
126
131
  }
127
- /**
128
- * Increments BOTH Android and iOS build numbers
129
- * Keeps version unchanged
130
- * Perfect for updating both platforms simultaneously
131
- */
132
- async function bumpBothBuilds() {
133
- // Pre-flight validation
132
+ async function bumpBothBuilds(skipConfirm = false) {
134
133
  if (!validateFlutterProject()) {
135
134
  process.exit(1);
136
135
  }
137
136
  try {
138
137
  const current = getCurrentVersion();
139
138
  const currentVersionString = `${current.major}.${current.minor}.${current.patch}`;
140
- // Read actual iOS build number from project.pbxproj
141
139
  const currentIosBuild = getCurrentIosBuildNumber();
142
140
  const nextAndroidBuild = current.buildNumber + 1;
143
141
  const nextIosBuild = currentIosBuild + 1;
@@ -147,26 +145,19 @@ async function bumpBothBuilds() {
147
145
  console.log(chalk.gray(" Version:"), chalk.white(`${currentVersionString} (unchanged)`));
148
146
  console.log(chalk.gray(" Android:"), chalk.white(`${current.buildNumber} → ${nextAndroidBuild}`), chalk.green("(incremented)"));
149
147
  console.log(chalk.gray(" iOS:"), chalk.white(`${currentIosBuild} → ${nextIosBuild}`), chalk.green("(incremented)"));
150
- console.log();
151
- // Update both Android and iOS build numbers
148
+ showDiffPreview(currentVersionString, currentVersionString, current.buildNumber, nextAndroidBuild, currentIosBuild, nextIosBuild);
149
+ const confirmed = await confirmAction("Proceed?", skipConfirm);
150
+ if (!confirmed) {
151
+ LoggerHelpers.info("Bump cancelled.");
152
+ return;
153
+ }
152
154
  await updateFlutterVersion(currentVersionString, nextAndroidBuild.toString(), nextIosBuild.toString());
153
155
  LoggerHelpers.success(`Both build numbers incremented successfully`);
154
- console.log(chalk.gray("\nResult:"), chalk.white(`${currentVersionString}+${nextAndroidBuild} (iOS: ${nextIosBuild})`));
155
- console.log(chalk.gray("Use this for:"), chalk.white("Simultaneous Android and iOS releases"));
156
156
  }
157
157
  catch (error) {
158
- if (error instanceof Error) {
159
- LoggerHelpers.error(`Error incrementing build numbers: ${error.message}`);
160
- }
161
- else {
162
- LoggerHelpers.error(`Error incrementing build numbers: ${error}`);
163
- }
164
- process.exit(1);
158
+ handleCommandError(error, "Error incrementing build numbers");
165
159
  }
166
160
  }
167
- /**
168
- * Shows current version information
169
- */
170
161
  async function showCurrentVersion() {
171
162
  try {
172
163
  const current = getCurrentVersion();
@@ -182,23 +173,6 @@ async function showCurrentVersion() {
182
173
  console.log();
183
174
  }
184
175
  catch (error) {
185
- if (error instanceof Error) {
186
- LoggerHelpers.error(`Error reading version: ${error.message}`);
187
- }
188
- else {
189
- LoggerHelpers.error(`Error reading version: ${error}`);
190
- }
191
- process.exit(1);
176
+ handleCommandError(error, "Error reading version");
192
177
  }
193
178
  }
194
- /**
195
- * Helper to get next iOS build number
196
- * In the future, this could read from Info.plist or project.pbxproj
197
- * For now, we'll use a simple increment
198
- */
199
- async function getNextIosBuildNumber() {
200
- // TODO: Read actual iOS build number from Info.plist or project.pbxproj
201
- // For now, we'll just increment based on timestamp or simple counter
202
- const current = getCurrentVersion();
203
- return current.buildNumber + 1;
204
- }
@@ -0,0 +1,63 @@
1
+ import { bumpVersion, bumpIosBuildOnly, bumpAndroidBuildOnly, bumpBothBuilds, showCurrentVersion, } from "./bump.js";
2
+ import { updateFlutterVersion } from "./update.js";
3
+ const yesOption = {
4
+ yes: { alias: "y", type: "boolean", default: false, description: "Skip confirmation prompt" },
5
+ };
6
+ export const versionCommands = [
7
+ {
8
+ command: "bump <type>",
9
+ describe: "Bump version (major, minor, or patch)",
10
+ builder: (yargs) => {
11
+ return yargs
12
+ .positional("type", {
13
+ describe: "Version bump type (major, minor, patch)",
14
+ type: "string",
15
+ choices: ["major", "minor", "patch"],
16
+ })
17
+ .option("yes", yesOption.yes);
18
+ },
19
+ handler: async (argv) => {
20
+ await bumpVersion(argv.type, argv.yes);
21
+ },
22
+ },
23
+ {
24
+ command: "bump-ios",
25
+ aliases: ["bi"],
26
+ describe: "Increment iOS build number only (for TestFlight)",
27
+ builder: yesOption,
28
+ handler: async (argv) => { await bumpIosBuildOnly(argv.yes); },
29
+ },
30
+ {
31
+ command: "bump-android",
32
+ aliases: ["ba"],
33
+ describe: "Increment Android build number only",
34
+ builder: yesOption,
35
+ handler: async (argv) => { await bumpAndroidBuildOnly(argv.yes); },
36
+ },
37
+ {
38
+ command: "bump-build",
39
+ aliases: ["bb"],
40
+ describe: "Increment both Android and iOS build numbers",
41
+ builder: yesOption,
42
+ handler: async (argv) => { await bumpBothBuilds(argv.yes); },
43
+ },
44
+ {
45
+ command: "version",
46
+ aliases: ["v"],
47
+ describe: "Show current version information",
48
+ handler: async () => { await showCurrentVersion(); },
49
+ },
50
+ {
51
+ command: "flutter-update-version",
52
+ aliases: ["vset"],
53
+ describe: "Update version and build numbers for both Android and iOS",
54
+ builder: {
55
+ "app-version": { type: "string", description: "The version number to set", default: "" },
56
+ "android-build": { type: "string", description: "The Android build number", default: "" },
57
+ "ios-build": { type: "string", description: "The iOS build number", default: "" },
58
+ },
59
+ handler: async (argv) => {
60
+ await updateFlutterVersion(argv["app-version"], argv["android-build"], argv["ios-build"]);
61
+ },
62
+ },
63
+ ];
@@ -2,85 +2,97 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import { LoggerHelpers } from "../../utils/services/logger.js";
4
4
  import { validateFlutterProject } from "../../utils/validators/validation.js";
5
- import { createBackup } from "../../utils/services/backup.js";
5
+ import { createBackupWithCleanup } from "../../utils/services/backup.js";
6
+ import { handleCommandError } from "../../utils/helpers/error.js";
7
+ import { isDryRunMode, DryRunManager } from "../../utils/helpers/dryRun.js";
8
+ import { PROJECT_PATHS } from "../../constants.js";
6
9
  const currentDir = process.cwd();
7
10
  export { updateFlutterVersion };
8
11
  async function updateFlutterVersion(version, build, iosBuild) {
9
- // Pre-flight validation
10
12
  if (!validateFlutterProject()) {
11
13
  process.exit(1);
12
14
  }
13
15
  try {
14
- // Always update the version since it is required.
15
16
  LoggerHelpers.info(`Starting update for version ${version}...`);
16
- // Update pubspec.yaml only if an Android build number is provided.
17
+ // Single dry-run manager for all operations
18
+ const dryRun = isDryRunMode() ? new DryRunManager() : null;
17
19
  if (build.trim().length > 0) {
18
20
  LoggerHelpers.info("Updating build number in pubspec.yaml...");
19
- await updatePubspecVersionAndBuild(version, build);
21
+ await updatePubspecVersionAndBuild(version, build, dryRun);
20
22
  }
21
23
  else {
22
24
  LoggerHelpers.info("Android build number not provided. Skipping pubspec.yaml update.");
23
25
  }
24
- // Update iOS settings only if an iOS build number is provided.
25
26
  if (iosBuild.trim().length > 0) {
26
27
  LoggerHelpers.info("Updating iOS version and build number...");
27
- await updateIosVersionAndBuild(version, iosBuild);
28
+ await updateIosVersionAndBuild(version, iosBuild, dryRun);
28
29
  }
29
30
  else {
30
31
  LoggerHelpers.info("iOS build number not provided. Skipping iOS update.");
31
32
  }
33
+ // Show dry-run summary once at the end
34
+ if (dryRun) {
35
+ dryRun.displaySummary();
36
+ return;
37
+ }
32
38
  LoggerHelpers.success(`Update complete. Version set to ${version}` +
33
39
  (build.trim().length > 0 ? `, Android build set to ${build}` : "") +
34
40
  (iosBuild.trim().length > 0 ? `, and iOS build set to ${iosBuild}` : ""));
35
41
  }
36
42
  catch (error) {
37
- LoggerHelpers.error(`Error while updating Flutter version and build: ${error instanceof Error ? error.message : String(error)}`);
38
- process.exit(1);
43
+ handleCommandError(error, "Error while updating Flutter version and build");
39
44
  }
40
45
  }
41
- async function updatePubspecVersionAndBuild(version, build) {
46
+ async function updatePubspecVersionAndBuild(version, build, dryRun) {
42
47
  try {
43
- const pubspecPath = path.join(currentDir, "pubspec.yaml");
48
+ const pubspecPath = path.join(currentDir, PROJECT_PATHS.PUBSPEC);
44
49
  if (!fs.existsSync(pubspecPath)) {
45
50
  throw new Error(`pubspec.yaml not found at ${pubspecPath}`);
46
51
  }
47
- // Create backup before modification
48
- createBackup(pubspecPath);
52
+ if (dryRun) {
53
+ dryRun.logFileOperation("Update pubspec.yaml", pubspecPath, `version: ${version}+${build}`);
54
+ return;
55
+ }
56
+ createBackupWithCleanup(pubspecPath);
49
57
  const pubspecContent = fs.readFileSync(pubspecPath, "utf8");
50
58
  const updatedPubspec = pubspecContent.replace(/version: \d+\.\d+\.\d+\+\d+/g, `version: ${version}+${build}`);
51
59
  fs.writeFileSync(pubspecPath, updatedPubspec);
52
60
  LoggerHelpers.success(`Updated pubspec.yaml with version ${version} and build ${build}`);
53
61
  }
54
62
  catch (error) {
55
- LoggerHelpers.error(`Error while updating pubspec.yaml version and build: ${error instanceof Error ? error.message : String(error)}`);
63
+ LoggerHelpers.error(`Error while updating pubspec.yaml: ${error instanceof Error ? error.message : String(error)}`);
56
64
  throw error;
57
65
  }
58
66
  }
59
- async function updateIosVersionAndBuild(version, iosBuild) {
67
+ async function updateIosVersionAndBuild(version, iosBuild, dryRun) {
60
68
  try {
61
- const currentDir = process.cwd();
62
- const projectPbxProjPath = path.join(currentDir, "ios/Runner.xcodeproj/project.pbxproj");
69
+ const projectPbxProjPath = path.join(currentDir, PROJECT_PATHS.IOS_PROJECT_PBXPROJ);
70
+ const infoPlistPath = path.join(currentDir, PROJECT_PATHS.IOS_INFO_PLIST);
63
71
  if (!fs.existsSync(projectPbxProjPath)) {
64
72
  throw new Error(`project.pbxproj not found at ${projectPbxProjPath}`);
65
73
  }
66
- // Create backup before modification
67
- createBackup(projectPbxProjPath);
74
+ if (dryRun) {
75
+ dryRun.logFileOperation("Update project.pbxproj", projectPbxProjPath, `MARKETING_VERSION = ${version}, CURRENT_PROJECT_VERSION = ${iosBuild}`);
76
+ dryRun.logFileOperation("Update Info.plist", infoPlistPath, `CFBundleShortVersionString = ${version}, CFBundleVersion = ${iosBuild}`);
77
+ return;
78
+ }
79
+ // Update project.pbxproj
80
+ createBackupWithCleanup(projectPbxProjPath);
68
81
  let projectContent = fs.readFileSync(projectPbxProjPath, 'utf8');
69
82
  projectContent = projectContent
70
83
  .replace(/MARKETING_VERSION\s*=\s*[^;]+;/g, `MARKETING_VERSION = ${version};`)
71
84
  .replace(/CURRENT_PROJECT_VERSION\s*=\s*[^;]+;/g, `CURRENT_PROJECT_VERSION = ${iosBuild};`);
72
85
  fs.writeFileSync(projectPbxProjPath, projectContent);
73
86
  LoggerHelpers.success(`Updated MARKETING_VERSION to ${version} and CURRENT_PROJECT_VERSION to ${iosBuild}`);
74
- const infoPlistPath = path.join(currentDir, "ios/Runner/Info.plist");
87
+ // Update Info.plist
75
88
  if (!fs.existsSync(infoPlistPath)) {
76
89
  throw new Error(`Info.plist not found at ${infoPlistPath}`);
77
90
  }
78
- // Create backup before modification
79
- createBackup(infoPlistPath);
91
+ createBackupWithCleanup(infoPlistPath);
80
92
  const infoPlistContent = fs.readFileSync(infoPlistPath, "utf8");
81
93
  const updatedPlist = infoPlistContent
82
- .replace(/<key>CFBundleShortVersionString<\/key>\s*<string>\$\{MARKETING_VERSION\}<\/string>/, `<key>CFBundleShortVersionString</key><string>${version}</string>`)
83
- .replace(/<key>CFBundleVersion<\/key>\s*<string>\$\{CURRENT_PROJECT_VERSION\}<\/string>/, `<key>CFBundleVersion</key><string>${iosBuild}</string>`);
94
+ .replace(/<key>CFBundleShortVersionString<\/key>\s*<string>[^<]+<\/string>/, `<key>CFBundleShortVersionString</key>\n\t<string>${version}</string>`)
95
+ .replace(/<key>CFBundleVersion<\/key>\s*<string>[^<]+<\/string>/, `<key>CFBundleVersion</key>\n\t<string>${iosBuild}</string>`);
84
96
  fs.writeFileSync(infoPlistPath, updatedPlist);
85
97
  LoggerHelpers.success("Updated Info.plist with the new version and iOS build.");
86
98
  }
package/dist/constants.js CHANGED
@@ -44,7 +44,7 @@ export const BACKUP_CONFIG = {
44
44
  };
45
45
  // Module generation
46
46
  export const MODULE_STRUCTURE = {
47
- DIRECTORIES: ["bloc", "event", "state", "screen", "import", "factory"],
47
+ DIRECTORIES: ["bloc", "event", "state", "screen", "import", "factory", "repo"],
48
48
  NAME_PATTERN: /^[a-z0-9_]+$/,
49
49
  };
50
50
  // Flutter commands
@@ -122,6 +122,11 @@ export const ERROR_MESSAGES = {
122
122
  MODULE_NAME_EMPTY: "Module name cannot be empty.",
123
123
  MODULE_NAME_INVALID: "Module name must contain only lowercase letters, numbers, and underscores.",
124
124
  };
125
+ // MCP server configuration
126
+ export const MCP_CONFIG = {
127
+ SERVER_NAME: "optikit",
128
+ CLAUDE_SETTINGS_FILE: ".claude.json",
129
+ };
125
130
  // Info messages
126
131
  export const INFO_MESSAGES = {
127
132
  RUN_FROM_PROJECT_ROOT: "Please run this command from the root of a Flutter project.",
package/dist/styles.js CHANGED
@@ -1,7 +1,44 @@
1
+ import chalk from 'chalk';
1
2
  export const boxenOptions = {
2
- padding: 1,
3
- margin: 1,
4
- borderStyle: 'round', // Cast to the proper type
5
- borderColor: 'blue',
6
- backgroundColor: '#555555',
3
+ padding: { top: 1, bottom: 1, left: 3, right: 3 },
4
+ margin: { top: 1, bottom: 0, left: 1, right: 1 },
5
+ borderStyle: 'round',
6
+ borderColor: 'cyan',
7
7
  };
8
+ export function createBanner(version) {
9
+ const greeting = chalk.white.bold('Hello ^_^');
10
+ const logo = chalk.cyan.bold('OptiKit CLI');
11
+ const ver = chalk.gray(` v${version}`);
12
+ const tagline = chalk.white('Flutter & Opticore Toolkit');
13
+ const divider = chalk.gray('─────────────────────────');
14
+ return `${greeting}\n${divider}\n${logo}${ver}\n${tagline}`;
15
+ }
16
+ export const bannerBoxOptions = {
17
+ padding: { top: 1, bottom: 1, left: 3, right: 3 },
18
+ margin: { top: 1, bottom: 0, left: 1, right: 1 },
19
+ borderStyle: 'round',
20
+ borderColor: 'cyan',
21
+ textAlignment: 'center',
22
+ };
23
+ export const icons = {
24
+ success: chalk.green('✔'),
25
+ error: chalk.red('✖'),
26
+ warning: chalk.yellow('⚠'),
27
+ info: chalk.cyan('ℹ'),
28
+ arrow: chalk.cyan('→'),
29
+ dot: chalk.gray('·'),
30
+ star: chalk.yellow('★'),
31
+ rocket: '🚀',
32
+ package: '📦',
33
+ phone: '📱',
34
+ wrench: '🔧',
35
+ check: '✅',
36
+ cross: '❌',
37
+ clock: '⏱',
38
+ };
39
+ export const divider = chalk.gray('─'.repeat(50));
40
+ export function sectionHeader(title) {
41
+ console.log();
42
+ console.log(chalk.cyan.bold(` ${title}`));
43
+ console.log(chalk.gray(` ${'─'.repeat(title.length + 4)}`));
44
+ }
@@ -0,0 +1,14 @@
1
+ import { LoggerHelpers } from "../services/logger.js";
2
+ export { handleCommandError };
3
+ /**
4
+ * Handles command errors with consistent formatting and exits the process.
5
+ * Replaces the repeated try-catch pattern across all command modules.
6
+ *
7
+ * @param error - The caught error (unknown type)
8
+ * @param context - Description of what was happening (e.g., "Error bumping version")
9
+ */
10
+ function handleCommandError(error, context) {
11
+ const message = error instanceof Error ? error.message : String(error);
12
+ LoggerHelpers.error(`${context}: ${message}`);
13
+ process.exit(1);
14
+ }
@@ -13,7 +13,7 @@ function createDirectories(modulePath, directories) {
13
13
  function writeFile(filePath, content) {
14
14
  fs.writeFileSync(filePath, content);
15
15
  }
16
- function getClassName(moduleName, type) {
16
+ function getClassName(moduleName) {
17
17
  // Split module name by underscores and capitalize each part
18
18
  const defineItems = moduleName.split("_").filter(item => item.length > 0);
19
19
  let className = "";
@@ -1,6 +1,7 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import { LoggerHelpers } from "../services/logger.js";
4
+ import { PROJECT_PATHS } from "../../constants.js";
4
5
  export { parseVersion, incrementVersion, getCurrentVersion, getCurrentIosBuildNumber };
5
6
  /**
6
7
  * Parses a version string in format "X.Y.Z+B"
@@ -85,7 +86,7 @@ export function getNextBuildNumber() {
85
86
  */
86
87
  function getCurrentIosBuildNumber() {
87
88
  try {
88
- const projectPbxProjPath = path.join(process.cwd(), "ios/Runner.xcodeproj/project.pbxproj");
89
+ const projectPbxProjPath = path.join(process.cwd(), PROJECT_PATHS.IOS_PROJECT_PBXPROJ);
89
90
  if (!fs.existsSync(projectPbxProjPath)) {
90
91
  LoggerHelpers.warning("iOS project.pbxproj not found. Defaulting to build number 1.");
91
92
  return 1;
@@ -1,7 +1,8 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import { LoggerHelpers } from "./logger.js";
4
- export { createBackup, restoreBackup, cleanupBackups, getBackupPath };
4
+ import { BACKUP_CONFIG } from "../../constants.js";
5
+ export { createBackup, createBackupWithCleanup, restoreBackup, cleanupBackups, getBackupPath };
5
6
  /**
6
7
  * Creates a backup of a file with timestamp
7
8
  * Returns the backup path if successful, null otherwise
@@ -30,6 +31,16 @@ function createBackup(filePath) {
30
31
  return null;
31
32
  }
32
33
  }
34
+ /**
35
+ * Creates a backup and enforces retention cleanup in one call.
36
+ */
37
+ function createBackupWithCleanup(filePath, retentionCount = BACKUP_CONFIG.RETENTION_COUNT) {
38
+ const backupPath = createBackup(filePath);
39
+ if (backupPath) {
40
+ cleanupBackups(path.dirname(filePath), retentionCount);
41
+ }
42
+ return backupPath;
43
+ }
33
44
  /**
34
45
  * Restores a file from backup
35
46
  */
@@ -1,36 +1,4 @@
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
- }
1
+ export { getFlutterCommand };
34
2
  /**
35
3
  * Gets the appropriate Flutter command based on FVM usage
36
4
  *
@@ -44,7 +12,6 @@ function validateBuildEnvironment(options) {
44
12
  */
45
13
  function getFlutterCommand(baseCommand, useFvm) {
46
14
  if (useFvm) {
47
- // Replace "flutter" with "fvm flutter"
48
15
  return baseCommand.replace(/^flutter\s/, "fvm flutter ");
49
16
  }
50
17
  return baseCommand;