devvami 1.5.0 → 1.5.1
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/README.md +65 -0
- package/oclif.manifest.json +249 -249
- package/package.json +1 -1
- package/src/commands/sync-config-ai/index.js +124 -10
- package/src/formatters/ai-config.js +100 -12
- package/src/services/ai-config-store.js +43 -12
- package/src/services/ai-env-deployer.js +216 -10
- package/src/services/ai-env-scanner.js +752 -11
- package/src/types.js +35 -3
- package/src/utils/tui/form.js +195 -17
- package/src/utils/tui/tab-tui.js +353 -64
package/src/utils/tui/tab-tui.js
CHANGED
|
@@ -12,8 +12,10 @@ import {
|
|
|
12
12
|
handleFormKeypress,
|
|
13
13
|
getMCPFormFields,
|
|
14
14
|
getCommandFormFields,
|
|
15
|
+
getRuleFormFields,
|
|
15
16
|
getSkillFormFields,
|
|
16
17
|
getAgentFormFields,
|
|
18
|
+
validateMCPForm,
|
|
17
19
|
} from './form.js'
|
|
18
20
|
|
|
19
21
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -76,12 +78,16 @@ let _keypressListener = null
|
|
|
76
78
|
|
|
77
79
|
/**
|
|
78
80
|
* @typedef {Object} CatTabState
|
|
79
|
-
* @property {import('../../types.js').CategoryEntry[]} entries -
|
|
80
|
-
* @property {
|
|
81
|
-
* @property {
|
|
81
|
+
* @property {import('../../types.js').CategoryEntry[]} entries - Managed entries for this category type
|
|
82
|
+
* @property {import('../../types.js').NativeEntry[]} nativeEntries - Native (unmanaged) entries for this category type
|
|
83
|
+
* @property {number} selectedIndex - Highlighted row in the active section
|
|
84
|
+
* @property {'native'|'managed'} section - Which section is focused
|
|
85
|
+
* @property {'list'|'form'|'confirm-delete'|'drift'} mode - Current sub-mode
|
|
82
86
|
* @property {import('./form.js').FormState|null} formState - Active form state (null when mode is 'list')
|
|
83
87
|
* @property {string|null} confirmDeleteId - Entry id pending deletion confirmation
|
|
84
88
|
* @property {string} chezmoidTip - Footer tip (empty if chezmoi configured)
|
|
89
|
+
* @property {string|null} revealedEntryId - Entry id whose env vars are currently revealed
|
|
90
|
+
* @property {import('../../types.js').DriftInfo[]} driftInfos - All drift infos for this category
|
|
85
91
|
*/
|
|
86
92
|
|
|
87
93
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -233,27 +239,84 @@ export function handleEnvironmentsKeypress(state, key) {
|
|
|
233
239
|
}
|
|
234
240
|
|
|
235
241
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
236
|
-
// Categories tab content builder
|
|
242
|
+
// Categories tab content builder — dual Native/Managed sections
|
|
237
243
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
238
244
|
|
|
239
245
|
/**
|
|
240
|
-
* Build the content lines for
|
|
246
|
+
* Build the content lines for a category tab with Native and Managed sections.
|
|
247
|
+
* @param {CatTabState} tabState
|
|
248
|
+
* @param {number} viewportHeight
|
|
249
|
+
* @param {import('../../formatters/ai-config.js').formatCategoriesTable} formatManaged
|
|
250
|
+
* @param {import('../../formatters/ai-config.js').formatNativeEntriesTable} formatNative
|
|
251
|
+
* @param {number} termCols
|
|
252
|
+
* @returns {string[]}
|
|
253
|
+
*/
|
|
254
|
+
export function buildCategoriesTab(tabState, viewportHeight, formatManaged, formatNative, termCols = 120) {
|
|
255
|
+
const {entries, nativeEntries = [], selectedIndex, section = 'managed', mode} = tabState
|
|
256
|
+
const confirmDeleteName = tabState._confirmDeleteName ?? null
|
|
257
|
+
const driftedIds = new Set((tabState.driftInfos ?? []).map((d) => d.entryId))
|
|
258
|
+
|
|
259
|
+
const lines = []
|
|
260
|
+
|
|
261
|
+
// ── Native section ──
|
|
262
|
+
if (nativeEntries.length > 0) {
|
|
263
|
+
lines.push(chalk.bold.cyan(' ── Native (read-only) ──────────────────────────────'))
|
|
264
|
+
|
|
265
|
+
const nativeLines = formatNative(nativeEntries, termCols)
|
|
266
|
+
const HEADER = 2
|
|
267
|
+
for (let i = 0; i < nativeLines.length; i++) {
|
|
268
|
+
const dataIndex = i - HEADER
|
|
269
|
+
if (section === 'native' && dataIndex >= 0 && dataIndex === selectedIndex) {
|
|
270
|
+
lines.push(`${ANSI_INVERSE_ON}${nativeLines[i]}${ANSI_INVERSE_OFF}`)
|
|
271
|
+
} else {
|
|
272
|
+
lines.push(chalk.dim(nativeLines[i]))
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
lines.push('')
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── Managed section ──
|
|
279
|
+
lines.push(chalk.bold.white(' ── Managed ────────────────────────────────────────'))
|
|
280
|
+
|
|
281
|
+
if (entries.length === 0) {
|
|
282
|
+
lines.push(chalk.dim(' No managed entries yet.'))
|
|
283
|
+
lines.push(chalk.dim(' Press ' + chalk.bold('n') + ' to create your first entry.'))
|
|
284
|
+
} else {
|
|
285
|
+
// Annotate entries with drift flag for formatter
|
|
286
|
+
const annotated = entries.map((e) => ({...e, drifted: driftedIds.has(e.id)}))
|
|
287
|
+
const tableLines = formatManaged(annotated, termCols)
|
|
288
|
+
const HEADER_LINES = 2
|
|
289
|
+
for (let i = 0; i < tableLines.length; i++) {
|
|
290
|
+
const dataIndex = i - HEADER_LINES
|
|
291
|
+
if (section === 'managed' && dataIndex >= 0 && dataIndex === selectedIndex) {
|
|
292
|
+
lines.push(`${ANSI_INVERSE_ON}${tableLines[i]}${ANSI_INVERSE_OFF}`)
|
|
293
|
+
} else {
|
|
294
|
+
lines.push(tableLines[i])
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Confirmation prompt
|
|
300
|
+
if (mode === 'confirm-delete' && confirmDeleteName) {
|
|
301
|
+
lines.push('')
|
|
302
|
+
lines.push(chalk.red(` Delete "${confirmDeleteName}"? This cannot be undone. `) + chalk.bold('[y/N]'))
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return lines.slice(0, viewportHeight)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Build content lines for the legacy single-section categories tab.
|
|
310
|
+
* Kept for backward compatibility in tests.
|
|
241
311
|
* @param {import('../../types.js').CategoryEntry[]} entries
|
|
242
312
|
* @param {number} selectedIndex
|
|
243
313
|
* @param {number} viewportHeight
|
|
244
314
|
* @param {import('../../formatters/ai-config.js').formatCategoriesTable} formatFn
|
|
245
315
|
* @param {number} termCols
|
|
246
|
-
* @param {string|null} [confirmDeleteName]
|
|
316
|
+
* @param {string|null} [confirmDeleteName]
|
|
247
317
|
* @returns {string[]}
|
|
248
318
|
*/
|
|
249
|
-
export function
|
|
250
|
-
entries,
|
|
251
|
-
selectedIndex,
|
|
252
|
-
viewportHeight,
|
|
253
|
-
formatFn,
|
|
254
|
-
termCols = 120,
|
|
255
|
-
confirmDeleteName = null,
|
|
256
|
-
) {
|
|
319
|
+
export function buildCategoriesTabLegacy(entries, selectedIndex, viewportHeight, formatFn, termCols = 120, confirmDeleteName = null) {
|
|
257
320
|
if (entries.length === 0) {
|
|
258
321
|
const lines = [
|
|
259
322
|
'',
|
|
@@ -277,7 +340,6 @@ export function buildCategoriesTab(
|
|
|
277
340
|
}
|
|
278
341
|
}
|
|
279
342
|
|
|
280
|
-
// Confirmation prompt overlay
|
|
281
343
|
if (confirmDeleteName !== null) {
|
|
282
344
|
resultLines.push('')
|
|
283
345
|
resultLines.push(chalk.red(` Delete "${confirmDeleteName}"? This cannot be undone. `) + chalk.bold('[y/N]'))
|
|
@@ -286,6 +348,61 @@ export function buildCategoriesTab(
|
|
|
286
348
|
return resultLines.slice(0, viewportHeight)
|
|
287
349
|
}
|
|
288
350
|
|
|
351
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
352
|
+
// Drift resolution screen
|
|
353
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Build the drift resolution screen for a drifted managed entry.
|
|
357
|
+
* @param {CatTabState} tabState
|
|
358
|
+
* @param {number} viewportHeight
|
|
359
|
+
* @param {number} termCols
|
|
360
|
+
* @returns {string[]}
|
|
361
|
+
*/
|
|
362
|
+
export function buildDriftScreen(tabState, viewportHeight, termCols) {
|
|
363
|
+
const entryId = tabState._driftEntryId
|
|
364
|
+
const drift = (tabState.driftInfos ?? []).find((d) => d.entryId === entryId)
|
|
365
|
+
const entry = (tabState.entries ?? []).find((e) => e.id === entryId)
|
|
366
|
+
|
|
367
|
+
if (!drift || !entry) {
|
|
368
|
+
return [chalk.dim(' No drift info available.')]
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const lines = []
|
|
372
|
+
lines.push(chalk.bold.yellow(` ⚠ Drift detected: ${entry.name}`))
|
|
373
|
+
lines.push(chalk.dim('─'.repeat(Math.min(termCols, 70))))
|
|
374
|
+
lines.push('')
|
|
375
|
+
lines.push(chalk.bold(' Expected (managed):'))
|
|
376
|
+
const expected = JSON.stringify(drift.expected, null, 2)
|
|
377
|
+
for (const l of expected.split('\n').slice(0, 8)) {
|
|
378
|
+
lines.push(chalk.green(` ${l}`))
|
|
379
|
+
}
|
|
380
|
+
lines.push('')
|
|
381
|
+
lines.push(chalk.bold(' Actual (on disk):'))
|
|
382
|
+
const actual = JSON.stringify(drift.actual, null, 2)
|
|
383
|
+
for (const l of actual.split('\n').slice(0, 8)) {
|
|
384
|
+
lines.push(chalk.red(` ${l}`))
|
|
385
|
+
}
|
|
386
|
+
lines.push('')
|
|
387
|
+
lines.push(chalk.dim(' Press r to re-deploy (overwrite file) a to accept changes (update store) Esc to go back'))
|
|
388
|
+
|
|
389
|
+
return lines.slice(0, viewportHeight)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Minimal fallback formatter for native entries (no chalk dependency at module load).
|
|
394
|
+
* Used only when formatNative is not provided to startTabTUI.
|
|
395
|
+
* @param {import('../../types.js').NativeEntry[]} entries
|
|
396
|
+
* @returns {string[]}
|
|
397
|
+
*/
|
|
398
|
+
function formatNativeEntriesTableFallback(entries) {
|
|
399
|
+
const lines = [' Name Environment Level Config', ' ' + '─'.repeat(60)]
|
|
400
|
+
for (const e of entries) {
|
|
401
|
+
lines.push(` ${e.name.padEnd(25)} ${e.environmentId.padEnd(13)} ${e.level.padEnd(8)} ${e.sourcePath}`)
|
|
402
|
+
}
|
|
403
|
+
return lines
|
|
404
|
+
}
|
|
405
|
+
|
|
289
406
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
290
407
|
// Categories tab keypress reducer (T037)
|
|
291
408
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -297,41 +414,70 @@ export function buildCategoriesTab(
|
|
|
297
414
|
* @returns {CatTabState | { exit: true }}
|
|
298
415
|
*/
|
|
299
416
|
export function handleCategoriesKeypress(state, key) {
|
|
300
|
-
const {selectedIndex, entries,
|
|
301
|
-
const
|
|
417
|
+
const {selectedIndex, entries, nativeEntries = [], section = 'managed', mode} = state
|
|
418
|
+
const activeList = section === 'native' ? nativeEntries : entries
|
|
419
|
+
const maxIndex = Math.max(0, activeList.length - 1)
|
|
302
420
|
|
|
303
421
|
// Confirm-delete mode
|
|
304
422
|
if (mode === 'confirm-delete') {
|
|
305
423
|
if (key.name === 'y') {
|
|
306
|
-
return {
|
|
307
|
-
...state,
|
|
308
|
-
mode: 'list',
|
|
309
|
-
confirmDeleteId: key.name === 'y' ? confirmDeleteId : null,
|
|
310
|
-
_deleteConfirmed: true,
|
|
311
|
-
}
|
|
424
|
+
return {...state, mode: 'list', _deleteConfirmed: true}
|
|
312
425
|
}
|
|
313
426
|
// Any other key cancels
|
|
314
427
|
return {...state, mode: 'list', confirmDeleteId: null}
|
|
315
428
|
}
|
|
316
429
|
|
|
317
|
-
//
|
|
430
|
+
// Drift resolution mode
|
|
431
|
+
if (mode === 'drift') {
|
|
432
|
+
if (key.name === 'escape') return {...state, mode: 'list'}
|
|
433
|
+
if (key.name === 'r') return {...state, mode: 'list', _redeploy: state._driftEntryId, _driftEntryId: null}
|
|
434
|
+
if (key.name === 'a') return {...state, mode: 'list', _acceptDrift: state._driftEntryId, _driftEntryId: null}
|
|
435
|
+
return state
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Navigation — clears env var reveal on any movement
|
|
439
|
+
// Cross-section: up from top of managed goes to last native; down from bottom of native goes to first managed
|
|
318
440
|
if (key.name === 'up' || key.name === 'k') {
|
|
319
|
-
|
|
441
|
+
if (selectedIndex === 0 && section === 'managed' && nativeEntries.length > 0) {
|
|
442
|
+
return {...state, section: 'native', selectedIndex: nativeEntries.length - 1, revealedEntryId: null}
|
|
443
|
+
}
|
|
444
|
+
return {...state, selectedIndex: Math.max(0, selectedIndex - 1), revealedEntryId: null}
|
|
320
445
|
}
|
|
321
446
|
if (key.name === 'down' || key.name === 'j') {
|
|
322
|
-
|
|
447
|
+
if (selectedIndex >= maxIndex && section === 'native' && entries.length > 0) {
|
|
448
|
+
return {...state, section: 'managed', selectedIndex: 0, revealedEntryId: null}
|
|
449
|
+
}
|
|
450
|
+
return {...state, selectedIndex: Math.min(maxIndex, selectedIndex + 1), revealedEntryId: null}
|
|
323
451
|
}
|
|
324
452
|
if (key.name === 'pageup') {
|
|
325
|
-
return {...state, selectedIndex: Math.max(0, selectedIndex - 10)}
|
|
453
|
+
return {...state, selectedIndex: Math.max(0, selectedIndex - 10), revealedEntryId: null}
|
|
326
454
|
}
|
|
327
455
|
if (key.name === 'pagedown') {
|
|
328
|
-
return {...state, selectedIndex: Math.min(maxIndex, selectedIndex + 10)}
|
|
456
|
+
return {...state, selectedIndex: Math.min(maxIndex, selectedIndex + 10), revealedEntryId: null}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Native section actions
|
|
460
|
+
if (section === 'native') {
|
|
461
|
+
if (key.name === 'i' && nativeEntries.length > 0) {
|
|
462
|
+
const nativeEntry = nativeEntries[selectedIndex]
|
|
463
|
+
if (nativeEntry) return {...state, _importNative: nativeEntry}
|
|
464
|
+
}
|
|
465
|
+
return state
|
|
329
466
|
}
|
|
467
|
+
|
|
468
|
+
// Managed section actions
|
|
330
469
|
if (key.name === 'n') {
|
|
331
470
|
return {...state, mode: 'form', _action: 'create'}
|
|
332
471
|
}
|
|
333
472
|
if (key.name === 'return' && entries.length > 0) {
|
|
334
|
-
|
|
473
|
+
const entry = entries[selectedIndex]
|
|
474
|
+
if (entry) {
|
|
475
|
+
const driftedIds = new Set((state.driftInfos ?? []).map((d) => d.entryId))
|
|
476
|
+
if (driftedIds.has(entry.id)) {
|
|
477
|
+
return {...state, mode: 'drift', _driftEntryId: entry.id}
|
|
478
|
+
}
|
|
479
|
+
return {...state, mode: 'form', _action: 'edit', _editId: entry.id}
|
|
480
|
+
}
|
|
335
481
|
}
|
|
336
482
|
if (key.name === 'd' && entries.length > 0) {
|
|
337
483
|
return {...state, _toggleId: entries[selectedIndex]?.id}
|
|
@@ -342,6 +488,13 @@ export function handleCategoriesKeypress(state, key) {
|
|
|
342
488
|
return {...state, mode: 'confirm-delete', confirmDeleteId: entry.id, _confirmDeleteName: entry.name}
|
|
343
489
|
}
|
|
344
490
|
}
|
|
491
|
+
// r — reveal/hide env vars for the selected MCP entry
|
|
492
|
+
if (key.name === 'r' && entries.length > 0) {
|
|
493
|
+
const entry = entries[selectedIndex]
|
|
494
|
+
if (entry?.type === 'mcp') {
|
|
495
|
+
return {...state, revealedEntryId: state.revealedEntryId === entry.id ? null : entry.id}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
345
498
|
|
|
346
499
|
return state
|
|
347
500
|
}
|
|
@@ -411,6 +564,7 @@ export function cleanupTerminal() {
|
|
|
411
564
|
* @property {(action: object) => Promise<void>} onAction - Callback for CRUD actions from category tabs
|
|
412
565
|
* @property {import('../../formatters/ai-config.js').formatEnvironmentsTable} formatEnvs - Environments table formatter
|
|
413
566
|
* @property {import('../../formatters/ai-config.js').formatCategoriesTable} formatCats - Categories table formatter
|
|
567
|
+
* @property {import('../../formatters/ai-config.js').formatNativeEntriesTable} [formatNative] - Native entries table formatter
|
|
414
568
|
* @property {(() => Promise<import('../../types.js').CategoryEntry[]>) | undefined} [refreshEntries] - Reload entries from store after mutations
|
|
415
569
|
*/
|
|
416
570
|
|
|
@@ -423,7 +577,7 @@ export function cleanupTerminal() {
|
|
|
423
577
|
* @returns {Promise<void>}
|
|
424
578
|
*/
|
|
425
579
|
export async function startTabTUI(opts) {
|
|
426
|
-
const {envs, onAction, formatEnvs, formatCats} = opts
|
|
580
|
+
const {envs, onAction, formatEnvs, formatCats, formatNative} = opts
|
|
427
581
|
const {entries: initialEntries, chezmoiEnabled} = opts
|
|
428
582
|
|
|
429
583
|
_cleanupCalled = false
|
|
@@ -443,11 +597,12 @@ export async function startTabTUI(opts) {
|
|
|
443
597
|
{label: 'Environments', key: 'environments'},
|
|
444
598
|
{label: 'MCPs', key: 'mcp'},
|
|
445
599
|
{label: 'Commands', key: 'command'},
|
|
600
|
+
{label: 'Rules', key: 'rule'},
|
|
446
601
|
{label: 'Skills', key: 'skill'},
|
|
447
602
|
{label: 'Agents', key: 'agent'},
|
|
448
603
|
]
|
|
449
604
|
|
|
450
|
-
const CATEGORY_TYPES = ['mcp', 'command', 'skill', 'agent']
|
|
605
|
+
const CATEGORY_TYPES = ['mcp', 'command', 'rule', 'skill', 'agent']
|
|
451
606
|
const chezmoidTip = chezmoiEnabled ? '' : 'Tip: Run `dvmi dotfiles setup` to enable automatic backup of your AI configs'
|
|
452
607
|
|
|
453
608
|
/** @type {TabTUIState} */
|
|
@@ -466,27 +621,53 @@ export async function startTabTUI(opts) {
|
|
|
466
621
|
/** @type {import('../../types.js').CategoryEntry[]} */
|
|
467
622
|
let allEntries = [...initialEntries]
|
|
468
623
|
|
|
624
|
+
/** Aggregate all drift infos from detected envs (flat list). */
|
|
625
|
+
const allDriftInfos = envs.flatMap((e) => e.driftedEntries ?? [])
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* @param {string} type
|
|
629
|
+
* @returns {import('../../types.js').NativeEntry[]}
|
|
630
|
+
*/
|
|
631
|
+
function getNativesByType(type) {
|
|
632
|
+
return envs.flatMap((e) => (e.nativeEntries ?? []).filter((ne) => ne.type === type))
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* @param {string} type
|
|
637
|
+
* @returns {import('../../types.js').DriftInfo[]}
|
|
638
|
+
*/
|
|
639
|
+
function getDriftsByType(type) {
|
|
640
|
+
const ids = new Set(allEntries.filter((e) => e.type === type).map((e) => e.id))
|
|
641
|
+
return allDriftInfos.filter((d) => ids.has(d.entryId))
|
|
642
|
+
}
|
|
643
|
+
|
|
469
644
|
/** @type {Record<string, CatTabState>} */
|
|
470
645
|
let catTabStates = Object.fromEntries(
|
|
471
646
|
CATEGORY_TYPES.map((type) => [
|
|
472
647
|
type,
|
|
473
648
|
/** @type {CatTabState} */ ({
|
|
474
649
|
entries: allEntries.filter((e) => e.type === type),
|
|
650
|
+
nativeEntries: getNativesByType(type),
|
|
475
651
|
selectedIndex: 0,
|
|
652
|
+
section: 'managed',
|
|
476
653
|
mode: 'list',
|
|
477
654
|
formState: null,
|
|
478
655
|
confirmDeleteId: null,
|
|
479
656
|
chezmoidTip,
|
|
657
|
+
revealedEntryId: null,
|
|
658
|
+
driftInfos: getDriftsByType(type),
|
|
480
659
|
}),
|
|
481
660
|
]),
|
|
482
661
|
)
|
|
483
662
|
|
|
484
|
-
/** Push filtered entries into each tab state — call after allEntries changes. */
|
|
663
|
+
/** Push filtered entries and drift infos into each tab state — call after allEntries changes. */
|
|
485
664
|
function syncTabEntries() {
|
|
486
665
|
for (const type of CATEGORY_TYPES) {
|
|
666
|
+
const entriesForType = allEntries.filter((e) => e.type === type)
|
|
667
|
+
const driftsForType = getDriftsByType(type)
|
|
487
668
|
catTabStates = {
|
|
488
669
|
...catTabStates,
|
|
489
|
-
[type]: {...catTabStates[type], entries:
|
|
670
|
+
[type]: {...catTabStates[type], entries: entriesForType, driftInfos: driftsForType},
|
|
490
671
|
}
|
|
491
672
|
}
|
|
492
673
|
}
|
|
@@ -517,7 +698,7 @@ export async function startTabTUI(opts) {
|
|
|
517
698
|
formatEnvs,
|
|
518
699
|
termCols,
|
|
519
700
|
)
|
|
520
|
-
hintStr = chalk.dim(' ↑↓ navigate
|
|
701
|
+
hintStr = chalk.dim(' ↑↓ navigate ←→ switch tabs q exit')
|
|
521
702
|
} else {
|
|
522
703
|
const tabKey = tabs[activeTabIndex].key
|
|
523
704
|
const tabState = catTabStates[tabKey]
|
|
@@ -525,20 +706,15 @@ export async function startTabTUI(opts) {
|
|
|
525
706
|
if (tabState.mode === 'form' && tabState.formState) {
|
|
526
707
|
contentLines = buildFormScreen(tabState.formState, contentViewportHeight, termCols)
|
|
527
708
|
hintStr = chalk.dim(' Tab next field Shift+Tab prev Ctrl+S save Esc cancel')
|
|
709
|
+
} else if (tabState.mode === 'drift') {
|
|
710
|
+
contentLines = buildDriftScreen(tabState, contentViewportHeight, termCols)
|
|
711
|
+
hintStr = chalk.dim(' r re-deploy a accept changes Esc back')
|
|
528
712
|
} else {
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
tabState.entries,
|
|
535
|
-
tabState.selectedIndex,
|
|
536
|
-
contentViewportHeight,
|
|
537
|
-
formatCats,
|
|
538
|
-
termCols,
|
|
539
|
-
confirmName,
|
|
540
|
-
)
|
|
541
|
-
hintStr = chalk.dim(' ↑↓ navigate n new Enter edit d toggle Del delete Tab switch q exit')
|
|
713
|
+
const nativeFmt = formatNative ?? formatNativeEntriesTableFallback
|
|
714
|
+
contentLines = buildCategoriesTab(tabState, contentViewportHeight, formatCats, nativeFmt, termCols)
|
|
715
|
+
const sectionHint = tabState.nativeEntries.length > 0 ? ' Tab section' : ''
|
|
716
|
+
const nativeHint = tabState.section === 'native' ? ' i import' : ' n new Enter edit d toggle Del delete r reveal'
|
|
717
|
+
hintStr = chalk.dim(` ↑↓ navigate ←→ tabs${nativeHint}${sectionHint} q exit`)
|
|
542
718
|
}
|
|
543
719
|
}
|
|
544
720
|
|
|
@@ -572,8 +748,16 @@ export async function startTabTUI(opts) {
|
|
|
572
748
|
const listener = async (_str, key) => {
|
|
573
749
|
if (!key) return
|
|
574
750
|
|
|
575
|
-
//
|
|
576
|
-
|
|
751
|
+
// ── Compute mode guards ──
|
|
752
|
+
const activeTabKey = tuiState.activeTabIndex > 0 ? tabs[tuiState.activeTabIndex].key : null
|
|
753
|
+
const activeCatState = activeTabKey ? catTabStates[activeTabKey] : null
|
|
754
|
+
const isInFormMode = activeCatState?.mode === 'form'
|
|
755
|
+
const isInDriftMode = activeCatState?.mode === 'drift'
|
|
756
|
+
const isInConfirmDelete = activeCatState?.mode === 'confirm-delete'
|
|
757
|
+
const isModalMode = isInFormMode || isInDriftMode || isInConfirmDelete
|
|
758
|
+
|
|
759
|
+
// ── Ctrl+C: always exit ──
|
|
760
|
+
if (key.ctrl && key.name === 'c') {
|
|
577
761
|
process.stdout.removeListener('resize', onResize)
|
|
578
762
|
process.removeListener('SIGINT', sigHandler)
|
|
579
763
|
process.removeListener('SIGTERM', sigHandler)
|
|
@@ -582,7 +766,9 @@ export async function startTabTUI(opts) {
|
|
|
582
766
|
resolve()
|
|
583
767
|
return
|
|
584
768
|
}
|
|
585
|
-
|
|
769
|
+
|
|
770
|
+
// ── Esc: close sub-mode first; exit TUI only from list mode ──
|
|
771
|
+
if (key.name === 'escape' && !isModalMode) {
|
|
586
772
|
process.stdout.removeListener('resize', onResize)
|
|
587
773
|
process.removeListener('SIGINT', sigHandler)
|
|
588
774
|
process.removeListener('SIGTERM', sigHandler)
|
|
@@ -591,19 +777,49 @@ export async function startTabTUI(opts) {
|
|
|
591
777
|
resolve()
|
|
592
778
|
return
|
|
593
779
|
}
|
|
780
|
+
// In modal modes Esc falls through to per-tab handlers (form cancel, drift back, etc.)
|
|
594
781
|
|
|
595
|
-
//
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
782
|
+
// ── q: exit TUI only from list mode (in forms q is a regular character) ──
|
|
783
|
+
if (key.name === 'q' && !isModalMode) {
|
|
784
|
+
process.stdout.removeListener('resize', onResize)
|
|
785
|
+
process.removeListener('SIGINT', sigHandler)
|
|
786
|
+
process.removeListener('SIGTERM', sigHandler)
|
|
787
|
+
process.removeListener('exit', exitHandler)
|
|
788
|
+
cleanupTerminal()
|
|
789
|
+
resolve()
|
|
790
|
+
return
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// ── Left/Right arrows: tab switching (blocked in modal sub-modes) ──
|
|
794
|
+
if ((key.name === 'right' || key.name === 'l') && !isModalMode) {
|
|
795
|
+
tuiState = {...tuiState, activeTabIndex: (tuiState.activeTabIndex + 1) % tabs.length}
|
|
796
|
+
render()
|
|
797
|
+
return
|
|
798
|
+
}
|
|
799
|
+
if ((key.name === 'left' || key.name === 'h') && !isModalMode) {
|
|
800
|
+
tuiState = {...tuiState, activeTabIndex: (tuiState.activeTabIndex - 1 + tabs.length) % tabs.length}
|
|
603
801
|
render()
|
|
604
802
|
return
|
|
605
803
|
}
|
|
606
804
|
|
|
805
|
+
// ── Tab: toggle Native/Managed section within category tabs ──
|
|
806
|
+
if (key.name === 'tab' && !key.shift && !isInFormMode) {
|
|
807
|
+
if (activeCatState && activeCatState.nativeEntries?.length > 0 && !isInDriftMode) {
|
|
808
|
+
catTabStates = {
|
|
809
|
+
...catTabStates,
|
|
810
|
+
[activeTabKey]: {
|
|
811
|
+
...activeCatState,
|
|
812
|
+
section: activeCatState.section === 'managed' ? 'native' : 'managed',
|
|
813
|
+
selectedIndex: 0,
|
|
814
|
+
revealedEntryId: null,
|
|
815
|
+
},
|
|
816
|
+
}
|
|
817
|
+
render()
|
|
818
|
+
return
|
|
819
|
+
}
|
|
820
|
+
// No native entries or Environments tab — Tab is a no-op (use ←/→)
|
|
821
|
+
}
|
|
822
|
+
|
|
607
823
|
// Delegate to active tab
|
|
608
824
|
if (tuiState.activeTabIndex === 0) {
|
|
609
825
|
// Environments tab — read-only
|
|
@@ -714,6 +930,73 @@ export async function startTabTUI(opts) {
|
|
|
714
930
|
return
|
|
715
931
|
}
|
|
716
932
|
|
|
933
|
+
// T017: Import native entry into managed sync
|
|
934
|
+
if (result._importNative) {
|
|
935
|
+
const nativeEntry = result._importNative
|
|
936
|
+
catTabStates = {...catTabStates, [tabKey]: {...result, _importNative: null}}
|
|
937
|
+
render()
|
|
938
|
+
try {
|
|
939
|
+
await onAction({type: 'import-native', nativeEntry})
|
|
940
|
+
if (opts.refreshEntries) {
|
|
941
|
+
allEntries = await opts.refreshEntries()
|
|
942
|
+
syncTabEntries()
|
|
943
|
+
// Update native entries: remove imported entry from native list
|
|
944
|
+
catTabStates = {
|
|
945
|
+
...catTabStates,
|
|
946
|
+
[tabKey]: {
|
|
947
|
+
...catTabStates[tabKey],
|
|
948
|
+
nativeEntries: catTabStates[tabKey].nativeEntries.filter(
|
|
949
|
+
(ne) => !(ne.name === nativeEntry.name && ne.environmentId === nativeEntry.environmentId),
|
|
950
|
+
),
|
|
951
|
+
section: 'managed',
|
|
952
|
+
selectedIndex: 0,
|
|
953
|
+
},
|
|
954
|
+
}
|
|
955
|
+
render()
|
|
956
|
+
}
|
|
957
|
+
} catch {
|
|
958
|
+
// Import failed — re-render so user sees the entry stayed in native
|
|
959
|
+
render()
|
|
960
|
+
}
|
|
961
|
+
return
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// T018: Re-deploy after drift resolution
|
|
965
|
+
if (result._redeploy) {
|
|
966
|
+
const idToRedeploy = result._redeploy
|
|
967
|
+
catTabStates = {...catTabStates, [tabKey]: {...result, _redeploy: null}}
|
|
968
|
+
render()
|
|
969
|
+
try {
|
|
970
|
+
await onAction({type: 'redeploy', id: idToRedeploy})
|
|
971
|
+
if (opts.refreshEntries) {
|
|
972
|
+
allEntries = await opts.refreshEntries()
|
|
973
|
+
syncTabEntries()
|
|
974
|
+
render()
|
|
975
|
+
}
|
|
976
|
+
} catch {
|
|
977
|
+
/* ignore */
|
|
978
|
+
}
|
|
979
|
+
return
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// T018: Accept drift (update store from file)
|
|
983
|
+
if (result._acceptDrift) {
|
|
984
|
+
const idToAccept = result._acceptDrift
|
|
985
|
+
catTabStates = {...catTabStates, [tabKey]: {...result, _acceptDrift: null}}
|
|
986
|
+
render()
|
|
987
|
+
try {
|
|
988
|
+
await onAction({type: 'accept-drift', id: idToAccept})
|
|
989
|
+
if (opts.refreshEntries) {
|
|
990
|
+
allEntries = await opts.refreshEntries()
|
|
991
|
+
syncTabEntries()
|
|
992
|
+
render()
|
|
993
|
+
}
|
|
994
|
+
} catch {
|
|
995
|
+
/* ignore */
|
|
996
|
+
}
|
|
997
|
+
return
|
|
998
|
+
}
|
|
999
|
+
|
|
717
1000
|
if (result._action === 'create') {
|
|
718
1001
|
const compatibleEnvs = envs.filter((e) => e.supportedCategories.includes(tabKey))
|
|
719
1002
|
const fields =
|
|
@@ -721,9 +1004,11 @@ export async function startTabTUI(opts) {
|
|
|
721
1004
|
? getMCPFormFields(null, compatibleEnvs)
|
|
722
1005
|
: tabKey === 'command'
|
|
723
1006
|
? getCommandFormFields(null, compatibleEnvs)
|
|
724
|
-
: tabKey === '
|
|
725
|
-
?
|
|
726
|
-
:
|
|
1007
|
+
: tabKey === 'rule'
|
|
1008
|
+
? getRuleFormFields(null, compatibleEnvs)
|
|
1009
|
+
: tabKey === 'skill'
|
|
1010
|
+
? getSkillFormFields(null, compatibleEnvs)
|
|
1011
|
+
: getAgentFormFields(null, compatibleEnvs)
|
|
727
1012
|
const tabLabel = tabKey === 'mcp' ? 'MCP' : tabKey.charAt(0).toUpperCase() + tabKey.slice(1)
|
|
728
1013
|
catTabStates = {
|
|
729
1014
|
...catTabStates,
|
|
@@ -738,6 +1023,7 @@ export async function startTabTUI(opts) {
|
|
|
738
1023
|
title: `Create ${tabLabel}`,
|
|
739
1024
|
status: 'editing',
|
|
740
1025
|
errorMessage: null,
|
|
1026
|
+
customValidator: tabKey === 'mcp' ? validateMCPForm : null,
|
|
741
1027
|
},
|
|
742
1028
|
},
|
|
743
1029
|
}
|
|
@@ -754,9 +1040,11 @@ export async function startTabTUI(opts) {
|
|
|
754
1040
|
? getMCPFormFields(entry, compatibleEnvs)
|
|
755
1041
|
: entry.type === 'command'
|
|
756
1042
|
? getCommandFormFields(entry, compatibleEnvs)
|
|
757
|
-
: entry.type === '
|
|
758
|
-
?
|
|
759
|
-
:
|
|
1043
|
+
: entry.type === 'rule'
|
|
1044
|
+
? getRuleFormFields(entry, compatibleEnvs)
|
|
1045
|
+
: entry.type === 'skill'
|
|
1046
|
+
? getSkillFormFields(entry, compatibleEnvs)
|
|
1047
|
+
: getAgentFormFields(entry, compatibleEnvs)
|
|
760
1048
|
catTabStates = {
|
|
761
1049
|
...catTabStates,
|
|
762
1050
|
[tabKey]: {
|
|
@@ -770,6 +1058,7 @@ export async function startTabTUI(opts) {
|
|
|
770
1058
|
title: `Edit ${entry.name}`,
|
|
771
1059
|
status: 'editing',
|
|
772
1060
|
errorMessage: null,
|
|
1061
|
+
customValidator: entry.type === 'mcp' ? validateMCPForm : null,
|
|
773
1062
|
},
|
|
774
1063
|
},
|
|
775
1064
|
}
|