@skaile/forge-settings 1.3.0 → 1.3.2
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 +8 -0
- package/README.md +2 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/ForgeProvidersPanel.d.vue.ts +3 -0
- package/dist/runtime/components/ForgeProvidersPanel.vue +175 -187
- package/dist/runtime/components/ForgeProvidersPanel.vue.d.ts +3 -0
- package/dist/runtime/vue/providers-panel-model.d.ts +17 -0
- package/dist/runtime/vue/providers-panel-model.js +9 -0
- package/package.json +1 -1
- package/src/runtime/components/ForgeProvidersPanel.vue +177 -189
- package/src/runtime/vue/providers-panel-model.ts +31 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.2
|
|
4
|
+
|
|
5
|
+
- `<ForgeProvidersPanel>` lists providers as stacked rows instead of a six-column table: name and badge, then one muted line with id, preset and base URL, the credential picker below, and Test / Edit / Delete / Enabled on the right. It fits a settings column without clipping the actions or wrapping the base URL per character. Test ids are unchanged.
|
|
6
|
+
|
|
7
|
+
## 1.3.1
|
|
8
|
+
|
|
9
|
+
- `<ForgeProvidersPanel>` takes an optional `note` prop: a string replaces the line above the provider list, `false` hides it (Add provider stays, right-aligned). Absent keeps today's text, so existing hosts see no change.
|
|
10
|
+
|
|
3
11
|
## 1.3.0
|
|
4
12
|
|
|
5
13
|
- Model roles (`/server` and `/vue`, browser-safe): `MODEL_ROLE_KINDS` (`chat`, `decision`, `image`, `tts`, `stt`), `ModelRoleDefinition`, `STANDARD_MODEL_ROLES` (`decision`, `image`, `tts`, `stt`), `validateModelRoleAssignments` (issues keyed by role id), `resolveModelRole` and `modelRoleAliases` (feeds `resolveRegistryLanguageModel({ roles })`). A dangling assignment throws `ModelResolutionError` (`unknown_selector`). omp naming: `decision` = `judge`, `tts` = `speech`, `stt` = `dictation`.
|
package/README.md
CHANGED
|
@@ -86,6 +86,8 @@ The panel (`PUT|DELETE <writeUrl>/:id`, `GET <writeUrl>/detected`, `GET <provide
|
|
|
86
86
|
|
|
87
87
|
Without URLs, pass `:client` (a `ProvidersPanelClient`: `list`, `detected`, `presets`, `upsert`, `remove`, `environment`, `updateEnvironment`, `models`, `test`) and the panel talks through it; the URL props then become optional. A passed client also turns on the per-provider Test button. `urlProvidersClient({ providersUrl, providersWriteUrl, … })` is the client the URL props build; its `models(id)` and `test(id)` call `GET|POST <writeUrl>/:id/models|test`, routes only a host that wants those features serves.
|
|
88
88
|
|
|
89
|
+
`note` sets the line above the list: absent keeps the panel's text for `layer`, a string replaces it, `false` hides it (the Add provider button stays, right-aligned).
|
|
90
|
+
|
|
89
91
|
## Model roles
|
|
90
92
|
|
|
91
93
|
A *model role* is a named purpose a host picks a model for. The host declares roles and stores the assignments (role id to selector); this package supplies the vocabulary, validation, resolution and a picker.
|
package/dist/module.json
CHANGED
|
@@ -20,6 +20,8 @@ type __VLS_Props = {
|
|
|
20
20
|
layer: ProvidersPanelLayer;
|
|
21
21
|
/** Disables add, edit and delete. */
|
|
22
22
|
readOnly?: boolean;
|
|
23
|
+
/** The line above the list. Absent: the default text for `layer`; a string replaces it; `false` hides it. */
|
|
24
|
+
note?: string | false;
|
|
23
25
|
};
|
|
24
26
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
25
27
|
changed: () => any;
|
|
@@ -27,5 +29,6 @@ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {
|
|
|
27
29
|
onChanged?: (() => any) | undefined;
|
|
28
30
|
}>, {
|
|
29
31
|
environmentEnabled: boolean;
|
|
32
|
+
note: string | false;
|
|
30
33
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
31
34
|
export default _default;
|
|
@@ -29,10 +29,9 @@
|
|
|
29
29
|
<p v-if="detectedError" class="text-xs text-error-500">{{ detectedError }}</p>
|
|
30
30
|
</div>
|
|
31
31
|
|
|
32
|
-
<div class="flex items-center gap-2">
|
|
33
|
-
<p class="text-xs text-foreground-muted leading-relaxed flex-1">
|
|
34
|
-
{{
|
|
35
|
-
Keys stay in the environment; an entry only references them.
|
|
32
|
+
<div class="flex items-center gap-2" :class="{ 'justify-end': noteText === null }">
|
|
33
|
+
<p v-if="noteText !== null" class="text-xs text-foreground-muted leading-relaxed flex-1">
|
|
34
|
+
{{ noteText }}
|
|
36
35
|
</p>
|
|
37
36
|
<UButton
|
|
38
37
|
v-if="!props.readOnly"
|
|
@@ -53,191 +52,176 @@
|
|
|
53
52
|
This registry has no providers yet.
|
|
54
53
|
</div>
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
v-if="keyEditing === row.id"
|
|
110
|
-
:data-testid="`provider-credential-enter-key-${row.id}`"
|
|
111
|
-
class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
|
|
55
|
+
<!-- One stacked row per provider: fits a settings column; a table squeezed the base URL and clipped the actions. -->
|
|
56
|
+
<ul v-else class="rounded-lg border border-surface-border divide-y divide-surface-border text-xs">
|
|
57
|
+
<li
|
|
58
|
+
v-for="row in rows"
|
|
59
|
+
:key="row.id"
|
|
60
|
+
:data-testid="`provider-row-${row.id}`"
|
|
61
|
+
class="p-3 space-y-2"
|
|
62
|
+
:class="row.enabled ? '' : 'opacity-60'"
|
|
63
|
+
>
|
|
64
|
+
<div class="flex items-start gap-3">
|
|
65
|
+
<div class="flex-1 min-w-0">
|
|
66
|
+
<div class="flex items-center gap-1.5 flex-wrap">
|
|
67
|
+
<span class="text-sm font-medium text-foreground">{{ row.name }}</span>
|
|
68
|
+
<UBadge
|
|
69
|
+
:data-testid="`provider-badge-${row.id}`"
|
|
70
|
+
size="sm"
|
|
71
|
+
variant="subtle"
|
|
72
|
+
color="neutral"
|
|
73
|
+
>{{ row.badge }}</UBadge>
|
|
74
|
+
<Icon v-if="row.locked" name="lucide:lock" class="text-foreground-subtle shrink-0" />
|
|
75
|
+
</div>
|
|
76
|
+
<p class="font-mono text-foreground-subtle truncate" :title="row.baseUrl ?? void 0">{{ rowMeta(row) }}</p>
|
|
77
|
+
<p v-if="row.inheritedNote" class="text-foreground-subtle italic">{{ row.inheritedNote }}</p>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="flex items-center gap-1 shrink-0">
|
|
80
|
+
<UButton
|
|
81
|
+
v-if="canTest"
|
|
82
|
+
size="sm"
|
|
83
|
+
variant="ghost"
|
|
84
|
+
:loading="testBusy === row.id"
|
|
85
|
+
:disabled="testBusy !== null"
|
|
86
|
+
:data-testid="`provider-test-${row.id}`"
|
|
87
|
+
@click="testProvider(row)"
|
|
88
|
+
>
|
|
89
|
+
<Icon name="lucide:plug-zap" class="mr-1.5 w-3.5 h-3.5" />Test
|
|
90
|
+
</UButton>
|
|
91
|
+
<UButton
|
|
92
|
+
v-if="row.canReset"
|
|
93
|
+
size="sm"
|
|
94
|
+
variant="ghost"
|
|
95
|
+
:loading="rowBusy === row.id"
|
|
96
|
+
:data-testid="`provider-reset-${row.id}`"
|
|
97
|
+
@click="resetPatch(row)"
|
|
98
|
+
>
|
|
99
|
+
<Icon name="lucide:undo-2" class="mr-1.5 w-3.5 h-3.5" />Reset
|
|
100
|
+
</UButton>
|
|
101
|
+
<template v-if="row.editable">
|
|
102
|
+
<UButton
|
|
103
|
+
size="sm"
|
|
104
|
+
variant="ghost"
|
|
105
|
+
:aria-label="`Edit ${row.id}`"
|
|
106
|
+
:data-testid="`provider-edit-${row.id}`"
|
|
107
|
+
@click="openEdit(row)"
|
|
112
108
|
>
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
autocomplete="off"
|
|
123
|
-
placeholder="Key"
|
|
124
|
-
:data-testid="`provider-key-value-${row.id}`"
|
|
125
|
-
class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
|
|
126
|
-
/>
|
|
127
|
-
<p v-if="keyError" class="text-error-500">{{ keyError }}</p>
|
|
128
|
-
<div class="flex justify-end gap-1">
|
|
129
|
-
<UButton size="xs" variant="ghost" :data-testid="`provider-key-cancel-${row.id}`" @click="cancelKeyEditor()">Cancel</UButton>
|
|
130
|
-
<UButton
|
|
131
|
-
size="xs"
|
|
132
|
-
color="primary"
|
|
133
|
-
:loading="rowBusy === row.id"
|
|
134
|
-
:data-testid="`provider-key-save-${row.id}`"
|
|
135
|
-
@click="saveKeyEditor()"
|
|
136
|
-
>Save</UButton>
|
|
137
|
-
</div>
|
|
138
|
-
</div>
|
|
139
|
-
|
|
140
|
-
<!-- Inline reference editor -->
|
|
141
|
-
<div
|
|
142
|
-
v-if="refEditing === row.id"
|
|
143
|
-
:data-testid="`provider-credential-ref-${row.id}`"
|
|
144
|
-
class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
|
|
109
|
+
<Icon name="lucide:pencil" class="w-3.5 h-3.5" />
|
|
110
|
+
</UButton>
|
|
111
|
+
<UButton
|
|
112
|
+
size="sm"
|
|
113
|
+
variant="ghost"
|
|
114
|
+
color="error"
|
|
115
|
+
:aria-label="`Delete ${row.id}`"
|
|
116
|
+
:data-testid="`provider-delete-${row.id}`"
|
|
117
|
+
@click="requestDelete(row)"
|
|
145
118
|
>
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
:data-testid="`provider-ref-save-${row.id}`"
|
|
160
|
-
@click="saveRefEditor()"
|
|
161
|
-
>Save</UButton>
|
|
162
|
-
</div>
|
|
163
|
-
</div>
|
|
119
|
+
<Icon name="lucide:trash-2" class="w-3.5 h-3.5" />
|
|
120
|
+
</UButton>
|
|
121
|
+
</template>
|
|
122
|
+
<USwitch
|
|
123
|
+
:model-value="row.enabled"
|
|
124
|
+
:disabled="!row.canToggle || rowBusy === row.id"
|
|
125
|
+
:aria-label="`Enable ${row.id}`"
|
|
126
|
+
:data-testid="`provider-enabled-${row.id}`"
|
|
127
|
+
class="ml-1"
|
|
128
|
+
@update:model-value="setEnabled(row, Boolean($event))"
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
164
132
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
133
|
+
<div class="space-y-1 max-w-md">
|
|
134
|
+
<USelect
|
|
135
|
+
v-if="row.needsCredential"
|
|
136
|
+
:model-value="row.credentialValue"
|
|
137
|
+
:items="row.credentialOptions"
|
|
138
|
+
placeholder="Choose a credential"
|
|
139
|
+
:disabled="!row.canEditCredential || rowBusy === row.id"
|
|
140
|
+
:data-testid="`provider-credential-${row.id}`"
|
|
141
|
+
class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
|
|
142
|
+
@update:model-value="onCredentialSelect(row, String($event ?? ''))"
|
|
143
|
+
/>
|
|
144
|
+
<p :class="row.configured ? 'text-foreground-subtle' : 'text-amber-600 dark:text-amber-400'" class="italic">
|
|
145
|
+
{{ row.status }}
|
|
146
|
+
</p>
|
|
147
|
+
|
|
148
|
+
<!-- Inline key editor -->
|
|
149
|
+
<div
|
|
150
|
+
v-if="keyEditing === row.id"
|
|
151
|
+
:data-testid="`provider-credential-enter-key-${row.id}`"
|
|
152
|
+
class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
|
|
153
|
+
>
|
|
154
|
+
<UInput
|
|
155
|
+
v-model="keyForm.name"
|
|
156
|
+
placeholder="PROVIDER_API_KEY"
|
|
157
|
+
:data-testid="`provider-key-name-${row.id}`"
|
|
158
|
+
class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
|
|
159
|
+
/>
|
|
160
|
+
<UInput
|
|
161
|
+
v-model="keyForm.value"
|
|
162
|
+
type="password"
|
|
163
|
+
autocomplete="off"
|
|
164
|
+
placeholder="Key"
|
|
165
|
+
:data-testid="`provider-key-value-${row.id}`"
|
|
166
|
+
class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
|
|
167
|
+
/>
|
|
168
|
+
<p v-if="keyError" class="text-error-500">{{ keyError }}</p>
|
|
169
|
+
<div class="flex justify-end gap-1">
|
|
170
|
+
<UButton size="xs" variant="ghost" :data-testid="`provider-key-cancel-${row.id}`" @click="cancelKeyEditor()">Cancel</UButton>
|
|
171
|
+
<UButton
|
|
172
|
+
size="xs"
|
|
173
|
+
color="primary"
|
|
174
|
+
:loading="rowBusy === row.id"
|
|
175
|
+
:data-testid="`provider-key-save-${row.id}`"
|
|
176
|
+
@click="saveKeyEditor()"
|
|
177
|
+
>Save</UButton>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<!-- Inline reference editor -->
|
|
182
|
+
<div
|
|
183
|
+
v-if="refEditing === row.id"
|
|
184
|
+
:data-testid="`provider-credential-ref-${row.id}`"
|
|
185
|
+
class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
|
|
186
|
+
>
|
|
187
|
+
<UInput
|
|
188
|
+
v-model="refForm.ref"
|
|
189
|
+
placeholder="keyring:forge/name · cmd:… · platform:…"
|
|
190
|
+
:data-testid="`provider-ref-input-${row.id}`"
|
|
191
|
+
class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
|
|
192
|
+
/>
|
|
193
|
+
<p v-if="refError" class="text-error-500">{{ refError }}</p>
|
|
194
|
+
<div class="flex justify-end gap-1">
|
|
195
|
+
<UButton size="xs" variant="ghost" :data-testid="`provider-ref-cancel-${row.id}`" @click="cancelRefEditor()">Cancel</UButton>
|
|
196
|
+
<UButton
|
|
197
|
+
size="xs"
|
|
198
|
+
color="primary"
|
|
199
|
+
:loading="rowBusy === row.id"
|
|
200
|
+
:data-testid="`provider-ref-save-${row.id}`"
|
|
201
|
+
@click="saveRefEditor()"
|
|
202
|
+
>Save</UButton>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<p
|
|
207
|
+
v-for="notice in row.overrideNotices"
|
|
208
|
+
:key="notice"
|
|
209
|
+
:data-testid="`provider-override-notice-${row.id}`"
|
|
210
|
+
class="text-foreground-subtle"
|
|
211
|
+
>{{ notice }}</p>
|
|
212
|
+
<p
|
|
213
|
+
v-if="testResults[row.id]"
|
|
214
|
+
:data-testid="`provider-test-result-${row.id}`"
|
|
215
|
+
:class="testResults[row.id]?.ok ? 'text-success-500' : 'text-error-500'"
|
|
216
|
+
>{{ testResults[row.id]?.message }}</p>
|
|
217
|
+
<p
|
|
218
|
+
v-if="rowError?.id === row.id"
|
|
219
|
+
:data-testid="`provider-row-error-${row.id}`"
|
|
220
|
+
class="text-error-500"
|
|
221
|
+
>{{ rowError.message }}</p>
|
|
222
|
+
</div>
|
|
223
|
+
</li>
|
|
224
|
+
</ul>
|
|
241
225
|
|
|
242
226
|
<!-- Add / edit dialog -->
|
|
243
227
|
<UModal
|
|
@@ -390,6 +374,8 @@
|
|
|
390
374
|
</template>
|
|
391
375
|
|
|
392
376
|
<script setup>
|
|
377
|
+
import { computed } from "vue";
|
|
378
|
+
import { providersPanelNote, providersPanelRowMeta as rowMeta } from "../vue/providers-panel-model";
|
|
393
379
|
import { useProvidersPanel } from "../vue/useProvidersPanel";
|
|
394
380
|
const props = defineProps({
|
|
395
381
|
client: { type: Object, required: false },
|
|
@@ -399,8 +385,10 @@ const props = defineProps({
|
|
|
399
385
|
environmentUrl: { type: String, required: false },
|
|
400
386
|
environmentEnabled: { type: Boolean, required: false, default: void 0 },
|
|
401
387
|
layer: { type: String, required: true },
|
|
402
|
-
readOnly: { type: Boolean, required: false }
|
|
388
|
+
readOnly: { type: Boolean, required: false },
|
|
389
|
+
note: { type: [String, Boolean], required: false, default: void 0 }
|
|
403
390
|
});
|
|
391
|
+
const noteText = computed(() => providersPanelNote(props.note, props.layer));
|
|
404
392
|
const emit = defineEmits(["changed"]);
|
|
405
393
|
const {
|
|
406
394
|
rows,
|
|
@@ -20,6 +20,8 @@ type __VLS_Props = {
|
|
|
20
20
|
layer: ProvidersPanelLayer;
|
|
21
21
|
/** Disables add, edit and delete. */
|
|
22
22
|
readOnly?: boolean;
|
|
23
|
+
/** The line above the list. Absent: the default text for `layer`; a string replaces it; `false` hides it. */
|
|
24
|
+
note?: string | false;
|
|
23
25
|
};
|
|
24
26
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
25
27
|
changed: () => any;
|
|
@@ -27,5 +29,6 @@ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {
|
|
|
27
29
|
onChanged?: (() => any) | undefined;
|
|
28
30
|
}>, {
|
|
29
31
|
environmentEnabled: boolean;
|
|
32
|
+
note: string | false;
|
|
30
33
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
31
34
|
export default _default;
|
|
@@ -38,6 +38,11 @@ export interface ProvidersPanelProps {
|
|
|
38
38
|
layer: ProvidersPanelLayer;
|
|
39
39
|
/** Disables every write (non-admin at instance scope; provider lock). */
|
|
40
40
|
readOnly?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The line above the provider list. Absent: the panel's own text for
|
|
43
|
+
* `layer`; a string replaces it; `false` hides it (Add provider stays).
|
|
44
|
+
*/
|
|
45
|
+
note?: string | false;
|
|
41
46
|
}
|
|
42
47
|
/** Emits both variants declare. */
|
|
43
48
|
export type ProvidersPanelEmit = (event: "changed") => void;
|
|
@@ -142,6 +147,18 @@ export declare const CREDENTIAL_OTHER_REF = "__other_ref__";
|
|
|
142
147
|
/** Registry ids the write routes accept (`isSafeProviderId` upstream). */
|
|
143
148
|
export declare const PROVIDER_ID_PATTERN: RegExp;
|
|
144
149
|
export declare const DEFAULT_PRESETS_URL = "/api/providers/presets";
|
|
150
|
+
/** The line `ForgeProvidersPanel` shows above its list; `null` = none. */
|
|
151
|
+
export declare function providersPanelNote(note: string | false | undefined, layer: ProvidersPanelLayer): string | null;
|
|
152
|
+
/**
|
|
153
|
+
* The muted line under a provider's name: its id (when the name differs), its
|
|
154
|
+
* preset or surface and its base URL, without repeating a value.
|
|
155
|
+
*/
|
|
156
|
+
export declare function providersPanelRowMeta(row: {
|
|
157
|
+
id: string;
|
|
158
|
+
name: string;
|
|
159
|
+
origin: string;
|
|
160
|
+
baseUrl?: string | null;
|
|
161
|
+
}): string;
|
|
145
162
|
/** An `env:NAME` credential reference; group 1 is the variable name. */
|
|
146
163
|
export declare const ENV_REF_PATTERN: RegExp;
|
|
147
164
|
/** The credential kind a written reference gets when the row states none. */
|
|
@@ -3,6 +3,15 @@ export const CREDENTIAL_ENTER_KEY = "__enter_key__";
|
|
|
3
3
|
export const CREDENTIAL_OTHER_REF = "__other_ref__";
|
|
4
4
|
export const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
5
5
|
export const DEFAULT_PRESETS_URL = "/api/providers/presets";
|
|
6
|
+
export function providersPanelNote(note, layer) {
|
|
7
|
+
if (note === false) return null;
|
|
8
|
+
if (note !== void 0) return note;
|
|
9
|
+
return layer === "workspace" ? "Entries of this workspace's providers.yml can be changed here; instance entries are inherited. Keys stay in the environment; an entry only references them." : "The instance registry, $FORGE_HOME/.skaile/providers.yml. Keys stay in the environment; an entry only references them.";
|
|
10
|
+
}
|
|
11
|
+
export function providersPanelRowMeta(row) {
|
|
12
|
+
const parts = [row.name !== row.id ? row.id : null, row.origin, row.baseUrl ?? null];
|
|
13
|
+
return [...new Set(parts.filter((part) => !!part))].join(" \xB7 ");
|
|
14
|
+
}
|
|
6
15
|
export const ENV_REF_PATTERN = /^env:(.+)$/;
|
|
7
16
|
export const FALLBACK_CREDENTIAL_KIND = "api_key_header";
|
|
8
17
|
export const ENV_SOURCE_LABEL = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skaile/forge-settings",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "Provider and settings operations for Skaile forge apps: provider registry layers, credential store, env layers, model discovery, the providers panel, and editor / AI SDK model adapters.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "bun@1.3.9",
|
|
@@ -29,12 +29,9 @@
|
|
|
29
29
|
<p v-if="detectedError" class="text-xs text-error-500">{{ detectedError }}</p>
|
|
30
30
|
</div>
|
|
31
31
|
|
|
32
|
-
<div class="flex items-center gap-2">
|
|
33
|
-
<p class="text-xs text-foreground-muted leading-relaxed flex-1">
|
|
34
|
-
{{
|
|
35
|
-
? 'Entries of this workspace\'s providers.yml can be changed here; instance entries are inherited.'
|
|
36
|
-
: 'The instance registry, $FORGE_HOME/.skaile/providers.yml.' }}
|
|
37
|
-
Keys stay in the environment; an entry only references them.
|
|
32
|
+
<div class="flex items-center gap-2" :class="{ 'justify-end': noteText === null }">
|
|
33
|
+
<p v-if="noteText !== null" class="text-xs text-foreground-muted leading-relaxed flex-1">
|
|
34
|
+
{{ noteText }}
|
|
38
35
|
</p>
|
|
39
36
|
<UButton
|
|
40
37
|
v-if="!props.readOnly"
|
|
@@ -55,191 +52,176 @@
|
|
|
55
52
|
This registry has no providers yet.
|
|
56
53
|
</div>
|
|
57
54
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
v-if="keyEditing === row.id"
|
|
112
|
-
:data-testid="`provider-credential-enter-key-${row.id}`"
|
|
113
|
-
class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
|
|
55
|
+
<!-- One stacked row per provider: fits a settings column; a table squeezed the base URL and clipped the actions. -->
|
|
56
|
+
<ul v-else class="rounded-lg border border-surface-border divide-y divide-surface-border text-xs">
|
|
57
|
+
<li
|
|
58
|
+
v-for="row in rows"
|
|
59
|
+
:key="row.id"
|
|
60
|
+
:data-testid="`provider-row-${row.id}`"
|
|
61
|
+
class="p-3 space-y-2"
|
|
62
|
+
:class="row.enabled ? '' : 'opacity-60'"
|
|
63
|
+
>
|
|
64
|
+
<div class="flex items-start gap-3">
|
|
65
|
+
<div class="flex-1 min-w-0">
|
|
66
|
+
<div class="flex items-center gap-1.5 flex-wrap">
|
|
67
|
+
<span class="text-sm font-medium text-foreground">{{ row.name }}</span>
|
|
68
|
+
<UBadge
|
|
69
|
+
:data-testid="`provider-badge-${row.id}`"
|
|
70
|
+
size="sm"
|
|
71
|
+
variant="subtle"
|
|
72
|
+
color="neutral"
|
|
73
|
+
>{{ row.badge }}</UBadge>
|
|
74
|
+
<Icon v-if="row.locked" name="lucide:lock" class="text-foreground-subtle shrink-0" />
|
|
75
|
+
</div>
|
|
76
|
+
<p class="font-mono text-foreground-subtle truncate" :title="row.baseUrl ?? undefined">{{ rowMeta(row) }}</p>
|
|
77
|
+
<p v-if="row.inheritedNote" class="text-foreground-subtle italic">{{ row.inheritedNote }}</p>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="flex items-center gap-1 shrink-0">
|
|
80
|
+
<UButton
|
|
81
|
+
v-if="canTest"
|
|
82
|
+
size="sm"
|
|
83
|
+
variant="ghost"
|
|
84
|
+
:loading="testBusy === row.id"
|
|
85
|
+
:disabled="testBusy !== null"
|
|
86
|
+
:data-testid="`provider-test-${row.id}`"
|
|
87
|
+
@click="testProvider(row)"
|
|
88
|
+
>
|
|
89
|
+
<Icon name="lucide:plug-zap" class="mr-1.5 w-3.5 h-3.5" />Test
|
|
90
|
+
</UButton>
|
|
91
|
+
<UButton
|
|
92
|
+
v-if="row.canReset"
|
|
93
|
+
size="sm"
|
|
94
|
+
variant="ghost"
|
|
95
|
+
:loading="rowBusy === row.id"
|
|
96
|
+
:data-testid="`provider-reset-${row.id}`"
|
|
97
|
+
@click="resetPatch(row)"
|
|
98
|
+
>
|
|
99
|
+
<Icon name="lucide:undo-2" class="mr-1.5 w-3.5 h-3.5" />Reset
|
|
100
|
+
</UButton>
|
|
101
|
+
<template v-if="row.editable">
|
|
102
|
+
<UButton
|
|
103
|
+
size="sm"
|
|
104
|
+
variant="ghost"
|
|
105
|
+
:aria-label="`Edit ${row.id}`"
|
|
106
|
+
:data-testid="`provider-edit-${row.id}`"
|
|
107
|
+
@click="openEdit(row)"
|
|
114
108
|
>
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
autocomplete="off"
|
|
125
|
-
placeholder="Key"
|
|
126
|
-
:data-testid="`provider-key-value-${row.id}`"
|
|
127
|
-
class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
|
|
128
|
-
/>
|
|
129
|
-
<p v-if="keyError" class="text-error-500">{{ keyError }}</p>
|
|
130
|
-
<div class="flex justify-end gap-1">
|
|
131
|
-
<UButton size="xs" variant="ghost" :data-testid="`provider-key-cancel-${row.id}`" @click="cancelKeyEditor()">Cancel</UButton>
|
|
132
|
-
<UButton
|
|
133
|
-
size="xs"
|
|
134
|
-
color="primary"
|
|
135
|
-
:loading="rowBusy === row.id"
|
|
136
|
-
:data-testid="`provider-key-save-${row.id}`"
|
|
137
|
-
@click="saveKeyEditor()"
|
|
138
|
-
>Save</UButton>
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
|
-
|
|
142
|
-
<!-- Inline reference editor -->
|
|
143
|
-
<div
|
|
144
|
-
v-if="refEditing === row.id"
|
|
145
|
-
:data-testid="`provider-credential-ref-${row.id}`"
|
|
146
|
-
class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
|
|
109
|
+
<Icon name="lucide:pencil" class="w-3.5 h-3.5" />
|
|
110
|
+
</UButton>
|
|
111
|
+
<UButton
|
|
112
|
+
size="sm"
|
|
113
|
+
variant="ghost"
|
|
114
|
+
color="error"
|
|
115
|
+
:aria-label="`Delete ${row.id}`"
|
|
116
|
+
:data-testid="`provider-delete-${row.id}`"
|
|
117
|
+
@click="requestDelete(row)"
|
|
147
118
|
>
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
:data-testid="`provider-ref-save-${row.id}`"
|
|
162
|
-
@click="saveRefEditor()"
|
|
163
|
-
>Save</UButton>
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
119
|
+
<Icon name="lucide:trash-2" class="w-3.5 h-3.5" />
|
|
120
|
+
</UButton>
|
|
121
|
+
</template>
|
|
122
|
+
<USwitch
|
|
123
|
+
:model-value="row.enabled"
|
|
124
|
+
:disabled="!row.canToggle || rowBusy === row.id"
|
|
125
|
+
:aria-label="`Enable ${row.id}`"
|
|
126
|
+
:data-testid="`provider-enabled-${row.id}`"
|
|
127
|
+
class="ml-1"
|
|
128
|
+
@update:model-value="setEnabled(row, Boolean($event))"
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
166
132
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
133
|
+
<div class="space-y-1 max-w-md">
|
|
134
|
+
<USelect
|
|
135
|
+
v-if="row.needsCredential"
|
|
136
|
+
:model-value="row.credentialValue"
|
|
137
|
+
:items="row.credentialOptions"
|
|
138
|
+
placeholder="Choose a credential"
|
|
139
|
+
:disabled="!row.canEditCredential || rowBusy === row.id"
|
|
140
|
+
:data-testid="`provider-credential-${row.id}`"
|
|
141
|
+
class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
|
|
142
|
+
@update:model-value="onCredentialSelect(row, String($event ?? ''))"
|
|
143
|
+
/>
|
|
144
|
+
<p :class="row.configured ? 'text-foreground-subtle' : 'text-amber-600 dark:text-amber-400'" class="italic">
|
|
145
|
+
{{ row.status }}
|
|
146
|
+
</p>
|
|
147
|
+
|
|
148
|
+
<!-- Inline key editor -->
|
|
149
|
+
<div
|
|
150
|
+
v-if="keyEditing === row.id"
|
|
151
|
+
:data-testid="`provider-credential-enter-key-${row.id}`"
|
|
152
|
+
class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
|
|
153
|
+
>
|
|
154
|
+
<UInput
|
|
155
|
+
v-model="keyForm.name"
|
|
156
|
+
placeholder="PROVIDER_API_KEY"
|
|
157
|
+
:data-testid="`provider-key-name-${row.id}`"
|
|
158
|
+
class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
|
|
159
|
+
/>
|
|
160
|
+
<UInput
|
|
161
|
+
v-model="keyForm.value"
|
|
162
|
+
type="password"
|
|
163
|
+
autocomplete="off"
|
|
164
|
+
placeholder="Key"
|
|
165
|
+
:data-testid="`provider-key-value-${row.id}`"
|
|
166
|
+
class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
|
|
167
|
+
/>
|
|
168
|
+
<p v-if="keyError" class="text-error-500">{{ keyError }}</p>
|
|
169
|
+
<div class="flex justify-end gap-1">
|
|
170
|
+
<UButton size="xs" variant="ghost" :data-testid="`provider-key-cancel-${row.id}`" @click="cancelKeyEditor()">Cancel</UButton>
|
|
171
|
+
<UButton
|
|
172
|
+
size="xs"
|
|
173
|
+
color="primary"
|
|
174
|
+
:loading="rowBusy === row.id"
|
|
175
|
+
:data-testid="`provider-key-save-${row.id}`"
|
|
176
|
+
@click="saveKeyEditor()"
|
|
177
|
+
>Save</UButton>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<!-- Inline reference editor -->
|
|
182
|
+
<div
|
|
183
|
+
v-if="refEditing === row.id"
|
|
184
|
+
:data-testid="`provider-credential-ref-${row.id}`"
|
|
185
|
+
class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
|
|
186
|
+
>
|
|
187
|
+
<UInput
|
|
188
|
+
v-model="refForm.ref"
|
|
189
|
+
placeholder="keyring:forge/name · cmd:… · platform:…"
|
|
190
|
+
:data-testid="`provider-ref-input-${row.id}`"
|
|
191
|
+
class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
|
|
192
|
+
/>
|
|
193
|
+
<p v-if="refError" class="text-error-500">{{ refError }}</p>
|
|
194
|
+
<div class="flex justify-end gap-1">
|
|
195
|
+
<UButton size="xs" variant="ghost" :data-testid="`provider-ref-cancel-${row.id}`" @click="cancelRefEditor()">Cancel</UButton>
|
|
196
|
+
<UButton
|
|
197
|
+
size="xs"
|
|
198
|
+
color="primary"
|
|
199
|
+
:loading="rowBusy === row.id"
|
|
200
|
+
:data-testid="`provider-ref-save-${row.id}`"
|
|
201
|
+
@click="saveRefEditor()"
|
|
202
|
+
>Save</UButton>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<p
|
|
207
|
+
v-for="notice in row.overrideNotices"
|
|
208
|
+
:key="notice"
|
|
209
|
+
:data-testid="`provider-override-notice-${row.id}`"
|
|
210
|
+
class="text-foreground-subtle"
|
|
211
|
+
>{{ notice }}</p>
|
|
212
|
+
<p
|
|
213
|
+
v-if="testResults[row.id]"
|
|
214
|
+
:data-testid="`provider-test-result-${row.id}`"
|
|
215
|
+
:class="testResults[row.id]?.ok ? 'text-success-500' : 'text-error-500'"
|
|
216
|
+
>{{ testResults[row.id]?.message }}</p>
|
|
217
|
+
<p
|
|
218
|
+
v-if="rowError?.id === row.id"
|
|
219
|
+
:data-testid="`provider-row-error-${row.id}`"
|
|
220
|
+
class="text-error-500"
|
|
221
|
+
>{{ rowError.message }}</p>
|
|
222
|
+
</div>
|
|
223
|
+
</li>
|
|
224
|
+
</ul>
|
|
243
225
|
|
|
244
226
|
<!-- Add / edit dialog -->
|
|
245
227
|
<UModal
|
|
@@ -392,7 +374,9 @@
|
|
|
392
374
|
</template>
|
|
393
375
|
|
|
394
376
|
<script setup lang="ts">
|
|
377
|
+
import { computed } from "vue";
|
|
395
378
|
import type { ProvidersPanelClient } from "../vue/providers-client";
|
|
379
|
+
import { providersPanelNote, providersPanelRowMeta as rowMeta } from "../vue/providers-panel-model";
|
|
396
380
|
import { type ProvidersPanelLayer, useProvidersPanel } from "../vue/useProvidersPanel";
|
|
397
381
|
|
|
398
382
|
const props = withDefaults(
|
|
@@ -416,11 +400,15 @@ const props = withDefaults(
|
|
|
416
400
|
layer: ProvidersPanelLayer;
|
|
417
401
|
/** Disables add, edit and delete. */
|
|
418
402
|
readOnly?: boolean;
|
|
403
|
+
/** The line above the list. Absent: the default text for `layer`; a string replaces it; `false` hides it. */
|
|
404
|
+
note?: string | false;
|
|
419
405
|
}>(),
|
|
420
406
|
// Without a default Vue casts an absent boolean to `false`; the panel tells absent from `false`.
|
|
421
|
-
{ environmentEnabled: undefined },
|
|
407
|
+
{ environmentEnabled: undefined, note: undefined },
|
|
422
408
|
);
|
|
423
409
|
|
|
410
|
+
const noteText = computed(() => providersPanelNote(props.note, props.layer));
|
|
411
|
+
|
|
424
412
|
const emit = defineEmits<{
|
|
425
413
|
/** After every successful `PUT`/`DELETE`. */
|
|
426
414
|
(event: "changed"): void;
|
|
@@ -45,6 +45,11 @@ export interface ProvidersPanelProps {
|
|
|
45
45
|
layer: ProvidersPanelLayer;
|
|
46
46
|
/** Disables every write (non-admin at instance scope; provider lock). */
|
|
47
47
|
readOnly?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* The line above the provider list. Absent: the panel's own text for
|
|
50
|
+
* `layer`; a string replaces it; `false` hides it (Add provider stays).
|
|
51
|
+
*/
|
|
52
|
+
note?: string | false;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
/** Emits both variants declare. */
|
|
@@ -161,6 +166,32 @@ export const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
|
161
166
|
|
|
162
167
|
export const DEFAULT_PRESETS_URL = "/api/providers/presets";
|
|
163
168
|
|
|
169
|
+
/** The line `ForgeProvidersPanel` shows above its list; `null` = none. */
|
|
170
|
+
export function providersPanelNote(
|
|
171
|
+
note: string | false | undefined,
|
|
172
|
+
layer: ProvidersPanelLayer,
|
|
173
|
+
): string | null {
|
|
174
|
+
if (note === false) return null;
|
|
175
|
+
if (note !== undefined) return note;
|
|
176
|
+
return layer === "workspace"
|
|
177
|
+
? "Entries of this workspace's providers.yml can be changed here; instance entries are inherited. Keys stay in the environment; an entry only references them."
|
|
178
|
+
: "The instance registry, $FORGE_HOME/.skaile/providers.yml. Keys stay in the environment; an entry only references them.";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The muted line under a provider's name: its id (when the name differs), its
|
|
183
|
+
* preset or surface and its base URL, without repeating a value.
|
|
184
|
+
*/
|
|
185
|
+
export function providersPanelRowMeta(row: {
|
|
186
|
+
id: string;
|
|
187
|
+
name: string;
|
|
188
|
+
origin: string;
|
|
189
|
+
baseUrl?: string | null;
|
|
190
|
+
}): string {
|
|
191
|
+
const parts = [row.name !== row.id ? row.id : null, row.origin, row.baseUrl ?? null];
|
|
192
|
+
return [...new Set(parts.filter((part): part is string => !!part))].join(" · ");
|
|
193
|
+
}
|
|
194
|
+
|
|
164
195
|
/** An `env:NAME` credential reference; group 1 is the variable name. */
|
|
165
196
|
export const ENV_REF_PATTERN = /^env:(.+)$/;
|
|
166
197
|
|