optikit 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,14 @@ We follow **Semantic Versioning (SemVer)**:
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
+
### 🌟 [1.4.0] - Interactive Repo Picker & Module Cleanup
|
|
16
|
+
|
|
17
|
+
- 📂 Interactive arrow-key directory picker for `gen repo` — browse, open, go back, or create folders
|
|
18
|
+
- 🔄 Removed repo from `generate module` — lighter module scaffolding
|
|
19
|
+
- 🏗️ Repo is now standalone (own import, no `part of`) and can live anywhere
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
15
23
|
### 🌟 [1.3.0] - MCP Plugin, Short Aliases & Combo Flags
|
|
16
24
|
|
|
17
25
|
- 🤖 **Claude Code Integration**:
|
|
@@ -29,7 +29,7 @@ export const projectCommands = [
|
|
|
29
29
|
builder: (yargs) => {
|
|
30
30
|
return yargs.positional("moduleName", { describe: "The module name", type: "string" });
|
|
31
31
|
},
|
|
32
|
-
handler: (argv) => { generateRepoModule(argv.moduleName); },
|
|
32
|
+
handler: async (argv) => { await generateRepoModule(argv.moduleName); },
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
command: "add-route <moduleName>",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
3
4
|
import { createDirectories, writeFile, getClassName, } from "../../utils/helpers/file.js";
|
|
4
5
|
import { LoggerHelpers } from "../../utils/services/logger.js";
|
|
5
6
|
import { handleCommandError } from "../../utils/helpers/error.js";
|
|
@@ -31,7 +32,6 @@ function generateModule(moduleName) {
|
|
|
31
32
|
generateScreen(moduleName, path.join(modulePath, "screen"));
|
|
32
33
|
generateImport(moduleName, path.join(modulePath, "import"));
|
|
33
34
|
generateStateFactory(moduleName, path.join(modulePath, "factory"));
|
|
34
|
-
generateRepo(moduleName, path.join(modulePath, "repo"));
|
|
35
35
|
LoggerHelpers.success(`Module ${moduleName} created with full structure.`);
|
|
36
36
|
}
|
|
37
37
|
catch (error) {
|
|
@@ -136,7 +136,6 @@ part '../event/${moduleName}_event.dart';
|
|
|
136
136
|
part '../screen/${moduleName}_screen.dart';
|
|
137
137
|
part '../state/${moduleName}_state.dart';
|
|
138
138
|
part '../factory/${moduleName}_factory.dart';
|
|
139
|
-
part '../repo/${moduleName}_repo.dart';
|
|
140
139
|
`;
|
|
141
140
|
writeFile(importFilePath, template);
|
|
142
141
|
LoggerHelpers.success(`Import file ${moduleName}_import.dart created in ${importPath}`);
|
|
@@ -159,7 +158,7 @@ class ${className}Factory extends BaseFactory {
|
|
|
159
158
|
function generateRepo(moduleName, repoPath) {
|
|
160
159
|
const repoFilePath = path.join(repoPath, `${moduleName}_repo.dart`);
|
|
161
160
|
const className = getClassName(moduleName);
|
|
162
|
-
const template = `
|
|
161
|
+
const template = `import 'package:opticore/opticore.dart';
|
|
163
162
|
|
|
164
163
|
class ${className}Repo extends BaseRepo {
|
|
165
164
|
|
|
@@ -170,17 +169,213 @@ class ${className}Repo extends BaseRepo {
|
|
|
170
169
|
);
|
|
171
170
|
}
|
|
172
171
|
}
|
|
173
|
-
|
|
172
|
+
`;
|
|
174
173
|
writeFile(repoFilePath, template);
|
|
175
174
|
LoggerHelpers.success(`Repo file ${moduleName}_repo.dart created in ${repoPath}`);
|
|
176
175
|
}
|
|
176
|
+
function buildPickerItems(currentPath, rootPath) {
|
|
177
|
+
const items = [];
|
|
178
|
+
// "Select here" always first
|
|
179
|
+
items.push({ label: "Select this directory", icon: "✔", action: "select" });
|
|
180
|
+
// Go back (unless at root)
|
|
181
|
+
const canGoBack = currentPath !== rootPath && currentPath !== ".";
|
|
182
|
+
if (canGoBack) {
|
|
183
|
+
items.push({ label: "..", icon: "↩", action: "back" });
|
|
184
|
+
}
|
|
185
|
+
// Subdirectories
|
|
186
|
+
if (fs.existsSync(currentPath)) {
|
|
187
|
+
const dirs = fs.readdirSync(currentPath, { withFileTypes: true })
|
|
188
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
189
|
+
.map(e => e.name)
|
|
190
|
+
.sort();
|
|
191
|
+
for (const dir of dirs) {
|
|
192
|
+
items.push({ label: `${dir}/`, icon: "📁", action: "open", dirName: dir });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Create new folder always last
|
|
196
|
+
items.push({ label: "Create new folder", icon: "✚", action: "create" });
|
|
197
|
+
return items;
|
|
198
|
+
}
|
|
177
199
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
200
|
+
* Interactive arrow-key directory picker.
|
|
201
|
+
* Navigate with arrow keys, enter to select/open, esc to cancel.
|
|
180
202
|
*/
|
|
181
|
-
function
|
|
203
|
+
function pickDirectory(startPath) {
|
|
204
|
+
return new Promise((resolve, reject) => {
|
|
205
|
+
let currentPath = startPath;
|
|
206
|
+
const rootPath = startPath;
|
|
207
|
+
let cursor = 0;
|
|
208
|
+
let items = buildPickerItems(currentPath, rootPath);
|
|
209
|
+
let totalLines = 0;
|
|
210
|
+
let creatingFolder = false;
|
|
211
|
+
let folderInput = "";
|
|
212
|
+
const stdin = process.stdin;
|
|
213
|
+
const wasRaw = stdin.isRaw ?? false;
|
|
214
|
+
stdin.setRawMode(true);
|
|
215
|
+
stdin.resume();
|
|
216
|
+
stdin.setEncoding("utf8");
|
|
217
|
+
function render() {
|
|
218
|
+
// Clear previous output
|
|
219
|
+
if (totalLines > 0) {
|
|
220
|
+
process.stdout.write(`\x1B[${totalLines}A`);
|
|
221
|
+
process.stdout.write("\x1B[0J");
|
|
222
|
+
}
|
|
223
|
+
const lines = [];
|
|
224
|
+
// Header — current path with breadcrumb
|
|
225
|
+
lines.push("");
|
|
226
|
+
lines.push(chalk.cyan.bold(` 📂 ${currentPath}/`));
|
|
227
|
+
lines.push(chalk.gray(" ─────────────────────────────────────"));
|
|
228
|
+
// Menu items
|
|
229
|
+
for (let i = 0; i < items.length; i++) {
|
|
230
|
+
const item = items[i];
|
|
231
|
+
const isActive = i === cursor;
|
|
232
|
+
if (isActive) {
|
|
233
|
+
const pointer = chalk.cyan.bold("❯");
|
|
234
|
+
const icon = item.action === "select" ? chalk.green(item.icon) :
|
|
235
|
+
item.action === "back" ? chalk.yellow(item.icon) :
|
|
236
|
+
item.action === "create" ? chalk.magenta(item.icon) :
|
|
237
|
+
chalk.blue(item.icon);
|
|
238
|
+
lines.push(` ${pointer} ${icon} ${chalk.white.bold(item.label)}`);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
const icon = chalk.gray(item.icon);
|
|
242
|
+
lines.push(` ${icon} ${chalk.gray(item.label)}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
lines.push(chalk.gray(" ─────────────────────────────────────"));
|
|
246
|
+
if (creatingFolder) {
|
|
247
|
+
lines.push(chalk.magenta(` ✚ New folder name: `) + chalk.white.bold(folderInput) + chalk.gray("▌"));
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
lines.push(chalk.gray(" ↑↓ navigate ⏎ select/open ← back esc cancel"));
|
|
251
|
+
}
|
|
252
|
+
const output = lines.join("\n") + "\n";
|
|
253
|
+
process.stdout.write(output);
|
|
254
|
+
totalLines = lines.length;
|
|
255
|
+
}
|
|
256
|
+
function cleanup() {
|
|
257
|
+
stdin.setRawMode(wasRaw);
|
|
258
|
+
stdin.removeListener("data", onKeypress);
|
|
259
|
+
stdin.pause();
|
|
260
|
+
}
|
|
261
|
+
function refreshItems() {
|
|
262
|
+
items = buildPickerItems(currentPath, rootPath);
|
|
263
|
+
cursor = 0;
|
|
264
|
+
}
|
|
265
|
+
function onKeypress(key) {
|
|
266
|
+
// Handle folder creation mode
|
|
267
|
+
if (creatingFolder) {
|
|
268
|
+
if (key === "\r") {
|
|
269
|
+
// Enter — create the folder
|
|
270
|
+
const trimmed = folderInput.trim();
|
|
271
|
+
if (trimmed.length > 0) {
|
|
272
|
+
const newPath = path.join(currentPath, trimmed);
|
|
273
|
+
fs.mkdirSync(newPath, { recursive: true });
|
|
274
|
+
currentPath = newPath;
|
|
275
|
+
refreshItems();
|
|
276
|
+
}
|
|
277
|
+
creatingFolder = false;
|
|
278
|
+
folderInput = "";
|
|
279
|
+
render();
|
|
280
|
+
}
|
|
281
|
+
else if (key === "\x1B") {
|
|
282
|
+
// Esc — cancel folder creation
|
|
283
|
+
creatingFolder = false;
|
|
284
|
+
folderInput = "";
|
|
285
|
+
render();
|
|
286
|
+
}
|
|
287
|
+
else if (key === "\x7F" || key === "\b") {
|
|
288
|
+
// Backspace
|
|
289
|
+
folderInput = folderInput.slice(0, -1);
|
|
290
|
+
render();
|
|
291
|
+
}
|
|
292
|
+
else if (key.length === 1 && key.charCodeAt(0) >= 32) {
|
|
293
|
+
// Regular character
|
|
294
|
+
folderInput += key;
|
|
295
|
+
render();
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const item = items[cursor];
|
|
300
|
+
// Escape — cancel (single \x1B only, not arrow key sequences)
|
|
301
|
+
if (key === "\x1B" && key.length === 1) {
|
|
302
|
+
cleanup();
|
|
303
|
+
reject(new Error("Directory selection cancelled."));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// Arrow keys
|
|
307
|
+
if (key === "\x1B[A") {
|
|
308
|
+
// Up
|
|
309
|
+
cursor = cursor > 0 ? cursor - 1 : items.length - 1;
|
|
310
|
+
render();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (key === "\x1B[B") {
|
|
314
|
+
// Down
|
|
315
|
+
cursor = cursor < items.length - 1 ? cursor + 1 : 0;
|
|
316
|
+
render();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (key === "\x1B[D") {
|
|
320
|
+
// Left arrow — go back
|
|
321
|
+
if (currentPath !== rootPath && currentPath !== ".") {
|
|
322
|
+
currentPath = path.dirname(currentPath);
|
|
323
|
+
refreshItems();
|
|
324
|
+
render();
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (key === "\x1B[C") {
|
|
329
|
+
// Right arrow — open directory if cursor is on a folder
|
|
330
|
+
if (item.action === "open" && item.dirName) {
|
|
331
|
+
currentPath = path.join(currentPath, item.dirName);
|
|
332
|
+
refreshItems();
|
|
333
|
+
render();
|
|
334
|
+
}
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
// Enter — perform action
|
|
338
|
+
if (key === "\r") {
|
|
339
|
+
if (item.action === "select") {
|
|
340
|
+
cleanup();
|
|
341
|
+
resolve(currentPath);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (item.action === "back") {
|
|
345
|
+
currentPath = path.dirname(currentPath);
|
|
346
|
+
refreshItems();
|
|
347
|
+
render();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (item.action === "open" && item.dirName) {
|
|
351
|
+
currentPath = path.join(currentPath, item.dirName);
|
|
352
|
+
refreshItems();
|
|
353
|
+
render();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (item.action === "create") {
|
|
357
|
+
creatingFolder = true;
|
|
358
|
+
folderInput = "";
|
|
359
|
+
render();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Ctrl+C
|
|
364
|
+
if (key === "\x03") {
|
|
365
|
+
cleanup();
|
|
366
|
+
process.exit(0);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
stdin.on("data", onKeypress);
|
|
370
|
+
render();
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Generates a repository file at a user-selected location.
|
|
375
|
+
* Opens an interactive directory picker starting from lib/.
|
|
376
|
+
*/
|
|
377
|
+
async function generateRepoModule(moduleName) {
|
|
182
378
|
try {
|
|
183
|
-
// Validate module name
|
|
184
379
|
if (!moduleName || moduleName.trim().length === 0) {
|
|
185
380
|
LoggerHelpers.error(ERROR_MESSAGES.MODULE_NAME_EMPTY);
|
|
186
381
|
process.exit(1);
|
|
@@ -189,37 +384,15 @@ function generateRepoModule(moduleName) {
|
|
|
189
384
|
LoggerHelpers.error(ERROR_MESSAGES.MODULE_NAME_INVALID);
|
|
190
385
|
process.exit(1);
|
|
191
386
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
process.exit(1);
|
|
197
|
-
}
|
|
198
|
-
// Create repo directory
|
|
199
|
-
const repoPath = path.join(modulePath, "repo");
|
|
387
|
+
LoggerHelpers.info(`Generating repository for ${moduleName}...`);
|
|
388
|
+
LoggerHelpers.info("Select where to create the repo file:");
|
|
389
|
+
const startDir = fs.existsSync("lib") ? "lib" : ".";
|
|
390
|
+
const repoPath = await pickDirectory(startDir);
|
|
200
391
|
if (!fs.existsSync(repoPath)) {
|
|
201
392
|
fs.mkdirSync(repoPath, { recursive: true });
|
|
202
393
|
}
|
|
203
|
-
LoggerHelpers.info(`Adding repository to module ${moduleName}...`);
|
|
204
394
|
generateRepo(moduleName, repoPath);
|
|
205
|
-
|
|
206
|
-
const importFilePath = path.join(modulePath, "import", `${moduleName}_import.dart`);
|
|
207
|
-
if (fs.existsSync(importFilePath)) {
|
|
208
|
-
const importContent = fs.readFileSync(importFilePath, "utf8");
|
|
209
|
-
const repoPartLine = `part '../repo/${moduleName}_repo.dart';`;
|
|
210
|
-
if (!importContent.includes(repoPartLine)) {
|
|
211
|
-
const updatedContent = importContent.trimEnd() + `\n${repoPartLine}\n`;
|
|
212
|
-
writeFile(importFilePath, updatedContent);
|
|
213
|
-
LoggerHelpers.success(`Added repo part directive to ${moduleName}_import.dart`);
|
|
214
|
-
}
|
|
215
|
-
else {
|
|
216
|
-
LoggerHelpers.info("Repo part directive already exists in import file.");
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
LoggerHelpers.warning(`Import file not found at ${importFilePath}. Add the part directive manually.`);
|
|
221
|
-
}
|
|
222
|
-
LoggerHelpers.success(`Repository added to module ${moduleName}.`);
|
|
395
|
+
LoggerHelpers.success(`Repository created at ${path.join(repoPath, `${moduleName}_repo.dart`)}`);
|
|
223
396
|
}
|
|
224
397
|
catch (error) {
|
|
225
398
|
handleCommandError(error, "Error generating repo");
|
package/package.json
CHANGED
|
@@ -29,7 +29,7 @@ export const projectCommands: CommandModule[] = [
|
|
|
29
29
|
builder: (yargs) => {
|
|
30
30
|
return yargs.positional("moduleName", { describe: "The module name", type: "string" as const });
|
|
31
31
|
},
|
|
32
|
-
handler: (argv) => { generateRepoModule(argv.moduleName as string); },
|
|
32
|
+
handler: async (argv) => { await generateRepoModule(argv.moduleName as string); },
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
command: "add-route <moduleName>",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
3
4
|
import {
|
|
4
5
|
createDirectories,
|
|
5
6
|
writeFile,
|
|
@@ -44,7 +45,6 @@ function generateModule(moduleName: string) {
|
|
|
44
45
|
generateScreen(moduleName, path.join(modulePath, "screen"));
|
|
45
46
|
generateImport(moduleName, path.join(modulePath, "import"));
|
|
46
47
|
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) {
|
|
@@ -170,7 +170,6 @@ part '../event/${moduleName}_event.dart';
|
|
|
170
170
|
part '../screen/${moduleName}_screen.dart';
|
|
171
171
|
part '../state/${moduleName}_state.dart';
|
|
172
172
|
part '../factory/${moduleName}_factory.dart';
|
|
173
|
-
part '../repo/${moduleName}_repo.dart';
|
|
174
173
|
`;
|
|
175
174
|
|
|
176
175
|
writeFile(importFilePath, template);
|
|
@@ -206,7 +205,7 @@ function generateRepo(moduleName: string, repoPath: string) {
|
|
|
206
205
|
const repoFilePath = path.join(repoPath, `${moduleName}_repo.dart`);
|
|
207
206
|
const className = getClassName(moduleName);
|
|
208
207
|
|
|
209
|
-
const template = `
|
|
208
|
+
const template = `import 'package:opticore/opticore.dart';
|
|
210
209
|
|
|
211
210
|
class ${className}Repo extends BaseRepo {
|
|
212
211
|
|
|
@@ -217,7 +216,7 @@ class ${className}Repo extends BaseRepo {
|
|
|
217
216
|
);
|
|
218
217
|
}
|
|
219
218
|
}
|
|
220
|
-
|
|
219
|
+
`;
|
|
221
220
|
|
|
222
221
|
writeFile(repoFilePath, template);
|
|
223
222
|
LoggerHelpers.success(
|
|
@@ -226,12 +225,238 @@ class ${className}Repo extends BaseRepo {
|
|
|
226
225
|
}
|
|
227
226
|
|
|
228
227
|
/**
|
|
229
|
-
*
|
|
230
|
-
|
|
228
|
+
* Builds the list of selectable items for the directory picker.
|
|
229
|
+
*/
|
|
230
|
+
interface PickerItem {
|
|
231
|
+
label: string;
|
|
232
|
+
icon: string;
|
|
233
|
+
action: "select" | "back" | "open" | "create";
|
|
234
|
+
dirName?: string;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function buildPickerItems(currentPath: string, rootPath: string): PickerItem[] {
|
|
238
|
+
const items: PickerItem[] = [];
|
|
239
|
+
|
|
240
|
+
// "Select here" always first
|
|
241
|
+
items.push({ label: "Select this directory", icon: "✔", action: "select" });
|
|
242
|
+
|
|
243
|
+
// Go back (unless at root)
|
|
244
|
+
const canGoBack = currentPath !== rootPath && currentPath !== ".";
|
|
245
|
+
if (canGoBack) {
|
|
246
|
+
items.push({ label: "..", icon: "↩", action: "back" });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Subdirectories
|
|
250
|
+
if (fs.existsSync(currentPath)) {
|
|
251
|
+
const dirs = fs.readdirSync(currentPath, { withFileTypes: true })
|
|
252
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
253
|
+
.map(e => e.name)
|
|
254
|
+
.sort();
|
|
255
|
+
for (const dir of dirs) {
|
|
256
|
+
items.push({ label: `${dir}/`, icon: "📁", action: "open", dirName: dir });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Create new folder always last
|
|
261
|
+
items.push({ label: "Create new folder", icon: "✚", action: "create" });
|
|
262
|
+
|
|
263
|
+
return items;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Interactive arrow-key directory picker.
|
|
268
|
+
* Navigate with arrow keys, enter to select/open, esc to cancel.
|
|
269
|
+
*/
|
|
270
|
+
function pickDirectory(startPath: string): Promise<string> {
|
|
271
|
+
return new Promise((resolve, reject) => {
|
|
272
|
+
let currentPath = startPath;
|
|
273
|
+
const rootPath = startPath;
|
|
274
|
+
let cursor = 0;
|
|
275
|
+
let items = buildPickerItems(currentPath, rootPath);
|
|
276
|
+
let totalLines = 0;
|
|
277
|
+
let creatingFolder = false;
|
|
278
|
+
let folderInput = "";
|
|
279
|
+
|
|
280
|
+
const stdin = process.stdin;
|
|
281
|
+
const wasRaw = stdin.isRaw ?? false;
|
|
282
|
+
stdin.setRawMode(true);
|
|
283
|
+
stdin.resume();
|
|
284
|
+
stdin.setEncoding("utf8");
|
|
285
|
+
|
|
286
|
+
function render() {
|
|
287
|
+
// Clear previous output
|
|
288
|
+
if (totalLines > 0) {
|
|
289
|
+
process.stdout.write(`\x1B[${totalLines}A`);
|
|
290
|
+
process.stdout.write("\x1B[0J");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const lines: string[] = [];
|
|
294
|
+
|
|
295
|
+
// Header — current path with breadcrumb
|
|
296
|
+
lines.push("");
|
|
297
|
+
lines.push(chalk.cyan.bold(` 📂 ${currentPath}/`));
|
|
298
|
+
lines.push(chalk.gray(" ─────────────────────────────────────"));
|
|
299
|
+
|
|
300
|
+
// Menu items
|
|
301
|
+
for (let i = 0; i < items.length; i++) {
|
|
302
|
+
const item = items[i];
|
|
303
|
+
const isActive = i === cursor;
|
|
304
|
+
|
|
305
|
+
if (isActive) {
|
|
306
|
+
const pointer = chalk.cyan.bold("❯");
|
|
307
|
+
const icon = item.action === "select" ? chalk.green(item.icon) :
|
|
308
|
+
item.action === "back" ? chalk.yellow(item.icon) :
|
|
309
|
+
item.action === "create" ? chalk.magenta(item.icon) :
|
|
310
|
+
chalk.blue(item.icon);
|
|
311
|
+
lines.push(` ${pointer} ${icon} ${chalk.white.bold(item.label)}`);
|
|
312
|
+
} else {
|
|
313
|
+
const icon = chalk.gray(item.icon);
|
|
314
|
+
lines.push(` ${icon} ${chalk.gray(item.label)}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
lines.push(chalk.gray(" ─────────────────────────────────────"));
|
|
319
|
+
|
|
320
|
+
if (creatingFolder) {
|
|
321
|
+
lines.push(chalk.magenta(` ✚ New folder name: `) + chalk.white.bold(folderInput) + chalk.gray("▌"));
|
|
322
|
+
} else {
|
|
323
|
+
lines.push(chalk.gray(" ↑↓ navigate ⏎ select/open ← back esc cancel"));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const output = lines.join("\n") + "\n";
|
|
327
|
+
process.stdout.write(output);
|
|
328
|
+
totalLines = lines.length;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function cleanup() {
|
|
332
|
+
stdin.setRawMode(wasRaw);
|
|
333
|
+
stdin.removeListener("data", onKeypress);
|
|
334
|
+
stdin.pause();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function refreshItems() {
|
|
338
|
+
items = buildPickerItems(currentPath, rootPath);
|
|
339
|
+
cursor = 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function onKeypress(key: string) {
|
|
343
|
+
// Handle folder creation mode
|
|
344
|
+
if (creatingFolder) {
|
|
345
|
+
if (key === "\r") {
|
|
346
|
+
// Enter — create the folder
|
|
347
|
+
const trimmed = folderInput.trim();
|
|
348
|
+
if (trimmed.length > 0) {
|
|
349
|
+
const newPath = path.join(currentPath, trimmed);
|
|
350
|
+
fs.mkdirSync(newPath, { recursive: true });
|
|
351
|
+
currentPath = newPath;
|
|
352
|
+
refreshItems();
|
|
353
|
+
}
|
|
354
|
+
creatingFolder = false;
|
|
355
|
+
folderInput = "";
|
|
356
|
+
render();
|
|
357
|
+
} else if (key === "\x1B") {
|
|
358
|
+
// Esc — cancel folder creation
|
|
359
|
+
creatingFolder = false;
|
|
360
|
+
folderInput = "";
|
|
361
|
+
render();
|
|
362
|
+
} else if (key === "\x7F" || key === "\b") {
|
|
363
|
+
// Backspace
|
|
364
|
+
folderInput = folderInput.slice(0, -1);
|
|
365
|
+
render();
|
|
366
|
+
} else if (key.length === 1 && key.charCodeAt(0) >= 32) {
|
|
367
|
+
// Regular character
|
|
368
|
+
folderInput += key;
|
|
369
|
+
render();
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const item = items[cursor];
|
|
375
|
+
|
|
376
|
+
// Escape — cancel (single \x1B only, not arrow key sequences)
|
|
377
|
+
if (key === "\x1B" && key.length === 1) {
|
|
378
|
+
cleanup();
|
|
379
|
+
reject(new Error("Directory selection cancelled."));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Arrow keys
|
|
384
|
+
if (key === "\x1B[A") {
|
|
385
|
+
// Up
|
|
386
|
+
cursor = cursor > 0 ? cursor - 1 : items.length - 1;
|
|
387
|
+
render();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (key === "\x1B[B") {
|
|
391
|
+
// Down
|
|
392
|
+
cursor = cursor < items.length - 1 ? cursor + 1 : 0;
|
|
393
|
+
render();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (key === "\x1B[D") {
|
|
397
|
+
// Left arrow — go back
|
|
398
|
+
if (currentPath !== rootPath && currentPath !== ".") {
|
|
399
|
+
currentPath = path.dirname(currentPath);
|
|
400
|
+
refreshItems();
|
|
401
|
+
render();
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (key === "\x1B[C") {
|
|
406
|
+
// Right arrow — open directory if cursor is on a folder
|
|
407
|
+
if (item.action === "open" && item.dirName) {
|
|
408
|
+
currentPath = path.join(currentPath, item.dirName);
|
|
409
|
+
refreshItems();
|
|
410
|
+
render();
|
|
411
|
+
}
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Enter — perform action
|
|
416
|
+
if (key === "\r") {
|
|
417
|
+
if (item.action === "select") {
|
|
418
|
+
cleanup();
|
|
419
|
+
resolve(currentPath);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (item.action === "back") {
|
|
423
|
+
currentPath = path.dirname(currentPath);
|
|
424
|
+
refreshItems();
|
|
425
|
+
render();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (item.action === "open" && item.dirName) {
|
|
429
|
+
currentPath = path.join(currentPath, item.dirName);
|
|
430
|
+
refreshItems();
|
|
431
|
+
render();
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (item.action === "create") {
|
|
435
|
+
creatingFolder = true;
|
|
436
|
+
folderInput = "";
|
|
437
|
+
render();
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Ctrl+C
|
|
443
|
+
if (key === "\x03") {
|
|
444
|
+
cleanup();
|
|
445
|
+
process.exit(0);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
stdin.on("data", onKeypress);
|
|
450
|
+
render();
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Generates a repository file at a user-selected location.
|
|
456
|
+
* Opens an interactive directory picker starting from lib/.
|
|
231
457
|
*/
|
|
232
|
-
function generateRepoModule(moduleName: string) {
|
|
458
|
+
async function generateRepoModule(moduleName: string) {
|
|
233
459
|
try {
|
|
234
|
-
// Validate module name
|
|
235
460
|
if (!moduleName || moduleName.trim().length === 0) {
|
|
236
461
|
LoggerHelpers.error(ERROR_MESSAGES.MODULE_NAME_EMPTY);
|
|
237
462
|
process.exit(1);
|
|
@@ -242,41 +467,19 @@ function generateRepoModule(moduleName: string) {
|
|
|
242
467
|
process.exit(1);
|
|
243
468
|
}
|
|
244
469
|
|
|
245
|
-
|
|
470
|
+
LoggerHelpers.info(`Generating repository for ${moduleName}...`);
|
|
471
|
+
LoggerHelpers.info("Select where to create the repo file:");
|
|
246
472
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
LoggerHelpers.error(`Module ${moduleName} does not exist at ${modulePath}. Create it first with: optikit generate module ${moduleName}`);
|
|
250
|
-
process.exit(1);
|
|
251
|
-
}
|
|
473
|
+
const startDir = fs.existsSync("lib") ? "lib" : ".";
|
|
474
|
+
const repoPath = await pickDirectory(startDir);
|
|
252
475
|
|
|
253
|
-
// Create repo directory
|
|
254
|
-
const repoPath = path.join(modulePath, "repo");
|
|
255
476
|
if (!fs.existsSync(repoPath)) {
|
|
256
477
|
fs.mkdirSync(repoPath, { recursive: true });
|
|
257
478
|
}
|
|
258
479
|
|
|
259
|
-
LoggerHelpers.info(`Adding repository to module ${moduleName}...`);
|
|
260
480
|
generateRepo(moduleName, repoPath);
|
|
261
481
|
|
|
262
|
-
|
|
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}.`);
|
|
482
|
+
LoggerHelpers.success(`Repository created at ${path.join(repoPath, `${moduleName}_repo.dart`)}`);
|
|
280
483
|
} catch (error) {
|
|
281
484
|
handleCommandError(error, "Error generating repo");
|
|
282
485
|
}
|