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,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
  }
@@ -0,0 +1,81 @@
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
+
8
+ export { runDoctor };
9
+
10
+ interface CheckResult {
11
+ name: string;
12
+ passed: boolean;
13
+ version?: string;
14
+ fix?: string;
15
+ }
16
+
17
+ async function checkCommand(
18
+ name: string,
19
+ command: string,
20
+ fix: string
21
+ ): Promise<CheckResult> {
22
+ try {
23
+ const output = await execCommandSilent(command);
24
+ const version = output.trim().split("\n")[0];
25
+ return { name, passed: true, version };
26
+ } catch {
27
+ return { name, passed: false, fix };
28
+ }
29
+ }
30
+
31
+ async function runDoctor(): Promise<void> {
32
+ sectionHeader("OptiKit Doctor");
33
+
34
+ const results: CheckResult[] = [];
35
+
36
+ // Tool checks (run in parallel)
37
+ const toolChecks = await Promise.all([
38
+ checkCommand("Flutter SDK", "flutter --version", `Install Flutter: ${HELP_URLS.FLUTTER_INSTALL}`),
39
+ checkCommand("FVM", "fvm --version", `Install FVM: ${HELP_URLS.FVM_INSTALL} (optional)`),
40
+ checkCommand("CocoaPods", "pod --version", "Install: gem install cocoapods"),
41
+ checkCommand("Xcode CLI Tools", "xcode-select -p", "Install: xcode-select --install"),
42
+ ]);
43
+ results.push(...toolChecks);
44
+
45
+ // Project checks
46
+ results.push(
47
+ { name: "Flutter project", passed: validateFlutterProject(true), fix: "Run from Flutter project root" },
48
+ { name: "iOS project", passed: validateIosProject(true), fix: "Run: flutter create ." },
49
+ { name: "Android project", passed: validateAndroidProject(true), fix: "Run: flutter create ." },
50
+ );
51
+
52
+ const configPath = getConfigPath();
53
+ results.push({
54
+ name: "OptiKit config",
55
+ passed: configPath !== null,
56
+ version: configPath ?? undefined,
57
+ fix: "Run: optikit init",
58
+ });
59
+
60
+ // Display results
61
+ console.log();
62
+ let passCount = 0;
63
+
64
+ for (const result of results) {
65
+ if (result.passed) {
66
+ passCount++;
67
+ const ver = result.version ? chalk.gray(` ${result.version}`) : "";
68
+ console.log(chalk.green(" ✔"), chalk.white(result.name) + ver);
69
+ } else {
70
+ console.log(chalk.red(" ✖"), chalk.white(result.name));
71
+ if (result.fix) {
72
+ console.log(chalk.gray(" └─"), chalk.yellow(result.fix));
73
+ }
74
+ }
75
+ }
76
+
77
+ const total = results.length;
78
+ const color = passCount === total ? chalk.green : passCount >= total - 2 ? chalk.yellow : chalk.red;
79
+ console.log(chalk.gray("\n ─────────────────────────────────"));
80
+ console.log(color(` ${passCount}/${total} checks passed\n`));
81
+ }
@@ -6,31 +6,30 @@ import {
6
6
  getClassName,
7
7
  } from "../../utils/helpers/file.js";
8
8
  import { LoggerHelpers } from "../../utils/services/logger.js";
9
+ import { handleCommandError } from "../../utils/helpers/error.js";
10
+ import { ERROR_MESSAGES, MODULE_STRUCTURE } from "../../constants.js";
9
11
 
10
- export { generateModule };
12
+ export { generateModule, generateRepoModule, addRoute };
11
13
 
