optikit 1.2.4 → 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 +79 -46
  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 -241
  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 +96 -82
  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 -362
  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 +124 -85
  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 -388
  104. package/VERSION_MANAGEMENT.md +0 -438
@@ -0,0 +1,69 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { homedir } from "os";
4
+ import { execSync } from "child_process";
5
+ import { LoggerHelpers } from "../../utils/services/logger.js";
6
+ import { MCP_CONFIG } from "../../constants.js";
7
+
8
+ function getSettingsPath(): string {
9
+ return path.join(homedir(), MCP_CONFIG.CLAUDE_SETTINGS_FILE);
10
+ }
11
+
12
+ function resolveOptikitBinary(): string {
13
+ try {
14
+ const cmd = process.platform === "win32" ? "where optikit" : "which optikit";
15
+ return execSync(cmd, { encoding: "utf8" }).trim().split("\n")[0];
16
+ } catch {
17
+ return "optikit";
18
+ }
19
+ }
20
+
21
+ function readSettings(filePath: string): Record<string, unknown> {
22
+ try {
23
+ if (fs.existsSync(filePath)) {
24
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
25
+ }
26
+ } catch {
27
+ LoggerHelpers.warning("Could not parse existing settings, creating fresh config.");
28
+ }
29
+ return {};
30
+ }
31
+
32
+ function writeSettings(filePath: string, settings: Record<string, unknown>): void {
33
+ fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + "\n", "utf8");
34
+ }
35
+
36
+ export function setupClaude(uninstall: boolean): void {
37
+ const settingsPath = getSettingsPath();
38
+
39
+ if (uninstall) {
40
+ const settings = readSettings(settingsPath);
41
+ const mcpServers = (settings.mcpServers ?? {}) as Record<string, unknown>;
42
+ if (mcpServers.optikit) {
43
+ delete mcpServers.optikit;
44
+ settings.mcpServers = mcpServers;
45
+ writeSettings(settingsPath, settings);
46
+ LoggerHelpers.success("OptiKit MCP server removed from Claude Code.");
47
+ } else {
48
+ LoggerHelpers.info("OptiKit MCP server was not registered.");
49
+ }
50
+ return;
51
+ }
52
+
53
+ const binaryPath = resolveOptikitBinary();
54
+ const settings = readSettings(settingsPath);
55
+ const mcpServers = (settings.mcpServers ?? {}) as Record<string, unknown>;
56
+
57
+ mcpServers.optikit = {
58
+ command: binaryPath,
59
+ args: ["mcp"],
60
+ };
61
+
62
+ settings.mcpServers = mcpServers;
63
+ writeSettings(settingsPath, settings);
64
+
65
+ LoggerHelpers.success("OptiKit MCP server registered with Claude Code.");
66
+ LoggerHelpers.dim(`Config: ${settingsPath}`);
67
+ LoggerHelpers.dim(`Binary: ${binaryPath}`);
68
+ LoggerHelpers.info("Restart Claude Code to activate OptiKit tools.");
69
+ }
@@ -0,0 +1,365 @@
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
+
19
+ export interface ToolDef {
20
+ name: string;
21
+ description: string;
22
+ schema: z.ZodObject<z.ZodRawShape>;
23
+ handler: (args: Record<string, unknown>) => Promise<string>;
24
+ }
25
+
26
+ async function captureOutput(fn: () => Promise<void> | void): Promise<string> {
27
+ const lines: string[] = [];
28
+ const origLog = console.log;
29
+ const origError = console.error;
30
+ console.log = (...args: unknown[]) => lines.push(args.map(String).join(" "));
31
+ console.error = (...args: unknown[]) => lines.push(args.map(String).join(" "));
32
+ try {
33
+ await fn();
34
+ } finally {
35
+ console.log = origLog;
36
+ console.error = origError;
37
+ }
38
+ return lines.join("\n") || "Done.";
39
+ }
40
+
41
+ export const tools: ToolDef[] = [
42
+ // ── Build ──
43
+ {
44
+ name: "build_apk",
45
+ 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.",
46
+ schema: z.object({
47
+ disableFvm: z.boolean().optional().describe("Use global Flutter SDK instead of FVM"),
48
+ clean: z.boolean().optional().describe("Clean project before building"),
49
+ bump: z.enum(["major", "minor", "patch"]).optional().describe("Bump version before building"),
50
+ open: z.boolean().optional().describe("Open output directory after build"),
51
+ }),
52
+ handler: async (args) => {
53
+ if (args.bump) await bumpVersion(args.bump as "major" | "minor" | "patch", true);
54
+ if (args.clean) await cleanProject(args.disableFvm as boolean ?? false);
55
+ await buildFlutterApk(args.disableFvm as boolean ?? false);
56
+ if (args.open) await openApkOutput();
57
+ return "APK build completed.";
58
+ },
59
+ },
60
+ {
61
+ name: "build_aab",
62
+ description: "Build a Flutter App Bundle (AAB) for Google Play Store. Use when the user needs an Android release bundle for store submission.",
63
+ schema: z.object({
64
+ disableFvm: z.boolean().optional().describe("Use global Flutter SDK instead of FVM"),
65
+ clean: z.boolean().optional().describe("Clean project before building"),
66
+ bump: z.enum(["major", "minor", "patch"]).optional().describe("Bump version before building"),
67
+ open: z.boolean().optional().describe("Open output directory after build"),
68
+ }),
69
+ handler: async (args) => {
70
+ if (args.bump) await bumpVersion(args.bump as "major" | "minor" | "patch", true);
71
+ if (args.clean) await cleanProject(args.disableFvm as boolean ?? false);
72
+ await buildFlutterBundle(args.disableFvm as boolean ?? false);
73
+ if (args.open) await openBundleOutput();
74
+ return "AAB bundle build completed.";
75
+ },
76
+ },
77
+ {
78
+ name: "build_ios",
79
+ description: "Build the Flutter iOS app in release mode. Use when the user wants to compile for iOS without creating an IPA archive.",
80
+ schema: z.object({
81
+ disableFvm: z.boolean().optional().describe("Use global Flutter SDK instead of FVM"),
82
+ clean: z.boolean().optional().describe("Clean project before building"),
83
+ bump: z.enum(["major", "minor", "patch"]).optional().describe("Bump version before building"),
84
+ }),
85
+ handler: async (args) => {
86
+ if (args.bump) await bumpVersion(args.bump as "major" | "minor" | "patch", true);
87
+ if (args.clean) {
88
+ await cleanProject(args.disableFvm as boolean ?? false);
89
+ await cleanIosProject(false, false);
90
+ }
91
+ await buildFlutterIos(args.disableFvm as boolean ?? false);
92
+ return "iOS build completed.";
93
+ },
94
+ },
95
+ {
96
+ name: "build_ipa",
97
+ 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.",
98
+ schema: z.object({
99
+ disableFvm: z.boolean().optional().describe("Use global Flutter SDK instead of FVM"),
100
+ clean: z.boolean().optional().describe("Clean project before building"),
101
+ bump: z.enum(["major", "minor", "patch"]).optional().describe("Bump version before building"),
102
+ bumpIos: z.boolean().optional().describe("Bump iOS build number before building"),
103
+ open: z.boolean().optional().describe("Open output directory after build"),
104
+ }),
105
+ handler: async (args) => {
106
+ if (args.bump) await bumpVersion(args.bump as "major" | "minor" | "patch", true);
107
+ if (args.bumpIos) await bumpIosBuildOnly(true);
108
+ if (args.clean) {
109
+ await cleanProject(args.disableFvm as boolean ?? false);
110
+ await cleanIosProject(false, false);
111
+ }
112
+ await buildFlutterIpa(args.disableFvm as boolean ?? false);
113
+ if (args.open) await openIpaOutput();
114
+ return "IPA build completed.";
115
+ },
116
+ },
117
+ {
118
+ name: "testflight",
119
+ description: "Full TestFlight workflow: bump iOS build number then build IPA. Use when preparing a new iOS build for TestFlight beta testing.",
120
+ schema: z.object({
121
+ disableFvm: z.boolean().optional().describe("Use global Flutter SDK instead of FVM"),
122
+ open: z.boolean().optional().describe("Open IPA output directory after build"),
123
+ }),
124
+ handler: async (args) => {
125
+ await runTestflight(args.disableFvm as boolean ?? false, args.open as boolean ?? false);
126
+ return "TestFlight build completed.";
127
+ },
128
+ },
129
+
130
+ // ── Clean ──
131
+ {
132
+ name: "clean",
133
+ 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.",
134
+ schema: z.object({
135
+ disableFvm: z.boolean().optional().describe("Use global Flutter SDK instead of FVM"),
136
+ ios: z.boolean().optional().describe("Also clean iOS project"),
137
+ all: z.boolean().optional().describe("Clean both Flutter and iOS"),
138
+ cleanCache: z.boolean().optional().describe("Clear CocoaPods cache (iOS)"),
139
+ repoUpdate: z.boolean().optional().describe("Update CocoaPods spec repo (iOS)"),
140
+ }),
141
+ handler: async (args) => {
142
+ await cleanProject(args.disableFvm as boolean ?? false);
143
+ if (args.ios || args.all) {
144
+ await cleanIosProject(args.cleanCache as boolean ?? false, args.repoUpdate as boolean ?? false);
145
+ }
146
+ return args.all || args.ios ? "Flutter and iOS cleaned." : "Flutter project cleaned.";
147
+ },
148
+ },
149
+ {
150
+ name: "clean_ios",
151
+ 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.",
152
+ schema: z.object({
153
+ cleanCache: z.boolean().optional().describe("Clear CocoaPods cache"),
154
+ repoUpdate: z.boolean().optional().describe("Update CocoaPods spec repo"),
155
+ }),
156
+ handler: async (args) => {
157
+ await cleanIosProject(args.cleanCache as boolean ?? false, args.repoUpdate as boolean ?? false);
158
+ return "iOS project cleaned.";
159
+ },
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 as "major" | "minor" | "patch", 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(
217
+ args.appVersion as string ?? "",
218
+ args.androidBuild as string ?? "",
219
+ args.iosBuild as string ?? "",
220
+ );
221
+ return "Version updated.";
222
+ },
223
+ },
224
+
225
+ // ── Generate ──
226
+ {
227
+ name: "generate_module",
228
+ 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.",
229
+ schema: z.object({
230
+ moduleName: z.string().describe("Module name in snake_case (e.g. user_profile)"),
231
+ withRoute: z.boolean().optional().describe("Also register a route in app_router.dart"),
232
+ }),
233
+ handler: async (args) => {
234
+ const name = args.moduleName as string;
235
+ generateModule(name);
236
+ if (args.withRoute) addRoute(name);
237
+ return `Module '${name}' generated.${args.withRoute ? " Route registered." : ""}`;
238
+ },
239
+ },
240
+ {
241
+ name: "generate_repo",
242
+ description: "Generate a repository file for an existing module. Use when adding data layer to a feature module.",
243
+ schema: z.object({
244
+ moduleName: z.string().describe("Module name in snake_case"),
245
+ }),
246
+ handler: async (args) => {
247
+ generateRepoModule(args.moduleName as string);
248
+ return `Repository generated for '${args.moduleName}'.`;
249
+ },
250
+ },
251
+ {
252
+ name: "add_route",
253
+ description: "Add a route entry for a module to app_router.dart. Use when a module exists but its route is not registered.",
254
+ schema: z.object({
255
+ moduleName: z.string().describe("Module name in snake_case"),
256
+ }),
257
+ handler: async (args) => {
258
+ addRoute(args.moduleName as string);
259
+ return `Route added for '${args.moduleName}'.`;
260
+ },
261
+ },
262
+
263
+ // ── Project ──
264
+ {
265
+ name: "open_xcode",
266
+ description: "Open the iOS project in Xcode.",
267
+ schema: z.object({}),
268
+ handler: async () => { await openIos(); return "Xcode opened."; },
269
+ },
270
+ {
271
+ name: "open_android_studio",
272
+ description: "Open the Android project in Android Studio.",
273
+ schema: z.object({}),
274
+ handler: async () => { await openAndroid(); return "Android Studio opened."; },
275
+ },
276
+ {
277
+ name: "list_devices",
278
+ description: "List all connected Flutter devices including emulators and physical devices. Use to see available devices before running the app.",
279
+ schema: z.object({
280
+ disableFvm: z.boolean().optional().describe("Use global Flutter SDK"),
281
+ }),
282
+ handler: async (args) => captureOutput(() => listDevices(!(args.disableFvm as boolean ?? false))),
283
+ },
284
+ {
285
+ name: "run_app",
286
+ description: "Run the Flutter app on a connected device. Use when the user wants to test the app.",
287
+ schema: z.object({
288
+ device: z.string().optional().describe("Device ID to run on"),
289
+ release: z.boolean().optional().describe("Run in release mode"),
290
+ flavor: z.string().optional().describe("Build flavor"),
291
+ disableFvm: z.boolean().optional().describe("Use global Flutter SDK"),
292
+ }),
293
+ handler: async (args) => {
294
+ await runApp({
295
+ device: args.device as string | undefined,
296
+ release: args.release as boolean ?? false,
297
+ flavor: args.flavor as string | undefined,
298
+ useFvm: !(args.disableFvm as boolean ?? false),
299
+ });
300
+ return "App running.";
301
+ },
302
+ },
303
+ {
304
+ name: "project_status",
305
+ description: "Show project version, config, platform info, and backup count. Use for a quick project overview.",
306
+ schema: z.object({}),
307
+ handler: async () => captureOutput(() => showStatus()),
308
+ },
309
+ {
310
+ name: "doctor",
311
+ description: "Check development environment health: Flutter SDK, FVM, CocoaPods, Xcode CLI tools, project structure. Use to diagnose setup issues or before starting work.",
312
+ schema: z.object({}),
313
+ handler: async () => captureOutput(() => runDoctor()),
314
+ },
315
+
316
+ // ── Config ──
317
+ {
318
+ name: "init",
319
+ description: "Initialize OptiKit configuration (.optikitrc.json) in the current project. Use when setting up OptiKit for the first time.",
320
+ schema: z.object({}),
321
+ handler: async () => { await initializeProject(); return "OptiKit initialized."; },
322
+ },
323
+ {
324
+ name: "init_app",
325
+ description: "Scaffold Opticore app structure (main.dart, config, router). Use when creating a new Opticore app from scratch.",
326
+ schema: z.object({
327
+ appName: z.string().optional().describe("App name for CoreSetup configuration"),
328
+ }),
329
+ handler: async (args) => {
330
+ await initOpticoreApp(args.appName as string ?? "MyApp");
331
+ return "Opticore app scaffolded.";
332
+ },
333
+ },
334
+ {
335
+ name: "rollback_list",
336
+ description: "List available OptiKit backups with timestamps. Use before restoring to see what backups exist.",
337
+ schema: z.object({
338
+ before: z.string().optional().describe("Filter backups before date (YYYY-MM-DD)"),
339
+ }),
340
+ handler: async (args) => captureOutput(() => rollbackFiles(args.before as string | undefined)),
341
+ },
342
+ {
343
+ name: "rollback_restore",
344
+ description: "Restore files from a specific OptiKit backup by index. Use to undo a version bump or other file modification.",
345
+ schema: z.object({
346
+ index: z.number().describe("Backup index number to restore"),
347
+ }),
348
+ handler: async (args) => {
349
+ await rollbackRestore(args.index as number);
350
+ return "Backup restored.";
351
+ },
352
+ },
353
+ {
354
+ name: "setup_vscode",
355
+ description: "Create .vscode/settings.json with Flutter/Dart settings including FVM SDK path and format-on-save.",
356
+ schema: z.object({}),
357
+ handler: async () => { await createVscodeSettings(); return "VS Code settings created."; },
358
+ },
359
+ {
360
+ name: "check_upgrade",
361
+ description: "Check if a newer version of OptiKit CLI is available on npm.",
362
+ schema: z.object({}),
363
+ handler: async () => captureOutput(() => checkUpgrade()),
364
+ },
365
+ ];
@@ -0,0 +1,132 @@
1
+ import type { CommandModule } from "yargs";
2
+ import { generateModule, generateRepoModule, addRoute } from "./generate.js";
3
+ import { openIos, openAndroid, openIpaOutput, openBundleOutput, openApkOutput } from "./open.js";
4
+ import { createVscodeSettings } from "./setup.js";
5
+ import { listDevices, runApp, runAppInteractive } from "./devices.js";
6
+ import { runDoctor } from "./doctor.js";
7
+ import { showStatus } from "./status.js";
8
+
9
+ export const projectCommands: CommandModule[] = [
10
+ {
11
+ command: "generate module <moduleName>",
12
+ aliases: ["gen module"],
13
+ describe: "Generate a module with structure",
14
+ builder: (yargs) => {
15
+ return yargs
16
+ .positional("moduleName", { describe: "The name of the module to generate", type: "string" as const })
17
+ .option("with-route", { alias: "r", type: "boolean", default: false, description: "Also register a route in app_router.dart" });
18
+ },
19
+ handler: (argv) => {
20
+ const moduleName = argv.moduleName as string;
21
+ generateModule(moduleName);
22
+ if (argv.withRoute) { addRoute(moduleName); }
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" as const });
31
+ },
32
+ handler: (argv) => { generateRepoModule(argv.moduleName as string); },
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" as const });
40
+ },
41
+ handler: (argv) => { addRoute(argv.moduleName as string); },
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 as string | undefined,
97
+ release: argv.release as boolean,
98
+ flavor: argv.flavor as string | undefined,
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 as boolean,
115
+ flavor: argv.flavor as string | undefined,
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,12 +1,9 @@
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
6
 
8
- const execAsync = promisify(exec);
9
-
10
7
  export { listDevices, runApp, getDevicesList, runAppInteractive };
11
8
 
12
9
  /**
@@ -26,7 +23,7 @@ async function getDevicesList(useFvm: boolean = false): Promise<DeviceInfo[]> {
26
23
  const flutterCommand = useFvm ? "fvm flutter" : "flutter";
27
24
 
28
25
  try {
29
- const { stdout } = await execAsync(`${flutterCommand} devices --machine`);
26
+ const stdout = await execCommandSilent(`${flutterCommand} devices --machine`);
30
27
  const devices = JSON.parse(stdout) as Array<{
31
28
  id: string;
32
29
  name: string;
@@ -95,12 +92,7 @@ async function listDevices(useFvm: boolean = false): Promise<void> {
95
92
  console.log(chalk.gray("═".repeat(60) + "\n"));
96
93
 
97
94
  } catch (error) {
98
- if (error instanceof Error) {
99
- LoggerHelpers.error(`Error listing devices: ${error.message}`);
100
- } else {
101
- LoggerHelpers.error(`Error listing devices: ${error}`);
102
- }
103
- process.exit(1);
95
+ handleCommandError(error, "Error listing devices");
104
96
  }
105
97
  }
106
98
 
@@ -159,12 +151,7 @@ async function runApp(config: RunConfig): Promise<void> {
159
151
  await execCommand(command);
160
152
 
161
153
  } catch (error) {
162
- if (error instanceof Error) {
163
- LoggerHelpers.error(`Error running app: ${error.message}`);
164
- } else {
165
- LoggerHelpers.error(`Error running app: ${error}`);
166
- }
167
- process.exit(1);
154
+ handleCommandError(error, "Error running app");
168
155
  }
169
156
  }
170
157
 
@@ -214,6 +201,11 @@ async function runAppInteractive(config: Omit<RunConfig, 'device'>): Promise<voi
214
201
  output: process.stdout
215
202
  });
216
203
 
204
+ rl.on('error', () => {
205
+ LoggerHelpers.error("Input error. Please try again.");
206
+ process.exit(1);
207
+ });
208
+
217
209
  rl.question(chalk.yellow("Device number: "), async (answer: string) => {
218
210
  rl.close();
219
211
 
@@ -236,11 +228,6 @@ async function runAppInteractive(config: Omit<RunConfig, 'device'>): Promise<voi
236
228
  });
237
229
 
238
230
  } catch (error) {
239
- if (error instanceof Error) {
240
- LoggerHelpers.error(`Error: ${error.message}`);
241
- } else {
242
- LoggerHelpers.error(`Error: ${error}`);
243
- }
244
- process.exit(1);
231
+ handleCommandError(error, "Error selecting device");
245
232
  }
246
233
  }