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
@@ -0,0 +1,359 @@
1
+ import { z } from "zod";
2
+ import { buildFlutterApk, buildFlutterBundle, buildFlutterIos, buildFlutterIpa } from "../build/releases.js";
3
+ import { runTestflight } from "../build/testflight.js";
4
+ import { cleanProject } from "../clean/flutter.js";
5
+ import { cleanIosProject } from "../clean/ios.js";
6
+ import { bumpVersion, bumpIosBuildOnly, bumpAndroidBuildOnly, bumpBothBuilds, showCurrentVersion } from "../version/bump.js";
7
+ import { updateFlutterVersion } from "../version/update.js";
8
+ import { generateModule, generateRepoModule, addRoute } from "../project/generate.js";
9
+ import { openIos, openAndroid, openIpaOutput, openApkOutput, openBundleOutput } from "../project/open.js";
10
+ import { listDevices, runApp } from "../project/devices.js";
11
+ import { showStatus } from "../project/status.js";
12
+ import { runDoctor } from "../project/doctor.js";
13
+ import { initializeProject } from "../config/init.js";
14
+ import { initOpticoreApp } from "../config/initApp.js";
15
+ import { rollbackFiles, rollbackRestore } from "../config/rollback.js";
16
+ import { createVscodeSettings } from "../project/setup.js";
17
+ import { checkUpgrade } from "../config/upgrade.js";
18
+ async function captureOutput(fn) {
19
+ const lines = [];
20
+ const origLog = console.log;
21
+ const origError = console.error;
22
+ console.log = (...args) => lines.push(args.map(String).join(" "));
23
+ console.error = (...args) => lines.push(args.map(String).join(" "));
24
+ try {
25
+ await fn();
26
+ }
27
+ finally {
28
+ console.log = origLog;
29
+ console.error = origError;
30
+ }
31
+ return lines.join("\n") || "Done.";
32
+ }
33
+ export const tools = [
34
+ // ── Build ──
35
+ {
36
+ name: "build_apk",
37
+ description: "Build a Flutter APK for Android. Use when the user needs an APK for testing, distribution, or Play Store upload. Supports cleaning before build, bumping version, and opening output.",
38
+ schema: z.object({
39
+ disableFvm: z.boolean().optional().describe("Use global Flutter SDK instead of FVM"),
40
+ clean: z.boolean().optional().describe("Clean project before building"),
41
+ bump: z.enum(["major", "minor", "patch"]).optional().describe("Bump version before building"),
42
+ open: z.boolean().optional().describe("Open output directory after build"),
43
+ }),
44
+ handler: async (args) => {
45
+ if (args.bump)
46
+ await bumpVersion(args.bump, true);
47
+ if (args.clean)
48
+ await cleanProject(args.disableFvm ?? false);
49
+ await buildFlutterApk(args.disableFvm ?? false);
50
+ if (args.open)
51
+ await openApkOutput();
52
+ return "APK build completed.";
53
+ },
54
+ },
55
+ {
56
+ name: "build_aab",
57
+ description: "Build a Flutter App Bundle (AAB) for Google Play Store. Use when the user needs an Android release bundle for store submission.",
58
+ schema: z.object({
59
+ disableFvm: z.boolean().optional().describe("Use global Flutter SDK instead of FVM"),
60
+ clean: z.boolean().optional().describe("Clean project before building"),
61
+ bump: z.enum(["major", "minor", "patch"]).optional().describe("Bump version before building"),
62
+ open: z.boolean().optional().describe("Open output directory after build"),
63
+ }),
64
+ handler: async (args) => {
65
+ if (args.bump)
66
+ await bumpVersion(args.bump, true);
67
+ if (args.clean)
68
+ await cleanProject(args.disableFvm ?? false);
69
+ await buildFlutterBundle(args.disableFvm ?? false);
70
+ if (args.open)
71
+ await openBundleOutput();
72
+ return "AAB bundle build completed.";
73
+ },
74
+ },
75
+ {
76
+ name: "build_ios",
77
+ description: "Build the Flutter iOS app in release mode. Use when the user wants to compile for iOS without creating an IPA archive.",
78
+ schema: z.object({
79
+ disableFvm: z.boolean().optional().describe("Use global Flutter SDK instead of FVM"),
80
+ clean: z.boolean().optional().describe("Clean project before building"),
81
+ bump: z.enum(["major", "minor", "patch"]).optional().describe("Bump version before building"),
82
+ }),
83
+ handler: async (args) => {
84
+ if (args.bump)
85
+ await bumpVersion(args.bump, true);
86
+ if (args.clean) {
87
+ await cleanProject(args.disableFvm ?? false);
88
+ await cleanIosProject(false, false);
89
+ }
90
+ await buildFlutterIos(args.disableFvm ?? false);
91
+ return "iOS build completed.";
92
+ },
93
+ },
94
+ {
95
+ name: "build_ipa",
96
+ description: "Create a release IPA for TestFlight or App Store distribution. Use when the user wants to distribute an iOS build or upload to TestFlight.",
97
+ schema: z.object({
98
+ disableFvm: z.boolean().optional().describe("Use global Flutter SDK instead of FVM"),
99
+ clean: z.boolean().optional().describe("Clean project before building"),
100
+ bump: z.enum(["major", "minor", "patch"]).optional().describe("Bump version before building"),
101
+ bumpIos: z.boolean().optional().describe("Bump iOS build number before building"),
102
+ open: z.boolean().optional().describe("Open output directory after build"),
103
+ }),
104
+ handler: async (args) => {
105
+ if (args.bump)
106
+ await bumpVersion(args.bump, true);
107
+ if (args.bumpIos)
108
+ await bumpIosBuildOnly(true);
109
+ if (args.clean) {
110
+ await cleanProject(args.disableFvm ?? false);
111
+ await cleanIosProject(false, false);
112
+ }
113
+ await buildFlutterIpa(args.disableFvm ?? false);
114
+ if (args.open)
115
+ await openIpaOutput();
116
+ return "IPA build completed.";
117
+ },
118
+ },
119
+ {
120
+ name: "testflight",
121
+ description: "Full TestFlight workflow: bump iOS build number then build IPA. Use when preparing a new iOS build for TestFlight beta testing.",
122
+ schema: z.object({
123
+ disableFvm: z.boolean().optional().describe("Use global Flutter SDK instead of FVM"),
124
+ open: z.boolean().optional().describe("Open IPA output directory after build"),
125
+ }),
126
+ handler: async (args) => {
127
+ await runTestflight(args.disableFvm ?? false, args.open ?? false);
128
+ return "TestFlight build completed.";
129
+ },
130
+ },
131
+ // ── Clean ──
132
+ {
133
+ name: "clean",
134
+ description: "Clean the Flutter project. Use when builds fail due to stale caches, after switching branches, or before a fresh release build. Can also clean iOS CocoaPods.",
135
+ schema: z.object({
136
+ disableFvm: z.boolean().optional().describe("Use global Flutter SDK instead of FVM"),
137
+ ios: z.boolean().optional().describe("Also clean iOS project"),
138
+ all: z.boolean().optional().describe("Clean both Flutter and iOS"),
139
+ cleanCache: z.boolean().optional().describe("Clear CocoaPods cache (iOS)"),
140
+ repoUpdate: z.boolean().optional().describe("Update CocoaPods spec repo (iOS)"),
141
+ }),
142
+ handler: async (args) => {
143
+ await cleanProject(args.disableFvm ?? false);
144
+ if (args.ios || args.all) {
145
+ await cleanIosProject(args.cleanCache ?? false, args.repoUpdate ?? false);
146
+ }
147
+ return args.all || args.ios ? "Flutter and iOS cleaned." : "Flutter project cleaned.";
148
+ },
149
+ },
150
+ {
151
+ name: "clean_ios",
152
+ description: "Clean only the iOS project: remove Pods, deintegrate CocoaPods. Use when iOS builds fail but Flutter is fine, or when CocoaPods has cache issues.",
153
+ schema: z.object({
154
+ cleanCache: z.boolean().optional().describe("Clear CocoaPods cache"),
155
+ repoUpdate: z.boolean().optional().describe("Update CocoaPods spec repo"),
156
+ }),
157
+ handler: async (args) => {
158
+ await cleanIosProject(args.cleanCache ?? false, args.repoUpdate ?? false);
159
+ return "iOS project cleaned.";
160
+ },
161
+ },
162
+ // ── Version ──
163
+ {
164
+ name: "bump_version",
165
+ description: "Bump the semantic version (major, minor, or patch). Updates pubspec.yaml and iOS project files. Resets iOS build to 1. Use when preparing a new release.",
166
+ schema: z.object({
167
+ type: z.enum(["major", "minor", "patch"]).describe("Version bump type"),
168
+ }),
169
+ handler: async (args) => {
170
+ await bumpVersion(args.type, true);
171
+ return `Version bumped (${args.type}).`;
172
+ },
173
+ },
174
+ {
175
+ name: "bump_ios_build",
176
+ description: "Increment only the iOS build number without changing the version. Use before uploading a new TestFlight build.",
177
+ schema: z.object({}),
178
+ handler: async () => {
179
+ await bumpIosBuildOnly(true);
180
+ return "iOS build number incremented.";
181
+ },
182
+ },
183
+ {
184
+ name: "bump_android_build",
185
+ description: "Increment only the Android build number without changing the version. Use before uploading to Google Play.",
186
+ schema: z.object({}),
187
+ handler: async () => {
188
+ await bumpAndroidBuildOnly(true);
189
+ return "Android build number incremented.";
190
+ },
191
+ },
192
+ {
193
+ name: "bump_both_builds",
194
+ description: "Increment both Android and iOS build numbers simultaneously without changing the version.",
195
+ schema: z.object({}),
196
+ handler: async () => {
197
+ await bumpBothBuilds(true);
198
+ return "Both build numbers incremented.";
199
+ },
200
+ },
201
+ {
202
+ name: "show_version",
203
+ description: "Display current version information including semver, Android build number, and iOS build number. Use to check project state before bumping or releasing.",
204
+ schema: z.object({}),
205
+ handler: async () => captureOutput(() => showCurrentVersion()),
206
+ },
207
+ {
208
+ name: "set_version",
209
+ description: "Manually set version and/or build numbers. Use when you need specific values rather than incrementing.",
210
+ schema: z.object({
211
+ appVersion: z.string().optional().describe("Version number to set (e.g. 2.0.0)"),
212
+ androidBuild: z.string().optional().describe("Android build number to set"),
213
+ iosBuild: z.string().optional().describe("iOS build number to set"),
214
+ }),
215
+ handler: async (args) => {
216
+ await updateFlutterVersion(args.appVersion ?? "", args.androidBuild ?? "", args.iosBuild ?? "");
217
+ return "Version updated.";
218
+ },
219
+ },
220
+ // ── Generate ──
221
+ {
222
+ name: "generate_module",
223
+ description: "Scaffold a complete BLoC module with bloc, event, state, screen, factory, and import files. Use when creating a new feature module in an Opticore project.",
224
+ schema: z.object({
225
+ moduleName: z.string().describe("Module name in snake_case (e.g. user_profile)"),
226
+ withRoute: z.boolean().optional().describe("Also register a route in app_router.dart"),
227
+ }),
228
+ handler: async (args) => {
229
+ const name = args.moduleName;
230
+ generateModule(name);
231
+ if (args.withRoute)
232
+ addRoute(name);
233
+ return `Module '${name}' generated.${args.withRoute ? " Route registered." : ""}`;
234
+ },
235
+ },
236
+ {
237
+ name: "generate_repo",
238
+ description: "Generate a repository file for an existing module. Use when adding data layer to a feature module.",
239
+ schema: z.object({
240
+ moduleName: z.string().describe("Module name in snake_case"),
241
+ }),
242
+ handler: async (args) => {
243
+ generateRepoModule(args.moduleName);
244
+ return `Repository generated for '${args.moduleName}'.`;
245
+ },
246
+ },
247
+ {
248
+ name: "add_route",
249
+ description: "Add a route entry for a module to app_router.dart. Use when a module exists but its route is not registered.",
250
+ schema: z.object({
251
+ moduleName: z.string().describe("Module name in snake_case"),
252
+ }),
253
+ handler: async (args) => {
254
+ addRoute(args.moduleName);
255
+ return `Route added for '${args.moduleName}'.`;
256
+ },
257
+ },
258
+ // ── Project ──
259
+ {
260
+ name: "open_xcode",
261
+ description: "Open the iOS project in Xcode.",
262
+ schema: z.object({}),
263
+ handler: async () => { await openIos(); return "Xcode opened."; },
264
+ },
265
+ {
266
+ name: "open_android_studio",
267
+ description: "Open the Android project in Android Studio.",
268
+ schema: z.object({}),
269
+ handler: async () => { await openAndroid(); return "Android Studio opened."; },
270
+ },
271
+ {
272
+ name: "list_devices",
273
+ description: "List all connected Flutter devices including emulators and physical devices. Use to see available devices before running the app.",
274
+ schema: z.object({
275
+ disableFvm: z.boolean().optional().describe("Use global Flutter SDK"),
276
+ }),
277
+ handler: async (args) => captureOutput(() => listDevices(!(args.disableFvm ?? false))),
278
+ },
279
+ {
280
+ name: "run_app",
281
+ description: "Run the Flutter app on a connected device. Use when the user wants to test the app.",
282
+ schema: z.object({
283
+ device: z.string().optional().describe("Device ID to run on"),
284
+ release: z.boolean().optional().describe("Run in release mode"),
285
+ flavor: z.string().optional().describe("Build flavor"),
286
+ disableFvm: z.boolean().optional().describe("Use global Flutter SDK"),
287
+ }),
288
+ handler: async (args) => {
289
+ await runApp({
290
+ device: args.device,
291
+ release: args.release ?? false,
292
+ flavor: args.flavor,
293
+ useFvm: !(args.disableFvm ?? false),
294
+ });
295
+ return "App running.";
296
+ },
297
+ },
298
+ {
299
+ name: "project_status",
300
+ description: "Show project version, config, platform info, and backup count. Use for a quick project overview.",
301
+ schema: z.object({}),
302
+ handler: async () => captureOutput(() => showStatus()),
303
+ },
304
+ {
305
+ name: "doctor",
306
+ description: "Check development environment health: Flutter SDK, FVM, CocoaPods, Xcode CLI tools, project structure. Use to diagnose setup issues or before starting work.",
307
+ schema: z.object({}),
308
+ handler: async () => captureOutput(() => runDoctor()),
309
+ },
310
+ // ── Config ──
311
+ {
312
+ name: "init",
313
+ description: "Initialize OptiKit configuration (.optikitrc.json) in the current project. Use when setting up OptiKit for the first time.",
314
+ schema: z.object({}),
315
+ handler: async () => { await initializeProject(); return "OptiKit initialized."; },
316
+ },
317
+ {
318
+ name: "init_app",
319
+ description: "Scaffold Opticore app structure (main.dart, config, router). Use when creating a new Opticore app from scratch.",
320
+ schema: z.object({
321
+ appName: z.string().optional().describe("App name for CoreSetup configuration"),
322
+ }),
323
+ handler: async (args) => {
324
+ await initOpticoreApp(args.appName ?? "MyApp");
325
+ return "Opticore app scaffolded.";
326
+ },
327
+ },
328
+ {
329
+ name: "rollback_list",
330
+ description: "List available OptiKit backups with timestamps. Use before restoring to see what backups exist.",
331
+ schema: z.object({
332
+ before: z.string().optional().describe("Filter backups before date (YYYY-MM-DD)"),
333
+ }),
334
+ handler: async (args) => captureOutput(() => rollbackFiles(args.before)),
335
+ },
336
+ {
337
+ name: "rollback_restore",
338
+ description: "Restore files from a specific OptiKit backup by index. Use to undo a version bump or other file modification.",
339
+ schema: z.object({
340
+ index: z.number().describe("Backup index number to restore"),
341
+ }),
342
+ handler: async (args) => {
343
+ await rollbackRestore(args.index);
344
+ return "Backup restored.";
345
+ },
346
+ },
347
+ {
348
+ name: "setup_vscode",
349
+ description: "Create .vscode/settings.json with Flutter/Dart settings including FVM SDK path and format-on-save.",
350
+ schema: z.object({}),
351
+ handler: async () => { await createVscodeSettings(); return "VS Code settings created."; },
352
+ },
353
+ {
354
+ name: "check_upgrade",
355
+ description: "Check if a newer version of OptiKit CLI is available on npm.",
356
+ schema: z.object({}),
357
+ handler: async () => captureOutput(() => checkUpgrade()),
358
+ },
359
+ ];
@@ -0,0 +1,132 @@
1
+ import { generateModule, generateRepoModule, addRoute } from "./generate.js";
2
+ import { openIos, openAndroid, openIpaOutput, openBundleOutput, openApkOutput } from "./open.js";
3
+ import { createVscodeSettings } from "./setup.js";
4
+ import { listDevices, runApp, runAppInteractive } from "./devices.js";
5
+ import { runDoctor } from "./doctor.js";
6
+ import { showStatus } from "./status.js";
7
+ export const projectCommands = [
8
+ {
9
+ command: "generate module <moduleName>",
10
+ aliases: ["gen module"],
11
+ describe: "Generate a module with structure",
12
+ builder: (yargs) => {
13
+ return yargs
14
+ .positional("moduleName", { describe: "The name of the module to generate", type: "string" })
15
+ .option("with-route", { alias: "r", type: "boolean", default: false, description: "Also register a route in app_router.dart" });
16
+ },
17
+ handler: (argv) => {
18
+ const moduleName = argv.moduleName;
19
+ generateModule(moduleName);
20
+ if (argv.withRoute) {
21
+ addRoute(moduleName);
22
+ }
23
+ },
24
+ },
25
+ {
26
+ command: "generate repo <moduleName>",
27
+ aliases: ["gen repo"],
28
+ describe: "Generate a repository file for an existing module",
29
+ builder: (yargs) => {
30
+ return yargs.positional("moduleName", { describe: "The module name", type: "string" });
31
+ },
32
+ handler: (argv) => { generateRepoModule(argv.moduleName); },
33
+ },
34
+ {
35
+ command: "add-route <moduleName>",
36
+ aliases: ["route"],
37
+ describe: "Add a route entry for a module to app_router.dart",
38
+ builder: (yargs) => {
39
+ return yargs.positional("moduleName", { describe: "The module name to add a route for", type: "string" });
40
+ },
41
+ handler: (argv) => { addRoute(argv.moduleName); },
42
+ },
43
+ {
44
+ command: "open-ios",
45
+ aliases: ["xcode"],
46
+ describe: "Open the iOS project in Xcode",
47
+ handler: async () => { await openIos(); },
48
+ },
49
+ {
50
+ command: "open-android",
51
+ aliases: ["studio"],
52
+ describe: "Open the Android project in Android Studio",
53
+ handler: async () => { await openAndroid(); },
54
+ },
55
+ {
56
+ command: "open-ipa",
57
+ describe: "Open the IPA build output directory",
58
+ handler: async () => { await openIpaOutput(); },
59
+ },
60
+ {
61
+ command: "open-apk",
62
+ describe: "Open the APK build output directory",
63
+ handler: async () => { await openApkOutput(); },
64
+ },
65
+ {
66
+ command: "open-bundle",
67
+ describe: "Open the Android Bundle build output directory",
68
+ handler: async () => { await openBundleOutput(); },
69
+ },
70
+ {
71
+ command: "setup-vscode",
72
+ aliases: ["vscode"],
73
+ describe: "Create a .vscode folder with recommended Flutter settings",
74
+ handler: async () => { await createVscodeSettings(); },
75
+ },
76
+ {
77
+ command: "devices",
78
+ aliases: ["devs"],
79
+ describe: "List all connected devices",
80
+ builder: {
81
+ "disable-fvm": { alias: "f", type: "boolean", default: false, description: "Run without FVM" },
82
+ },
83
+ handler: async (argv) => { await listDevices(!argv.disableFvm); },
84
+ },
85
+ {
86
+ command: "run",
87
+ describe: "Run Flutter app on connected device",
88
+ builder: {
89
+ device: { alias: "d", type: "string", description: "Specific device ID to run on" },
90
+ release: { alias: "r", type: "boolean", default: false, description: "Run in release mode" },
91
+ flavor: { type: "string", description: "Build flavor to use" },
92
+ "disable-fvm": { alias: "f", type: "boolean", default: false, description: "Run without FVM" },
93
+ },
94
+ handler: async (argv) => {
95
+ await runApp({
96
+ device: argv.device,
97
+ release: argv.release,
98
+ flavor: argv.flavor,
99
+ useFvm: !argv.disableFvm,
100
+ });
101
+ },
102
+ },
103
+ {
104
+ command: "run-select",
105
+ aliases: ["rs"],
106
+ describe: "Interactive device selection and run",
107
+ builder: {
108
+ release: { alias: "r", type: "boolean", default: false, description: "Run in release mode" },
109
+ flavor: { type: "string", description: "Build flavor to use" },
110
+ "disable-fvm": { alias: "f", type: "boolean", default: false, description: "Run without FVM" },
111
+ },
112
+ handler: async (argv) => {
113
+ await runAppInteractive({
114
+ release: argv.release,
115
+ flavor: argv.flavor,
116
+ useFvm: !argv.disableFvm,
117
+ });
118
+ },
119
+ },
120
+ {
121
+ command: "status",
122
+ aliases: ["info"],
123
+ describe: "Show project version, config, and platform info",
124
+ handler: async () => { await showStatus(); },
125
+ },
126
+ {
127
+ command: "doctor",
128
+ aliases: ["dr"],
129
+ describe: "Check development environment and project health",
130
+ handler: async () => { await runDoctor(); },
131
+ },
132
+ ];
@@ -1,10 +1,8 @@
1
- import { exec } from "child_process";
2
- import { promisify } from "util";
3
- import { execCommand } from "../../utils/services/exec.js";
1
+ import { execCommand, execCommandSilent } from "../../utils/services/exec.js";
4
2
  import { LoggerHelpers } from "../../utils/services/logger.js";