12
14
  function generateModule(moduleName: string) {
13
- try {
14
- // Validate module name
15
- if (!moduleName || moduleName.trim().length === 0) {
16
- LoggerHelpers.error("Module name cannot be empty.");
17
- process.exit(1);
18
- }
15
+ // Validate module name
16
+ if (!moduleName || moduleName.trim().length === 0) {
17
+ LoggerHelpers.error(ERROR_MESSAGES.MODULE_NAME_EMPTY);
18
+ process.exit(1);
19
+ }
19
20
 
20
- // Validate module name format (only lowercase letters, numbers, and underscores)
21
- const validNamePattern = /^[a-z0-9_]+$/;
22
- if (!validNamePattern.test(moduleName)) {
23
- LoggerHelpers.error(
24
- "Module name must contain only lowercase letters, numbers, and underscores."
25
- );
26
- process.exit(1);
27
- }
21
+ if (!MODULE_STRUCTURE.NAME_PATTERN.test(moduleName)) {
22
+ LoggerHelpers.error(ERROR_MESSAGES.MODULE_NAME_INVALID);
23
+ process.exit(1);
24
+ }
28
25
 
29
- const modulePath = path.join("lib", "module", moduleName);
30
- const directories = ["bloc", "event", "state", "screen", "import", "factory"];
26
+ const modulePath = path.join("lib", "module", moduleName);
27
+ const isNew = !fs.existsSync(modulePath);
31
28
 
32
- // Check if module already exists
33
- if (fs.existsSync(modulePath)) {
29
+ try {
30
+ const directories = [...MODULE_STRUCTURE.DIRECTORIES];
31
+
32
+ if (!isNew) {
34
33
  LoggerHelpers.warning(`Module ${moduleName} already exists at ${modulePath}`);
35
34
  LoggerHelpers.info("Files will be overwritten...");
36
35
  }
@@ -45,29 +44,43 @@ function generateModule(moduleName: string) {
45
44
  generateScreen(moduleName, path.join(modulePath, "screen"));
46
45
  generateImport(moduleName, path.join(modulePath, "import"));
47
46
  generateStateFactory(moduleName, path.join(modulePath, "factory"));
47
+ generateRepo(moduleName, path.join(modulePath, "repo"));
48
48
 
49
49
  LoggerHelpers.success(`Module ${moduleName} created with full structure.`);
50
50
  } catch (error) {
51
- if (error instanceof Error) {
52
- LoggerHelpers.error(`Error generating module: ${error.message}`);
53
- } else {
54
- LoggerHelpers.error(`Error generating module: ${error}`);
51
+ // Clean up only if we created a new module directory
52
+ if (isNew) {
53
+ if (fs.existsSync(modulePath)) {
54
+ try {
55
+ fs.rmSync(modulePath, { recursive: true });
56
+ LoggerHelpers.warning("Cleaned up partial module files.");
57
+ } catch { /* ignore cleanup errors */ }
58
+ }
55
59
  }
56
- process.exit(1);
60
+ handleCommandError(error, "Error generating module");
57
61
  }
58
62
  }
59
63
 
60
64
  function generateBloc(moduleName: string, blocPath: string) {
61
65
  const blocFilePath = path.join(blocPath, `${moduleName}_bloc.dart`);
62
- const className = getClassName(moduleName, "bloc");
66
+ const className = getClassName(moduleName);
63
67
 
64
68
  const template = `part of '../import/${moduleName}_import.dart';
65
69
 
66
70
  class ${className}Bloc extends BaseBloc {
67
71
  ${className}Bloc() : super(
68
- ${className}Factory(),
72
+ ${className}Factory(),
69
73
  initialState: ${className}InitialState(),
70
- ) {}
74
+ ) {
75
+ on<${className}InitialEvent>(_onInitialEvent);
76
+ }
77
+
78
+ Future<void> _onInitialEvent(
79
+ ${className}InitialEvent event,
80
+ Emitter<BaseState> emit,
81
+ ) async {
82
+ // Handle initial event
83
+ }
71
84
 
72
85
  @override
73
86
  void onDispose() {}
@@ -82,7 +95,7 @@ class ${className}Bloc extends BaseBloc {
82
95
 
83
96
  function generateEvent(moduleName: string, eventPath: string) {
84
97
  const eventFilePath = path.join(eventPath, `${moduleName}_event.dart`);
85
- const className = getClassName(moduleName, "event");
98
+ const className = getClassName(moduleName);
86
99
 
87
100
  const template = `part of '../import/${moduleName}_import.dart';
88
101
 
@@ -97,7 +110,7 @@ class ${className}InitialEvent extends BaseEvent {}
97
110
 
98
111
  function generateState(moduleName: string, statePath: string) {
99
112
  const stateFilePath = path.join(statePath, `${moduleName}_state.dart`);
100
- const className = getClassName(moduleName, "state");
113
+ const className = getClassName(moduleName);
101
114
 
102
115
  const template = `part of '../import/${moduleName}_import.dart';
103
116
 
@@ -114,7 +127,7 @@ class ${className}InitialState extends RenderDataState {
114
127
 
115
128
  function generateScreen(moduleName: string, screenPath: string) {
116
129
  const screenFilePath = path.join(screenPath, `${moduleName}_screen.dart`);
117
- const className = getClassName(moduleName, "screen");
130
+ const className = getClassName(moduleName);
118
131
 
119
132
  const template = `part of '../import/${moduleName}_import.dart';
120
133
 
@@ -147,7 +160,7 @@ class _${className}ScreenState extends BaseScreen<${className}Bloc, ${className}
147
160
 
148
161
  function generateImport(moduleName: string, importPath: string) {
149
162
  const importFilePath = path.join(importPath, `${moduleName}_import.dart`);
150
- const className = getClassName(moduleName, "import");
163
+ const className = getClassName(moduleName);
151
164
 
152
165
  const template = `import 'package:flutter/material.dart';
153
166
  import 'package:opticore/opticore.dart';
@@ -157,6 +170,7 @@ part '../event/${moduleName}_event.dart';
157
170
  part '../screen/${moduleName}_screen.dart';
158
171
  part '../state/${moduleName}_state.dart';
159
172
  part '../factory/${moduleName}_factory.dart';
173
+ part '../repo/${moduleName}_repo.dart';
160
174
  `;
161
175
 
162
176
  writeFile(importFilePath, template);
@@ -170,7 +184,7 @@ function generateStateFactory(moduleName: string, factoryPath: string) {
170
184
  factoryPath,
171
185
  `${moduleName}_factory.dart`
172
186
  );
173
- const className = getClassName(moduleName, "state_factory");
187
+ const className = getClassName(moduleName);
174
188
 
175
189
  const template = `part of '../import/${moduleName}_import.dart';
176
190
 
@@ -187,3 +201,168 @@ class ${className}Factory extends BaseFactory {
187
201
  `State Factory file ${moduleName}_factory.dart created in ${factoryPath}`
188
202
  );
189
203
  }
204
+
205
+ function generateRepo(moduleName: string, repoPath: string) {
206
+ const repoFilePath = path.join(repoPath, `${moduleName}_repo.dart`);
207
+ const className = getClassName(moduleName);
208
+
209
+ const template = `part of '../import/${moduleName}_import.dart';
210
+
211
+ class ${className}Repo extends BaseRepo {
212
+
213
+ Future<ApiResponse<Map<String, dynamic>?>> fetchData() async {
214
+ return await networkHelper.request<Map<String, dynamic>>(
215
+ '/${moduleName.replace(/_/g, '-')}',
216
+ (json) => json ?? {},
217
+ );
218
+ }
219
+ }
220
+ `;
221
+
222
+ writeFile(repoFilePath, template);
223
+ LoggerHelpers.success(
224
+ `Repo file ${moduleName}_repo.dart created in ${repoPath}`
225
+ );
226
+ }
227
+
228
+ /**
229
+ * Generates a repository for an existing module.
230
+ * Creates repo directory, generates repo file, and appends part directive to import file.
231
+ */
232
+ function generateRepoModule(moduleName: string) {
233
+ try {
234
+ // Validate module name
235
+ if (!moduleName || moduleName.trim().length === 0) {
236
+ LoggerHelpers.error(ERROR_MESSAGES.MODULE_NAME_EMPTY);
237
+ process.exit(1);
238
+ }
239
+
240
+ if (!MODULE_STRUCTURE.NAME_PATTERN.test(moduleName)) {
241
+ LoggerHelpers.error(ERROR_MESSAGES.MODULE_NAME_INVALID);
242
+ process.exit(1);
243
+ }
244
+
245
+ const modulePath = path.join("lib", "module", moduleName);
246
+
247
+ // Verify module exists
248
+ if (!fs.existsSync(modulePath)) {
249
+ LoggerHelpers.error(`Module ${moduleName} does not exist at ${modulePath}. Create it first with: optikit generate module ${moduleName}`);
250
+ process.exit(1);
251
+ }
252
+
253
+ // Create repo directory
254
+ const repoPath = path.join(modulePath, "repo");
255
+ if (!fs.existsSync(repoPath)) {
256
+ fs.mkdirSync(repoPath, { recursive: true });
257
+ }
258
+
259
+ LoggerHelpers.info(`Adding repository to module ${moduleName}...`);
260
+ generateRepo(moduleName, repoPath);
261
+
262
+ // Append part directive to import file if not already present
263
+ const importFilePath = path.join(modulePath, "import", `${moduleName}_import.dart`);
264
+ if (fs.existsSync(importFilePath)) {
265
+ const importContent = fs.readFileSync(importFilePath, "utf8");
266
+ const repoPartLine = `part '../repo/${moduleName}_repo.dart';`;
267
+
268
+ if (!importContent.includes(repoPartLine)) {
269
+ const updatedContent = importContent.trimEnd() + `\n${repoPartLine}\n`;
270
+ writeFile(importFilePath, updatedContent);
271
+ LoggerHelpers.success(`Added repo part directive to ${moduleName}_import.dart`);
272
+ } else {
273
+ LoggerHelpers.info("Repo part directive already exists in import file.");
274
+ }
275
+ } else {
276
+ LoggerHelpers.warning(`Import file not found at ${importFilePath}. Add the part directive manually.`);
277
+ }
278
+
279
+ LoggerHelpers.success(`Repository added to module ${moduleName}.`);
280
+ } catch (error) {
281
+ handleCommandError(error, "Error generating repo");
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Adds a route entry for a module to the app_router.dart file.
287
+ * Looks for the OPTIKIT_ROUTE_ANCHOR comment and inserts above it.
288
+ */
289
+ function addRoute(moduleName: string) {
290
+ try {
291
+ if (!moduleName || moduleName.trim().length === 0) {
292
+ LoggerHelpers.error(ERROR_MESSAGES.MODULE_NAME_EMPTY);
293
+ process.exit(1);
294
+ }
295
+
296
+ if (!MODULE_STRUCTURE.NAME_PATTERN.test(moduleName)) {
297
+ LoggerHelpers.error(ERROR_MESSAGES.MODULE_NAME_INVALID);
298
+ process.exit(1);
299
+ }
300
+
301
+ const routerPath = path.join("lib", "config", "app_router.dart");
302
+
303
+ if (!fs.existsSync(routerPath)) {
304
+ LoggerHelpers.error("app_router.dart not found at lib/config/app_router.dart");
305
+ LoggerHelpers.info("Run 'optikit init-app' first to scaffold the app structure.");
306
+ process.exit(1);
307
+ }
308
+
309
+ const className = getClassName(moduleName);
310
+ const routeName = moduleName.replace(/_/g, '-');
311
+ const anchor = "// === OPTIKIT_ROUTE_ANCHOR === (do not remove this line)";
312
+
313
+ const routerContent = fs.readFileSync(routerPath, "utf8");
314
+
315
+ // Check if route already exists
316
+ if (routerContent.includes(`'/${routeName}'`)) {
317
+ LoggerHelpers.warning(`Route '/${routeName}' already exists in app_router.dart`);
318
+ return;
319
+ }
320
+
321
+ if (!routerContent.includes(anchor)) {
322
+ LoggerHelpers.error("Route anchor not found in app_router.dart. Cannot insert route.");
323
+ LoggerHelpers.info("Ensure the file contains: " + anchor);
324
+ process.exit(1);
325
+ }
326
+
327
+ // Build the import path for the module
328
+ const importPath = `import '../module/${moduleName}/import/${moduleName}_import.dart';`;
329
+
330
+ // Build route case
331
+ const routeCase = ` case '/${routeName}':
332
+ return MaterialPageRoute(
333
+ builder: (_) => BlocProvider(
334
+ create: (_) => ${className}Bloc(),
335
+ child: ${className}Screen(bloc: ${className}Bloc()),
336
+ ),
337
+ );`;
338
+
339
+ // Insert route above anchor
340
+ const updatedRouter = routerContent.replace(
341
+ anchor,
342
+ `${routeCase}\n ${anchor}`
343
+ );
344
+
345
+ // Add import if not already present
346
+ let finalContent = updatedRouter;
347
+ if (!finalContent.includes(importPath)) {
348
+ // Insert import after the last existing import line
349
+ const lastImportIndex = finalContent.lastIndexOf("import ");
350
+ if (lastImportIndex !== -1) {
351
+ const endOfImportLine = finalContent.indexOf("\n", lastImportIndex);
352
+ finalContent =
353
+ finalContent.slice(0, endOfImportLine + 1) +
354
+ importPath + "\n" +
355
+ finalContent.slice(endOfImportLine + 1);
356
+ }
357
+ }
358
+
359
+ writeFile(routerPath, finalContent);
360
+
361
+ LoggerHelpers.success(`Route '/${routeName}' added for module ${moduleName}`);
362
+ LoggerHelpers.info(` Route: '/${routeName}'`);
363
+ LoggerHelpers.info(` Screen: ${className}Screen`);
364
+ LoggerHelpers.info(` BLoC: ${className}Bloc`);
365
+ } catch (error) {
366
+ handleCommandError(error, "Error adding route");
367
+ }
368
+ }
@@ -1,21 +1,23 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import { LoggerHelpers } from "../../utils/services/logger.js";
4
+ import { validateFlutterProject } from "../../utils/validators/validation.js";
5
+ import { handleCommandError } from "../../utils/helpers/error.js";
6
+ import { VSCODE_SETTINGS_TEMPLATE, PROJECT_PATHS } from "../../constants.js";
4
7
 
5
8
  export {
6
9
  createVscodeSettings,
7
10
  };
8
11
 
9
- /**
10
- * Creates a .vscode directory (if it doesn't exist) and writes a settings.json file
11
- * with recommended Flutter configuration. The Flutter SDK path is set to ".fvm/flutter_sdk".
12
- */
13
- async function createVscodeSettings(){
12
+ async function createVscodeSettings() {
13
+ if (!validateFlutterProject()) {
14
+ process.exit(1);
15
+ }
16
+
14
17
  try {
15
- const vscodeDir = path.join(process.cwd(), ".vscode");
16
- const settingsPath = path.join(vscodeDir, "settings.json");
18
+ const vscodeDir = path.join(process.cwd(), PROJECT_PATHS.VSCODE_DIR);
19
+ const settingsPath = path.join(process.cwd(), PROJECT_PATHS.VSCODE_SETTINGS);
17
20
 
18
- // Create the .vscode folder using Node.js fs (cross-platform)
19
21
  if (!fs.existsSync(vscodeDir)) {
20
22
  fs.mkdirSync(vscodeDir, { recursive: true });
21
23
  LoggerHelpers.success("Created .vscode directory.");
@@ -23,28 +25,9 @@ async function createVscodeSettings(){
23
25
  LoggerHelpers.info(".vscode directory already exists.");
24
26
  }
25
27
 
26
- // Define the settings object
27
- const settings = {
28
- "dart.flutterSdkPath": ".fvm/flutter_sdk",
29
- "editor.formatOnSave": true,
30
- "dart.previewFlutterUiGuides": true,
31
- "files.exclude": {
32
- "**/.git": true,
33
- "**/.DS_Store": true,
34
- "**/node_modules": true,
35
- "**/build": true
36
- }
37
- };
38
-
39
- // Write settings.json using Node.js fs
40
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf8");
28
+ fs.writeFileSync(settingsPath, JSON.stringify(VSCODE_SETTINGS_TEMPLATE, null, 2), "utf8");
41
29
  LoggerHelpers.success("Created .vscode/settings.json with Flutter configuration.");
42
30
  } catch (error) {
43
- if (error instanceof Error) {
44
- LoggerHelpers.error(`Error while creating VSCode settings: ${error.message}`);
45
- } else {
46
- LoggerHelpers.error(`Error while creating VSCode settings: ${error}`);
47
- }
48
- process.exit(1);
31
+ handleCommandError(error, "Error creating VSCode settings");
49
32
  }
50
- }
33
+ }
@@ -0,0 +1,72 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import chalk from "chalk";
4
+ import { getCurrentVersion, formatVersion, getCurrentIosBuildNumber } from "../../utils/helpers/version.js";
5
+ import { getConfigPath } from "../../utils/services/config.js";
6
+ import { validateFlutterProject } from "../../utils/validators/validation.js";
7
+ import { PROJECT_PATHS, BACKUP_CONFIG } from "../../constants.js";
8
+
9
+ export { showStatus };
10
+
11
+ async function showStatus(): Promise<void> {
12
+ console.log(chalk.bold("\nOptiKit Status"));
13
+ console.log(chalk.gray("─".repeat(45)));
14
+
15
+ // Version info
16
+ if (validateFlutterProject(true)) {
17
+ try {
18
+ const current = getCurrentVersion();
19
+ const iosBuild = getCurrentIosBuildNumber();
20
+ console.log(chalk.cyan("\n Version:"), chalk.white.bold(formatVersion(current)));
21
+ console.log(chalk.gray(" Android Build:"), chalk.white(current.buildNumber));
22
+ console.log(chalk.gray(" iOS Build:"), chalk.white(iosBuild));
23
+ } catch {
24
+ console.log(chalk.yellow("\n Version:"), chalk.gray("Unable to read"));
25
+ }
26
+ } else {
27
+ console.log(chalk.yellow("\n Version:"), chalk.gray("Not a Flutter project"));
28
+ }
29
+
30
+ // FVM status
31
+ const fvmPath = path.join(process.cwd(), PROJECT_PATHS.FVM_FLUTTER_SDK);
32
+ const useFvm = fs.existsSync(fvmPath);
33
+ console.log(chalk.cyan(" Flutter:"), chalk.white(useFvm ? "FVM" : "Global SDK"));
34
+
35
+ // Backup count
36
+ const backupCount = countBackups(process.cwd());
37
+ console.log(chalk.cyan(" Backups:"), chalk.white(backupCount > 0 ? `${backupCount} file(s)` : "None"));
38
+
39
+ // Config
40
+ const configPath = getConfigPath();
41
+ console.log(chalk.cyan(" Config:"), chalk.white(configPath ?? "Not initialized (run: optikit init)"));
42
+
43
+ // Project structure
44
+ const hasIos = fs.existsSync(path.join(process.cwd(), PROJECT_PATHS.IOS_DIR));
45
+ const hasAndroid = fs.existsSync(path.join(process.cwd(), PROJECT_PATHS.ANDROID_DIR));
46
+ const platforms = [hasIos ? "iOS" : null, hasAndroid ? "Android" : null].filter(Boolean).join(", ");
47
+ console.log(chalk.cyan(" Platforms:"), chalk.white(platforms || "None detected"));
48
+
49
+ console.log(chalk.gray("\n" + "─".repeat(45) + "\n"));
50
+ }
51
+
52
+ function countBackups(dir: string): number {
53
+ let count = 0;
54
+
55
+ function search(currentDir: string): void {
56
+ try {
57
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
58
+ for (const entry of entries) {
59
+ if (entry.isDirectory()) {
60
+ if (entry.name === BACKUP_CONFIG.DIR_NAME) {
61
+ count += fs.readdirSync(path.join(currentDir, entry.name)).length;
62
+ } else if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
63
+ search(path.join(currentDir, entry.name));
64
+ }
65
+ }
66
+ }
67
+ } catch { /* skip inaccessible dirs */ }
68
+ }
69
+
70
+ search(dir);
71
+ return count;
72
+ }