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/types.js
CHANGED
|
@@ -337,11 +337,11 @@
|
|
|
337
337
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
338
338
|
|
|
339
339
|
/**
|
|
340
|
-
* @typedef {'mcp'|'command'|'skill'|'agent'} CategoryType
|
|
340
|
+
* @typedef {'mcp'|'command'|'rule'|'skill'|'agent'} CategoryType
|
|
341
341
|
*/
|
|
342
342
|
|
|
343
343
|
/**
|
|
344
|
-
* @typedef {'vscode-copilot'|'claude-code'|'opencode'|'gemini-cli'|'copilot-cli'} EnvironmentId
|
|
344
|
+
* @typedef {'vscode-copilot'|'claude-code'|'claude-desktop'|'opencode'|'gemini-cli'|'copilot-cli'|'cursor'|'windsurf'|'continue-dev'|'zed'|'amazon-q'} EnvironmentId
|
|
345
345
|
*/
|
|
346
346
|
|
|
347
347
|
/**
|
|
@@ -359,6 +359,12 @@
|
|
|
359
359
|
* @property {string} [description] - Short description of the command
|
|
360
360
|
*/
|
|
361
361
|
|
|
362
|
+
/**
|
|
363
|
+
* @typedef {Object} RuleParams
|
|
364
|
+
* @property {string} content - Rules/instructions content (multi-line Markdown)
|
|
365
|
+
* @property {string} [description] - Short description of the rule
|
|
366
|
+
*/
|
|
367
|
+
|
|
362
368
|
/**
|
|
363
369
|
* @typedef {Object} SkillParams
|
|
364
370
|
* @property {string} content - Skill definition content (multi-line)
|
|
@@ -378,7 +384,7 @@
|
|
|
378
384
|
* @property {CategoryType} type - Category type
|
|
379
385
|
* @property {boolean} active - true = deployed to environments, false = removed but kept in store
|
|
380
386
|
* @property {EnvironmentId[]} environments - Target environments for deployment
|
|
381
|
-
* @property {MCPParams|CommandParams|SkillParams|AgentParams} params - Type-specific parameters
|
|
387
|
+
* @property {MCPParams|CommandParams|RuleParams|SkillParams|AgentParams} params - Type-specific parameters
|
|
382
388
|
* @property {string} createdAt - ISO 8601 timestamp
|
|
383
389
|
* @property {string} updatedAt - ISO 8601 timestamp
|
|
384
390
|
*/
|
|
@@ -400,10 +406,33 @@
|
|
|
400
406
|
* @typedef {Object} CategoryCounts
|
|
401
407
|
* @property {number} mcp
|
|
402
408
|
* @property {number} command
|
|
409
|
+
* @property {number} rule
|
|
403
410
|
* @property {number} skill
|
|
404
411
|
* @property {number} agent
|
|
405
412
|
*/
|
|
406
413
|
|
|
414
|
+
/**
|
|
415
|
+
* @typedef {Object} NativeEntry
|
|
416
|
+
* Runtime only — not persisted. Represents an item found in an environment's config
|
|
417
|
+
* file that is NOT managed by dvmi.
|
|
418
|
+
* @property {string} name - Entry name (extracted from config key or filename)
|
|
419
|
+
* @property {CategoryType} type - Category type
|
|
420
|
+
* @property {EnvironmentId} environmentId - Source environment
|
|
421
|
+
* @property {'project'|'global'} level - Whether from project-level or global-level config
|
|
422
|
+
* @property {string} sourcePath - Absolute path to the source config file
|
|
423
|
+
* @property {object} params - Normalized parameters (same structure as managed entry params)
|
|
424
|
+
*/
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* @typedef {Object} DriftInfo
|
|
428
|
+
* Runtime only — not persisted. Describes a managed entry whose deployed state
|
|
429
|
+
* diverges from dvmi's stored expected state.
|
|
430
|
+
* @property {string} entryId - ID of the managed CategoryEntry that drifted
|
|
431
|
+
* @property {EnvironmentId} environmentId - Environment where drift was detected
|
|
432
|
+
* @property {object} expected - What dvmi expects (from store)
|
|
433
|
+
* @property {object} actual - What was found in the file
|
|
434
|
+
*/
|
|
435
|
+
|
|
407
436
|
/**
|
|
408
437
|
* @typedef {Object} DetectedEnvironment
|
|
409
438
|
* @property {EnvironmentId} id - Environment identifier
|
|
@@ -414,6 +443,9 @@
|
|
|
414
443
|
* @property {string[]} unreadable - Paths that exist but failed to parse
|
|
415
444
|
* @property {CategoryType[]} supportedCategories - Category types this environment supports
|
|
416
445
|
* @property {CategoryCounts} counts - Per-category item counts from dvmi-managed entries
|
|
446
|
+
* @property {CategoryCounts} nativeCounts - Per-category native item counts (items in config files)
|
|
447
|
+
* @property {NativeEntry[]} nativeEntries - All native entries found for this environment
|
|
448
|
+
* @property {DriftInfo[]} driftedEntries - Managed entries that have drifted from expected state
|
|
417
449
|
* @property {'project'|'global'|'both'} scope - Where detection occurred
|
|
418
450
|
*/
|
|
419
451
|
|
package/src/utils/tui/form.js
CHANGED
|
@@ -20,6 +20,7 @@ import chalk from 'chalk'
|
|
|
20
20
|
* @property {boolean} required
|
|
21
21
|
* @property {string} placeholder
|
|
22
22
|
* @property {string} [key] - Optional override key for extractValues output
|
|
23
|
+
* @property {boolean} [hidden] - When true, field is skipped in rendering, navigation, validation, and extraction
|
|
23
24
|
*/
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -30,6 +31,7 @@ import chalk from 'chalk'
|
|
|
30
31
|
* @property {number} selectedIndex
|
|
31
32
|
* @property {boolean} required
|
|
32
33
|
* @property {string} [key]
|
|
34
|
+
* @property {boolean} [hidden]
|
|
33
35
|
*/
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -45,6 +47,7 @@ import chalk from 'chalk'
|
|
|
45
47
|
* @property {number} focusedOptionIndex
|
|
46
48
|
* @property {boolean} required
|
|
47
49
|
* @property {string} [key]
|
|
50
|
+
* @property {boolean} [hidden]
|
|
48
51
|
*/
|
|
49
52
|
|
|
50
53
|
/**
|
|
@@ -56,6 +59,7 @@ import chalk from 'chalk'
|
|
|
56
59
|
* @property {number} cursorCol
|
|
57
60
|
* @property {boolean} required
|
|
58
61
|
* @property {string} [key]
|
|
62
|
+
* @property {boolean} [hidden]
|
|
59
63
|
*/
|
|
60
64
|
|
|
61
65
|
/**
|
|
@@ -69,6 +73,7 @@ import chalk from 'chalk'
|
|
|
69
73
|
* @property {string} title
|
|
70
74
|
* @property {'editing'|'submitted'|'cancelled'} status
|
|
71
75
|
* @property {string|null} errorMessage
|
|
76
|
+
* @property {((state: FormState) => string|null)|null} [customValidator] - Optional transport-specific validator
|
|
72
77
|
*/
|
|
73
78
|
|
|
74
79
|
/**
|
|
@@ -97,6 +102,25 @@ function fieldKey(field) {
|
|
|
97
102
|
return field.label.toLowerCase().replace(/\s+/g, '_')
|
|
98
103
|
}
|
|
99
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Find the next visible (non-hidden) field index in a given direction.
|
|
107
|
+
* Wraps around the array. Returns current index if all fields are hidden.
|
|
108
|
+
* @param {Field[]} fields
|
|
109
|
+
* @param {number} current - Current focused index
|
|
110
|
+
* @param {1|-1} direction - 1 for forward, -1 for backward
|
|
111
|
+
* @returns {number}
|
|
112
|
+
*/
|
|
113
|
+
function nextVisibleIndex(fields, current, direction) {
|
|
114
|
+
const len = fields.length
|
|
115
|
+
let next = (current + direction + len) % len
|
|
116
|
+
let checked = 0
|
|
117
|
+
while (fields[next]?.hidden && checked < len) {
|
|
118
|
+
next = (next + direction + len) % len
|
|
119
|
+
checked++
|
|
120
|
+
}
|
|
121
|
+
return next
|
|
122
|
+
}
|
|
123
|
+
|
|
100
124
|
/**
|
|
101
125
|
* Render the text cursor inside a string value at the given position.
|
|
102
126
|
* Inserts a `|` character at the cursor index.
|
|
@@ -263,6 +287,7 @@ export function buildFormScreen(formState, viewportHeight, termCols) {
|
|
|
263
287
|
|
|
264
288
|
for (let i = 0; i < formState.fields.length; i++) {
|
|
265
289
|
const field = formState.fields[i]
|
|
290
|
+
if (field.hidden) continue
|
|
266
291
|
const isFocused = i === formState.focusedFieldIndex
|
|
267
292
|
|
|
268
293
|
// Header line
|
|
@@ -311,6 +336,7 @@ export function extractValues(formState) {
|
|
|
311
336
|
const result = {}
|
|
312
337
|
|
|
313
338
|
for (const field of formState.fields) {
|
|
339
|
+
if (field.hidden) continue
|
|
314
340
|
const key = fieldKey(field)
|
|
315
341
|
|
|
316
342
|
if (field.type === 'text') {
|
|
@@ -339,6 +365,7 @@ export function extractValues(formState) {
|
|
|
339
365
|
*/
|
|
340
366
|
function validateForm(formState) {
|
|
341
367
|
for (const field of formState.fields) {
|
|
368
|
+
if (field.hidden) continue
|
|
342
369
|
if (!field.required) continue
|
|
343
370
|
|
|
344
371
|
if (field.type === 'text' && field.value.trim() === '') {
|
|
@@ -636,20 +663,20 @@ export function handleFormKeypress(formState, key) {
|
|
|
636
663
|
return attemptSubmit(formState)
|
|
637
664
|
}
|
|
638
665
|
|
|
639
|
-
// ── Tab: next field
|
|
666
|
+
// ── Tab: next field (skip hidden) ──────────────────────────────────────────
|
|
640
667
|
if (key.name === 'tab' && !key.shift) {
|
|
641
668
|
return {
|
|
642
669
|
...formState,
|
|
643
|
-
focusedFieldIndex: (focusedFieldIndex
|
|
670
|
+
focusedFieldIndex: nextVisibleIndex(fields, focusedFieldIndex, 1),
|
|
644
671
|
errorMessage: null,
|
|
645
672
|
}
|
|
646
673
|
}
|
|
647
674
|
|
|
648
|
-
// ── Shift+Tab: previous field
|
|
675
|
+
// ── Shift+Tab: previous field (skip hidden) ──────────────────────────────
|
|
649
676
|
if (key.name === 'tab' && key.shift) {
|
|
650
677
|
return {
|
|
651
678
|
...formState,
|
|
652
|
-
focusedFieldIndex: (focusedFieldIndex -
|
|
679
|
+
focusedFieldIndex: nextVisibleIndex(fields, focusedFieldIndex, -1),
|
|
653
680
|
errorMessage: null,
|
|
654
681
|
}
|
|
655
682
|
}
|
|
@@ -680,9 +707,15 @@ export function handleFormKeypress(formState, key) {
|
|
|
680
707
|
if (focusedField.type === 'selector') {
|
|
681
708
|
const updated = handleSelectorFieldKey(focusedField, key)
|
|
682
709
|
if (updated === focusedField) return formState
|
|
710
|
+
let newFields = replaceAt(fields, focusedFieldIndex, updated)
|
|
711
|
+
// Dynamic visibility: when transport selector changes, toggle field visibility
|
|
712
|
+
if (fieldKey(focusedField) === 'transport') {
|
|
713
|
+
const newTransport = updated.options[updated.selectedIndex]
|
|
714
|
+
newFields = updateMCPFieldVisibility(newFields, newTransport)
|
|
715
|
+
}
|
|
683
716
|
return {
|
|
684
717
|
...formState,
|
|
685
|
-
fields:
|
|
718
|
+
fields: newFields,
|
|
686
719
|
}
|
|
687
720
|
}
|
|
688
721
|
|
|
@@ -691,7 +724,7 @@ export function handleFormKeypress(formState, key) {
|
|
|
691
724
|
if ('advanceField' in result) {
|
|
692
725
|
return {
|
|
693
726
|
...formState,
|
|
694
|
-
focusedFieldIndex:
|
|
727
|
+
focusedFieldIndex: nextVisibleIndex(fields, focusedFieldIndex, 1),
|
|
695
728
|
}
|
|
696
729
|
}
|
|
697
730
|
if (result === focusedField) return formState
|
|
@@ -704,11 +737,10 @@ export function handleFormKeypress(formState, key) {
|
|
|
704
737
|
if (focusedField.type === 'editor') {
|
|
705
738
|
const result = handleEditorFieldKey(focusedField, key)
|
|
706
739
|
if ('advanceField' in result) {
|
|
707
|
-
// Esc in editor
|
|
708
|
-
// Per spec, Esc in editor moves to next field.
|
|
740
|
+
// Esc in editor moves to next field (skip hidden)
|
|
709
741
|
return {
|
|
710
742
|
...formState,
|
|
711
|
-
focusedFieldIndex:
|
|
743
|
+
focusedFieldIndex: nextVisibleIndex(fields, focusedFieldIndex, 1),
|
|
712
744
|
}
|
|
713
745
|
}
|
|
714
746
|
if (result === focusedField) return formState
|
|
@@ -735,6 +767,13 @@ function attemptSubmit(formState) {
|
|
|
735
767
|
errorMessage: `"${invalidLabel}" is required.`,
|
|
736
768
|
}
|
|
737
769
|
}
|
|
770
|
+
// Run custom validator (e.g. MCP transport-specific checks)
|
|
771
|
+
if (formState.customValidator) {
|
|
772
|
+
const customError = formState.customValidator(formState)
|
|
773
|
+
if (customError !== null) {
|
|
774
|
+
return {...formState, errorMessage: customError}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
738
777
|
return {
|
|
739
778
|
submitted: /** @type {true} */ (true),
|
|
740
779
|
values: extractValues(formState),
|
|
@@ -760,8 +799,11 @@ function replaceAt(arr, index, value) {
|
|
|
760
799
|
/**
|
|
761
800
|
* Return form fields for creating or editing an MCP entry.
|
|
762
801
|
*
|
|
763
|
-
* Fields: name (text), environments (multiselect), transport (selector),
|
|
764
|
-
*
|
|
802
|
+
* Fields: name (text), environments (multiselect), transport (selector),
|
|
803
|
+
* command (text, stdio only), args (editor, stdio only), url (text, remote only),
|
|
804
|
+
* env vars (editor), description (text, optional).
|
|
805
|
+
*
|
|
806
|
+
* Fields are dynamically shown/hidden based on the selected transport.
|
|
765
807
|
*
|
|
766
808
|
* @param {import('../../types.js').CategoryEntry|null} [entry] - Existing entry to pre-fill from, or null to create
|
|
767
809
|
* @param {import('../../types.js').DetectedEnvironment[]} [compatibleEnvs] - Environments compatible with this category type
|
|
@@ -773,8 +815,10 @@ export function getMCPFormFields(entry = null, compatibleEnvs = []) {
|
|
|
773
815
|
|
|
774
816
|
const transportOptions = ['stdio', 'sse', 'streamable-http']
|
|
775
817
|
const transportIndex = p ? Math.max(0, transportOptions.indexOf(p.transport)) : 0
|
|
818
|
+
const transport = transportOptions[transportIndex]
|
|
776
819
|
|
|
777
|
-
|
|
820
|
+
/** @type {Field[]} */
|
|
821
|
+
const fields = [
|
|
778
822
|
/** @type {TextField} */ ({
|
|
779
823
|
type: 'text',
|
|
780
824
|
label: 'Name',
|
|
@@ -810,14 +854,14 @@ export function getMCPFormFields(entry = null, compatibleEnvs = []) {
|
|
|
810
854
|
required: false,
|
|
811
855
|
placeholder: 'npx my-mcp-server',
|
|
812
856
|
}),
|
|
813
|
-
/** @type {
|
|
814
|
-
type: '
|
|
857
|
+
/** @type {MiniEditorField} */ ({
|
|
858
|
+
type: 'editor',
|
|
815
859
|
label: 'Args',
|
|
816
860
|
key: 'args',
|
|
817
|
-
|
|
818
|
-
|
|
861
|
+
lines: p?.args?.length > 0 ? [...p.args] : [''],
|
|
862
|
+
cursorLine: 0,
|
|
863
|
+
cursorCol: 0,
|
|
819
864
|
required: false,
|
|
820
|
-
placeholder: '--port 3000 --verbose',
|
|
821
865
|
}),
|
|
822
866
|
/** @type {TextField} */ ({
|
|
823
867
|
type: 'text',
|
|
@@ -828,6 +872,15 @@ export function getMCPFormFields(entry = null, compatibleEnvs = []) {
|
|
|
828
872
|
required: false,
|
|
829
873
|
placeholder: 'https://mcp.example.com',
|
|
830
874
|
}),
|
|
875
|
+
/** @type {MiniEditorField} */ ({
|
|
876
|
+
type: 'editor',
|
|
877
|
+
label: 'Env Vars',
|
|
878
|
+
key: 'env',
|
|
879
|
+
lines: p?.env ? Object.entries(p.env).map(([k, v]) => `${k}=${v}`) : [''],
|
|
880
|
+
cursorLine: 0,
|
|
881
|
+
cursorCol: 0,
|
|
882
|
+
required: false,
|
|
883
|
+
}),
|
|
831
884
|
/** @type {TextField} */ ({
|
|
832
885
|
type: 'text',
|
|
833
886
|
label: 'Description',
|
|
@@ -838,6 +891,67 @@ export function getMCPFormFields(entry = null, compatibleEnvs = []) {
|
|
|
838
891
|
placeholder: 'Optional description',
|
|
839
892
|
}),
|
|
840
893
|
]
|
|
894
|
+
|
|
895
|
+
return updateMCPFieldVisibility(fields, transport)
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Toggle visibility of MCP-specific fields based on the selected transport.
|
|
900
|
+
* - stdio: show Command + Args, hide URL
|
|
901
|
+
* - sse/streamable-http: show URL, hide Command + Args
|
|
902
|
+
* - Env Vars and Description are always visible
|
|
903
|
+
*
|
|
904
|
+
* @param {Field[]} fields
|
|
905
|
+
* @param {string} transport
|
|
906
|
+
* @returns {Field[]}
|
|
907
|
+
*/
|
|
908
|
+
export function updateMCPFieldVisibility(fields, transport) {
|
|
909
|
+
const isStdio = transport === 'stdio'
|
|
910
|
+
return fields.map((f) => {
|
|
911
|
+
const key = f.key || f.label.toLowerCase().replace(/\s+/g, '_')
|
|
912
|
+
if (key === 'command' || key === 'args') {
|
|
913
|
+
return {...f, hidden: !isStdio}
|
|
914
|
+
}
|
|
915
|
+
if (key === 'url') {
|
|
916
|
+
return {...f, hidden: isStdio}
|
|
917
|
+
}
|
|
918
|
+
return f
|
|
919
|
+
})
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* MCP-specific form validator. Checks that:
|
|
924
|
+
* - stdio transport has a non-empty command
|
|
925
|
+
* - sse/streamable-http transport has a non-empty URL
|
|
926
|
+
* - Env var lines (if any) follow KEY=VALUE format
|
|
927
|
+
*
|
|
928
|
+
* @param {FormState} formState
|
|
929
|
+
* @returns {string|null} Error message or null if valid
|
|
930
|
+
*/
|
|
931
|
+
export function validateMCPForm(formState) {
|
|
932
|
+
const values = extractValues(formState)
|
|
933
|
+
const transport = values.transport
|
|
934
|
+
|
|
935
|
+
if (transport === 'stdio') {
|
|
936
|
+
if (!values.command || /** @type {string} */ (values.command).trim() === '') {
|
|
937
|
+
return 'Command is required for stdio transport'
|
|
938
|
+
}
|
|
939
|
+
} else if (transport === 'sse' || transport === 'streamable-http') {
|
|
940
|
+
if (!values.url || /** @type {string} */ (values.url).trim() === '') {
|
|
941
|
+
return 'URL is required for sse/streamable-http transport'
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (values.env && typeof values.env === 'string') {
|
|
946
|
+
const lines = /** @type {string} */ (values.env).split('\n').filter((l) => l.trim() !== '')
|
|
947
|
+
for (const line of lines) {
|
|
948
|
+
if (!line.includes('=')) {
|
|
949
|
+
return `Invalid env var format: "${line}" (expected KEY=VALUE)`
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
return null
|
|
841
955
|
}
|
|
842
956
|
|
|
843
957
|
/**
|
|
@@ -950,6 +1064,70 @@ export function getSkillFormFields(entry = null, compatibleEnvs = []) {
|
|
|
950
1064
|
]
|
|
951
1065
|
}
|
|
952
1066
|
|
|
1067
|
+
/**
|
|
1068
|
+
* Return form fields for creating or editing an Agent entry.
|
|
1069
|
+
*
|
|
1070
|
+
* Fields: name (text), environments (multiselect), description (text, optional), instructions (editor).
|
|
1071
|
+
*
|
|
1072
|
+
* @param {import('../../types.js').CategoryEntry|null} [entry] - Existing entry to pre-fill from, or null to create
|
|
1073
|
+
* @param {import('../../types.js').DetectedEnvironment[]} [compatibleEnvs] - Environments compatible with this category type
|
|
1074
|
+
* @returns {Field[]}
|
|
1075
|
+
*/
|
|
1076
|
+
/**
|
|
1077
|
+
* Return form fields for creating or editing a Rule entry.
|
|
1078
|
+
*
|
|
1079
|
+
* Fields: name (text), environments (multiselect), description (text, optional), content (editor).
|
|
1080
|
+
*
|
|
1081
|
+
* @param {import('../../types.js').CategoryEntry|null} [entry] - Existing entry to pre-fill from, or null to create
|
|
1082
|
+
* @param {import('../../types.js').DetectedEnvironment[]} [compatibleEnvs] - Environments compatible with this category type
|
|
1083
|
+
* @returns {Field[]}
|
|
1084
|
+
*/
|
|
1085
|
+
export function getRuleFormFields(entry = null, compatibleEnvs = []) {
|
|
1086
|
+
/** @type {import('../../types.js').RuleParams|null} */
|
|
1087
|
+
const p = entry ? /** @type {import('../../types.js').RuleParams} */ (entry.params) : null
|
|
1088
|
+
const contentStr = p?.content ?? ''
|
|
1089
|
+
const contentLines = contentStr.length > 0 ? contentStr.split('\n') : ['']
|
|
1090
|
+
|
|
1091
|
+
return [
|
|
1092
|
+
/** @type {TextField} */ ({
|
|
1093
|
+
type: 'text',
|
|
1094
|
+
label: 'Name',
|
|
1095
|
+
key: 'name',
|
|
1096
|
+
value: entry ? entry.name : '',
|
|
1097
|
+
cursor: entry ? entry.name.length : 0,
|
|
1098
|
+
required: true,
|
|
1099
|
+
placeholder: 'my-rule',
|
|
1100
|
+
}),
|
|
1101
|
+
/** @type {MultiSelectField} */ ({
|
|
1102
|
+
type: 'multiselect',
|
|
1103
|
+
label: 'Environments',
|
|
1104
|
+
key: 'environments',
|
|
1105
|
+
options: compatibleEnvs.map((env) => ({id: env.id, label: env.name})),
|
|
1106
|
+
selected: new Set(entry ? entry.environments : []),
|
|
1107
|
+
focusedOptionIndex: 0,
|
|
1108
|
+
required: true,
|
|
1109
|
+
}),
|
|
1110
|
+
/** @type {TextField} */ ({
|
|
1111
|
+
type: 'text',
|
|
1112
|
+
label: 'Description',
|
|
1113
|
+
key: 'description',
|
|
1114
|
+
value: p?.description ?? '',
|
|
1115
|
+
cursor: (p?.description ?? '').length,
|
|
1116
|
+
required: false,
|
|
1117
|
+
placeholder: 'Optional description',
|
|
1118
|
+
}),
|
|
1119
|
+
/** @type {MiniEditorField} */ ({
|
|
1120
|
+
type: 'editor',
|
|
1121
|
+
label: 'Content',
|
|
1122
|
+
key: 'content',
|
|
1123
|
+
lines: contentLines,
|
|
1124
|
+
cursorLine: 0,
|
|
1125
|
+
cursorCol: 0,
|
|
1126
|
+
required: true,
|
|
1127
|
+
}),
|
|
1128
|
+
]
|
|
1129
|
+
}
|
|
1130
|
+
|
|
953
1131
|
/**
|
|
954
1132
|
* Return form fields for creating or editing an Agent entry.
|
|
955
1133
|
*
|