5
3
  import { validateFlutterProject, validateFlutterSdk } from "../../utils/validators/validation.js";
4
+ import { handleCommandError } from "../../utils/helpers/error.js";
6
5
  import chalk from "chalk";
7
- const execAsync = promisify(exec);
8
6
  export { listDevices, runApp, getDevicesList, runAppInteractive };
9
7
  ;
10
8
  /**
@@ -13,7 +11,7 @@ export { listDevices, runApp, getDevicesList, runAppInteractive };
13
11
  async function getDevicesList(useFvm = false) {
14
12
  const flutterCommand = useFvm ? "fvm flutter" : "flutter";
15
13
  try {
16
- const { stdout } = await execAsync(`${flutterCommand} devices --machine`);
14
+ const stdout = await execCommandSilent(`${flutterCommand} devices --machine`);
17
15
  const devices = JSON.parse(stdout);
18
16
  return devices.map(device => ({
19
17
  id: device.id,
@@ -68,13 +66,7 @@ async function listDevices(useFvm = false) {
68
66
  console.log(chalk.gray("═".repeat(60) + "\n"));
69
67
  }
70
68
  catch (error) {
71
- if (error instanceof Error) {
72
- LoggerHelpers.error(`Error listing devices: ${error.message}`);
73
- }
74
- else {
75
- LoggerHelpers.error(`Error listing devices: ${error}`);
76
- }
77
- process.exit(1);
69
+ handleCommandError(error, "Error listing devices");
78
70
  }
79
71
  }
80
72
  /**
@@ -115,13 +107,7 @@ async function runApp(config) {
115
107
  await execCommand(command);
116
108
  }
117
109
  catch (error) {
118
- if (error instanceof Error) {
119
- LoggerHelpers.error(`Error running app: ${error.message}`);
120
- }
121
- else {
122
- LoggerHelpers.error(`Error running app: ${error}`);
123
- }
124
- process.exit(1);
110
+ handleCommandError(error, "Error running app");
125
111
  }
126
112
  }
127
113
  /**
@@ -160,6 +146,10 @@ async function runAppInteractive(config) {
160
146
  input: process.stdin,
161
147
  output: process.stdout
162
148
  });
149
+ rl.on('error', () => {
150
+ LoggerHelpers.error("Input error. Please try again.");
151
+ process.exit(1);
152
+ });
163
153
  rl.question(chalk.yellow("Device number: "), async (answer) => {
164
154
  rl.close();
165
155
  const deviceIndex = parseInt(answer) - 1;
@@ -177,12 +167,6 @@ async function runAppInteractive(config) {
177
167
  });
178
168
  }
179
169
  catch (error) {
180
- if (error instanceof Error) {
181
- LoggerHelpers.error(`Error: ${error.message}`);
182
- }
183
- else {
184
- LoggerHelpers.error(`Error: ${error}`);
185
- }
186
- process.exit(1);
170
+ handleCommandError(error, "Error selecting device");
187
171
  }
188
172
  }
@@ -0,0 +1,58 @@
1
+ import chalk from "chalk";
2
+ import { execCommandSilent } from "../../utils/services/exec.js";
3
+ import { validateFlutterProject, validateIosProject, validateAndroidProject } from "../../utils/validators/validation.js";
4
+ import { getConfigPath } from "../../utils/services/config.js";
5
+ import { HELP_URLS } from "../../constants.js";
6
+ import { sectionHeader } from "../../styles.js";
7
+ export { runDoctor };
8
+ async function checkCommand(name, command, fix) {
9
+ try {
10
+ const output = await execCommandSilent(command);
11
+ const version = output.trim().split("\n")[0];
12
+ return { name, passed: true, version };
13
+ }
14
+ catch {
15
+ return { name, passed: false, fix };
16
+ }
17
+ }
18
+ async function runDoctor() {
19
+ sectionHeader("OptiKit Doctor");
20
+ const results = [];
21
+ // Tool checks (run in parallel)
22
+ const toolChecks = await Promise.all([
23
+ checkCommand("Flutter SDK", "flutter --version", `Install Flutter: ${HELP_URLS.FLUTTER_INSTALL}`),
24
+ checkCommand("FVM", "fvm --version", `Install FVM: ${HELP_URLS.FVM_INSTALL} (optional)`),
25
+ checkCommand("CocoaPods", "pod --version", "Install: gem install cocoapods"),
26
+ checkCommand("Xcode CLI Tools", "xcode-select -p", "Install: xcode-select --install"),
27
+ ]);
28
+ results.push(...toolChecks);
29
+ // Project checks
30
+ results.push({ name: "Flutter project", passed: validateFlutterProject(true), fix: "Run from Flutter project root" }, { name: "iOS project", passed: validateIosProject(true), fix: "Run: flutter create ." }, { name: "Android project", passed: validateAndroidProject(true), fix: "Run: flutter create ." });
31
+ const configPath = getConfigPath();
32
+ results.push({
33
+ name: "OptiKit config",
34
+ passed: configPath !== null,
35
+ version: configPath ?? undefined,
36
+ fix: "Run: optikit init",
37
+ });
38
+ // Display results
39
+ console.log();
40
+ let passCount = 0;
41
+ for (const result of results) {
42
+ if (result.passed) {
43
+ passCount++;
44
+ const ver = result.version ? chalk.gray(` ${result.version}`) : "";
45
+ console.log(chalk.green(" ✔"), chalk.white(result.name) + ver);
46
+ }
47
+ else {
48
+ console.log(chalk.red(" ✖"), chalk.white(result.name));
49
+ if (result.fix) {
50
+ console.log(chalk.gray(" └─"), chalk.yellow(result.fix));
51
+ }
52
+ }
53
+ }
54
+ const total = results.length;
55
+ const color = passCount === total ? chalk.green : passCount >= total - 2 ? chalk.yellow : chalk.red;
56
+ console.log(chalk.gray("\n ─────────────────────────────────"));
57
+ console.log(color(` ${passCount}/${total} checks passed\n`));
58
+ }