@nevermorelove/ompp 0.3.2 → 0.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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/ompp.js +126 -30
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -32,6 +32,10 @@ ompp list print mode names
32
32
  ompp pentest --dry-run show the omp command line instead of running it
33
33
  ```
34
34
 
35
+ In the picker, `r` renames the highlighted mode and `d` deletes it (after a
36
+ confirmation). The picker reopens after either, so a mispress costs nothing:
37
+ cancel the rename or keep the mode, and you are back in the list.
38
+
35
39
  Before launch it prints one line to stderr, `[ompp] mode: pentest`, so you always know where you are.
36
40
 
37
41
  Modes are read from two places, and same-named modes from the first win:
package/ompp.js CHANGED
@@ -226,6 +226,7 @@ function usage() {
226
226
 
227
227
  Usage:
228
228
  ompp pick a mode interactively, then launch omp
229
+ (r: rename, d: delete on the highlighted mode)
229
230
  ompp <mode> [args...] launch omp in a mode; everything after <mode>
230
231
  is passed to omp unchanged and wins over the mode
231
232
  ompp create <name> create a new mode with placeholder files
@@ -343,44 +344,135 @@ function createMode(name) {
343
344
  return 0;
344
345
  }
345
346
 
347
+ function renameMode(mode, newName) {
348
+ if (!newName || !isValidModeName(newName)) {
349
+ console.error(`[ompp] "${newName}" is not a valid mode name.`);
350
+ return false;
351
+ }
352
+ if (RESERVED_NAMES.includes(newName)) {
353
+ console.error(`[ompp] "${newName}" is reserved for ompp commands.`);
354
+ return false;
355
+ }
356
+ const from = modeDirOf(mode.name, mode.source);
357
+ const to = modeDirOf(newName, mode.source);
358
+ if (fs.existsSync(to)) {
359
+ console.error(`[ompp] mode "${newName}" already exists.`);
360
+ return false;
361
+ }
362
+ fs.renameSync(from, to);
363
+ console.error(`[ompp] renamed "${mode.name}" to "${newName}"`);
364
+ return true;
365
+ }
366
+
367
+ function deleteMode(mode) {
368
+ const dir = modeDirOf(mode.name, mode.source);
369
+ fs.rmSync(dir, { recursive: true, force: true });
370
+ console.error(`[ompp] deleted mode "${mode.name}"`);
371
+ return true;
372
+ }
373
+
346
374
  const CREATE_OPTION = "__create__";
347
375
 
348
376
  function isValidModeName(name) {
349
377
  return /^[a-z0-9][a-z0-9-]*$/.test(name);
350
378
  }
351
379
 
352
- async function pickMode(modeNames) {
353
- const { select, text, isCancel, cancel } = require("@clack/prompts");
354
- const options = [
355
- ...modeNames.map((name) => ({ value: name, label: name })),
356
- { value: CREATE_OPTION, label: "Create a new mode +" },
357
- ];
358
- const chosen = await select({
359
- message: "Pick a mode",
360
- options,
361
- });
362
- if (isCancel(chosen)) {
363
- cancel("Cancelled");
364
- process.exit(130);
365
- }
366
- if (chosen === CREATE_OPTION) {
367
- const name = await text({
368
- message: "New mode name",
369
- validate: (v) => {
370
- if (!v || !isValidModeName(v)) {
371
- return "Lowercase letters, digits, and dashes only";
372
- }
373
- if (RESERVED_NAMES.includes(v)) return `"${v}" is reserved`;
374
- return undefined;
375
- },
380
+ // The interactive picker. Receives mode objects ({name, source}), returns
381
+ // the picked mode object, or {create: <name>} for the create flow.
382
+ // r renames and d deletes the highlighted mode; both re-render the picker.
383
+ async function pickMode(modes) {
384
+ const { select, text, confirm, isCancel, cancel } = require("@clack/prompts");
385
+ for (;;) {
386
+ const options = [
387
+ ...modes.map((m) => ({
388
+ value: m,
389
+ label: m.name,
390
+ hint: "r: rename, d: delete",
391
+ })),
392
+ { value: CREATE_OPTION, label: "Create a new mode +" },
393
+ ];
394
+ let action = null; // "rename" | "delete" | null
395
+ // Registered before the prompt so it sees the keypress first and can
396
+ // rewrite r/d into "return" for the prompt's own keypress listener.
397
+ const onKeypress = (ch, key) => {
398
+ if (!key || !key.name) return;
399
+ if (key.name === "r" && !key.ctrl && !key.meta) {
400
+ action = "rename";
401
+ key.name = "return";
402
+ } else if (key.name === "d" && !key.ctrl && !key.meta) {
403
+ action = "delete";
404
+ key.name = "return";
405
+ }
406
+ };
407
+ process.stdin.on("keypress", onKeypress);
408
+ const chosen = await select({
409
+ message: "Pick a mode",
410
+ options,
376
411
  });
377
- if (isCancel(name)) {
412
+ process.stdin.removeListener("keypress", onKeypress);
413
+
414
+ if (isCancel(chosen)) {
378
415
  cancel("Cancelled");
379
416
  process.exit(130);
380
417
  }
381
- return { create: name };
418
+
419
+ // r/d on the highlighted row: run the action, then reopen the picker.
420
+ if (action && chosen !== CREATE_OPTION) {
421
+ if (action === "rename") {
422
+ const newName = await text({
423
+ message: `Rename "${chosen.name}" to`,
424
+ validate: (v) => {
425
+ if (!v || !isValidModeName(v)) {
426
+ return "Lowercase letters, digits, and dashes only";
427
+ }
428
+ if (RESERVED_NAMES.includes(v)) return `"${v}" is reserved`;
429
+ if (modes.some((m) => m.name === v)) return `"${v}" already exists`;
430
+ return undefined;
431
+ },
432
+ });
433
+ if (!isCancel(newName) && renameMode(chosen, newName)) {
434
+ chosen.name = newName;
435
+ } else {
436
+ cancel("Rename cancelled");
437
+ }
438
+ } else {
439
+ const sure = await confirm({
440
+ message: `Delete "${chosen.name}"? This removes its folder.`,
441
+ active: "Delete",
442
+ inactive: "Keep",
443
+ });
444
+ if (!isCancel(sure) && sure) {
445
+ deleteMode(chosen);
446
+ // Splice in place: main() keeps a reference to this array.
447
+ modes.splice(
448
+ modes.findIndex((m) => m.name === chosen.name),
449
+ 1,
450
+ );
451
+ } else {
452
+ cancel("Delete cancelled");
453
+ }
454
+ }
455
+ continue; // reopen the picker with the updated list
456
+ }
457
+ if (chosen === CREATE_OPTION) {
458
+ const name = await text({
459
+ message: "New mode name",
460
+ validate: (v) => {
461
+ if (!v || !isValidModeName(v)) {
462
+ return "Lowercase letters, digits, and dashes only";
463
+ }
464
+ if (RESERVED_NAMES.includes(v)) return `"${v}" is reserved`;
465
+ return undefined;
466
+ },
467
+ });
468
+ if (isCancel(name)) {
469
+ cancel("Cancelled");
470
+ process.exit(130);
471
+ }
472
+ return { create: name };
473
+ }
474
+ return chosen;
382
475
  }
383
- return chosen;
384
476
  }
385
477
 
386
478
  // Non-TTY stdin: clack needs an interactive terminal, so numbered input.
@@ -521,16 +613,20 @@ async function main() {
521
613
 
522
614
  if (!mode) {
523
615
  try {
524
- const picked = process.stdin.isTTY
525
- ? await pickMode(modeNames)
616
+ let picked = process.stdin.isTTY
617
+ ? await pickMode(modes)
526
618
  : await pickModePiped(modeNames);
619
+ if (typeof picked === "string") {
620
+ // The piped picker returns a name; normalize to the mode object.
621
+ picked = modes.find((m) => m.name === picked);
622
+ }
527
623
  if (picked && typeof picked === "object" && picked.create) {
528
624
  // Scaffold and stop: the mode is all placeholders, launching it
529
625
  // now would run an unconfigured mode. The user fills it in and
530
626
  // launches when ready.
531
627
  return createMode(picked.create);
532
628
  } else {
533
- mode = modes.find((m) => m.name === picked);
629
+ mode = picked;
534
630
  }
535
631
  } catch (err) {
536
632
  console.error(`[ompp] ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nevermorelove/ompp",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Mode presets for the omp coding agent: one folder per mode, zero config code.",
5
5
  "repository": {
6
6
  "type": "git",