@skaile/forge-settings 0.0.0-stage → 1.0.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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +94 -2
- package/dist/module.d.mts +17 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +23 -0
- package/dist/runtime/ai-sdk/index.d.ts +46 -0
- package/dist/runtime/ai-sdk/index.js +31 -0
- package/dist/runtime/components/ForgeProvidersPanel.d.vue.ts +21 -0
- package/dist/runtime/components/ForgeProvidersPanel.vue +435 -0
- package/dist/runtime/components/ForgeProvidersPanel.vue.d.ts +21 -0
- package/dist/runtime/editor/index.d.ts +58 -0
- package/dist/runtime/editor/index.js +72 -0
- package/dist/runtime/model/errors.d.ts +32 -0
- package/dist/runtime/model/errors.js +19 -0
- package/dist/runtime/server/credential-policy.d.ts +61 -0
- package/dist/runtime/server/credential-policy.js +65 -0
- package/dist/runtime/server/credential-store.d.ts +184 -0
- package/dist/runtime/server/credential-store.js +234 -0
- package/dist/runtime/server/env-layers.d.ts +62 -0
- package/dist/runtime/server/env-layers.js +183 -0
- package/dist/runtime/server/env.d.ts +30 -0
- package/dist/runtime/server/env.js +15 -0
- package/dist/runtime/server/harness-settings.d.ts +150 -0
- package/dist/runtime/server/harness-settings.js +166 -0
- package/dist/runtime/server/index.d.ts +23 -0
- package/dist/runtime/server/index.js +14 -0
- package/dist/runtime/server/provider-discovery.d.ts +73 -0
- package/dist/runtime/server/provider-discovery.js +77 -0
- package/dist/runtime/server/provider-layers.d.ts +27 -0
- package/dist/runtime/server/provider-layers.js +35 -0
- package/dist/runtime/server/provider-service.d.ts +126 -0
- package/dist/runtime/server/provider-service.js +87 -0
- package/dist/runtime/server/provider-settings.d.ts +78 -0
- package/dist/runtime/server/provider-settings.js +81 -0
- package/dist/runtime/server/provider-write.d.ts +154 -0
- package/dist/runtime/server/provider-write.js +221 -0
- package/dist/runtime/server/settings-defaults.d.ts +26 -0
- package/dist/runtime/server/settings-defaults.js +46 -0
- package/dist/runtime/shared/env-vars.d.ts +55 -0
- package/dist/runtime/shared/env-vars.js +22 -0
- package/dist/runtime/shared/guards.d.ts +2 -0
- package/dist/runtime/shared/guards.js +3 -0
- package/dist/runtime/shared/harness-providers.d.ts +62 -0
- package/dist/runtime/shared/harness-providers.js +38 -0
- package/dist/runtime/shared/harnesses.d.ts +19 -0
- package/dist/runtime/shared/harnesses.js +15 -0
- package/dist/runtime/shared/provider-registry.d.ts +255 -0
- package/dist/runtime/shared/provider-registry.js +112 -0
- package/dist/runtime/shared/settings-types.d.ts +84 -0
- package/dist/runtime/shared/settings-types.js +0 -0
- package/dist/runtime/shims-vue.d.ts +8 -0
- package/dist/runtime/vue/index.d.ts +14 -0
- package/dist/runtime/vue/index.js +6 -0
- package/dist/runtime/vue/providers-panel-model.d.ts +147 -0
- package/dist/runtime/vue/providers-panel-model.js +37 -0
- package/dist/runtime/vue/settings-modal-scope.d.ts +62 -0
- package/dist/runtime/vue/settings-modal-scope.js +2 -0
- package/dist/runtime/vue/useProvidersPanel.d.ts +115 -0
- package/dist/runtime/vue/useProvidersPanel.js +633 -0
- package/dist/types.d.mts +9 -0
- package/package.json +117 -4
- package/src/runtime/ai-sdk/index.ts +88 -0
- package/src/runtime/components/ForgeProvidersPanel.vue +449 -0
- package/src/runtime/editor/index.ts +146 -0
- package/src/runtime/model/errors.ts +52 -0
- package/src/runtime/server/credential-policy.ts +139 -0
- package/src/runtime/server/credential-store.ts +473 -0
- package/src/runtime/server/env-layers.ts +287 -0
- package/src/runtime/server/env.ts +46 -0
- package/src/runtime/server/harness-settings.ts +373 -0
- package/src/runtime/server/index.ts +24 -0
- package/src/runtime/server/provider-discovery.ts +164 -0
- package/src/runtime/server/provider-layers.ts +67 -0
- package/src/runtime/server/provider-service.ts +244 -0
- package/src/runtime/server/provider-settings.ts +192 -0
- package/src/runtime/server/provider-write.ts +433 -0
- package/src/runtime/server/settings-defaults.ts +87 -0
- package/src/runtime/shared/env-vars.ts +82 -0
- package/src/runtime/shared/guards.ts +4 -0
- package/src/runtime/shared/harness-providers.ts +118 -0
- package/src/runtime/shared/harnesses.ts +34 -0
- package/src/runtime/shared/provider-registry.ts +391 -0
- package/src/runtime/shared/settings-types.ts +87 -0
- package/src/runtime/shims-vue.d.ts +8 -0
- package/src/runtime/vue/index.ts +15 -0
- package/src/runtime/vue/providers-panel-model.ts +203 -0
- package/src/runtime/vue/settings-modal-scope.ts +66 -0
- package/src/runtime/vue/useProvidersPanel.ts +930 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
import { $fetch } from "ofetch";
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
onMounted,
|
|
5
|
+
reactive,
|
|
6
|
+
ref,
|
|
7
|
+
watch
|
|
8
|
+
} from "vue";
|
|
9
|
+
import {
|
|
10
|
+
validateEnvVarName
|
|
11
|
+
} from "../shared/env-vars.js";
|
|
12
|
+
import {
|
|
13
|
+
AI_PROVIDER_SURFACES,
|
|
14
|
+
validateCredentialRef
|
|
15
|
+
} from "../shared/provider-registry.js";
|
|
16
|
+
import {
|
|
17
|
+
CREDENTIAL_ENTER_KEY,
|
|
18
|
+
CREDENTIAL_OTHER_REF,
|
|
19
|
+
CUSTOM_PRESET,
|
|
20
|
+
DEFAULT_PRESETS_URL,
|
|
21
|
+
EMPTY_FORM,
|
|
22
|
+
ENV_SOURCE_LABEL,
|
|
23
|
+
envNameOfRef,
|
|
24
|
+
FALLBACK_CREDENTIAL_KIND,
|
|
25
|
+
nonEnvSourceLabel,
|
|
26
|
+
OVERRIDES_INSTANCE_NOTICE,
|
|
27
|
+
PROVIDER_ID_PATTERN,
|
|
28
|
+
requestErrorMessage
|
|
29
|
+
} from "./providers-panel-model.js";
|
|
30
|
+
export { CREDENTIAL_ENTER_KEY, CREDENTIAL_OTHER_REF, CUSTOM_PRESET } from "./providers-panel-model.js";
|
|
31
|
+
export function useProvidersPanel(props, emit) {
|
|
32
|
+
const providers = ref([]);
|
|
33
|
+
const presets = ref([]);
|
|
34
|
+
const detected = ref([]);
|
|
35
|
+
const envDocument = ref(null);
|
|
36
|
+
const loading = ref(false);
|
|
37
|
+
const loadError = ref(null);
|
|
38
|
+
const envError = ref(null);
|
|
39
|
+
async function loadEnvironment() {
|
|
40
|
+
if (!props.environmentUrl) {
|
|
41
|
+
envDocument.value = null;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
envDocument.value = await $fetch(props.environmentUrl);
|
|
46
|
+
envError.value = null;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
envError.value = requestErrorMessage(err, "Could not load the environment variables.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function refresh() {
|
|
52
|
+
loading.value = true;
|
|
53
|
+
loadError.value = null;
|
|
54
|
+
try {
|
|
55
|
+
const [list, found] = await Promise.all([
|
|
56
|
+
$fetch(props.providersUrl, { query: { all: "1" } }),
|
|
57
|
+
$fetch(`${props.providersWriteUrl}/detected`),
|
|
58
|
+
loadEnvironment()
|
|
59
|
+
]);
|
|
60
|
+
providers.value = Array.isArray(list) ? list : [];
|
|
61
|
+
detected.value = Array.isArray(found) ? found : [];
|
|
62
|
+
} catch (err) {
|
|
63
|
+
loadError.value = requestErrorMessage(err, "Could not load the providers.");
|
|
64
|
+
} finally {
|
|
65
|
+
loading.value = false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function loadPresets() {
|
|
69
|
+
try {
|
|
70
|
+
const list = await $fetch(
|
|
71
|
+
props.presetsUrl ?? DEFAULT_PRESETS_URL
|
|
72
|
+
);
|
|
73
|
+
presets.value = Array.isArray(list) ? list : [];
|
|
74
|
+
} catch (err) {
|
|
75
|
+
loadError.value = requestErrorMessage(err, "Could not load the provider presets.");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function afterWrite() {
|
|
79
|
+
await refresh();
|
|
80
|
+
emit("changed");
|
|
81
|
+
}
|
|
82
|
+
onMounted(() => {
|
|
83
|
+
void loadPresets();
|
|
84
|
+
void refresh();
|
|
85
|
+
});
|
|
86
|
+
watch(
|
|
87
|
+
() => [props.providersUrl, props.providersWriteUrl, props.environmentUrl],
|
|
88
|
+
() => void refresh()
|
|
89
|
+
);
|
|
90
|
+
const envVars = computed(() => {
|
|
91
|
+
const byName = /* @__PURE__ */ new Map();
|
|
92
|
+
for (const v of envDocument.value?.vars ?? []) byName.set(v.name, v);
|
|
93
|
+
return byName;
|
|
94
|
+
});
|
|
95
|
+
const envEditable = computed(() => envDocument.value?.editable === true);
|
|
96
|
+
const canEnterKey = computed(
|
|
97
|
+
() => Boolean(props.environmentUrl) && envDocument.value?.editable !== false
|
|
98
|
+
);
|
|
99
|
+
function stateOf(entry) {
|
|
100
|
+
if (props.layer !== "workspace" || !entry.layers.includes("instance")) return "own";
|
|
101
|
+
return entry.layers.includes("workspace") ? "patched" : "inherited";
|
|
102
|
+
}
|
|
103
|
+
function isEditable(entry) {
|
|
104
|
+
return !props.readOnly && !entry.locked && stateOf(entry) === "own";
|
|
105
|
+
}
|
|
106
|
+
function credentialOptionsOf(entry) {
|
|
107
|
+
const doc = envDocument.value;
|
|
108
|
+
const currentEnv = envNameOfRef(entry.credentialRef);
|
|
109
|
+
const names = /* @__PURE__ */ new Set();
|
|
110
|
+
for (const v of doc?.vars ?? []) names.add(v.name);
|
|
111
|
+
for (const m of doc?.missing ?? []) names.add(m.name);
|
|
112
|
+
const rowPresetEnv = presets.value.find((p) => p.id === entry.preset)?.credential.env;
|
|
113
|
+
if (rowPresetEnv) names.add(rowPresetEnv);
|
|
114
|
+
for (const p of detected.value) if (p.credential.env) names.add(p.credential.env);
|
|
115
|
+
if (currentEnv) names.add(currentEnv);
|
|
116
|
+
const options = [...names].sort().map((name) => {
|
|
117
|
+
const v = envVars.value.get(name);
|
|
118
|
+
if (!doc) return { value: name, label: name };
|
|
119
|
+
if (!v) return { value: name, label: `${name} (not set)`, defined: false };
|
|
120
|
+
return {
|
|
121
|
+
value: name,
|
|
122
|
+
label: v.hint ? `${name} \u2022\u2022\u2022\u2022${v.hint}` : name,
|
|
123
|
+
defined: true,
|
|
124
|
+
hint: v.hint,
|
|
125
|
+
source: v.source
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
if (entry.credentialRef && !currentEnv) {
|
|
129
|
+
options.push({ value: entry.credentialRef, label: entry.credentialRef });
|
|
130
|
+
}
|
|
131
|
+
if (canEnterKey.value) {
|
|
132
|
+
options.push({ value: CREDENTIAL_ENTER_KEY, label: "Enter a key\u2026", action: "enter-key" });
|
|
133
|
+
}
|
|
134
|
+
options.push({ value: CREDENTIAL_OTHER_REF, label: "Other reference\u2026", action: "other-ref" });
|
|
135
|
+
return options;
|
|
136
|
+
}
|
|
137
|
+
function statusOf(entry, needsCredential) {
|
|
138
|
+
if (!needsCredential) return "No credential needed.";
|
|
139
|
+
const ref2 = entry.credentialRef;
|
|
140
|
+
const envName = envNameOfRef(ref2);
|
|
141
|
+
if (entry.configured) {
|
|
142
|
+
const source = envName ? (() => {
|
|
143
|
+
const v = envVars.value.get(envName);
|
|
144
|
+
return v ? ENV_SOURCE_LABEL[v.source] : void 0;
|
|
145
|
+
})() : ref2 ? nonEnvSourceLabel(ref2) : void 0;
|
|
146
|
+
return source ? `configured \xB7 ${source}` : "configured";
|
|
147
|
+
}
|
|
148
|
+
const missing = envName ?? entry.envKeyName;
|
|
149
|
+
return missing ? `missing ${missing}` : entry.error ?? "not configured";
|
|
150
|
+
}
|
|
151
|
+
function overrideNoticesOf(entry) {
|
|
152
|
+
const notices = [];
|
|
153
|
+
const envName = envNameOfRef(entry.credentialRef);
|
|
154
|
+
const v = envName ? envVars.value.get(envName) : void 0;
|
|
155
|
+
if (v && v.shadows.length > 0) {
|
|
156
|
+
notices.push(`Also defined in ${v.shadows.join(", ")}; the ${v.source} value is used.`);
|
|
157
|
+
}
|
|
158
|
+
if (v && props.layer === "workspace" && (v.source === "instance" || v.source === "process")) {
|
|
159
|
+
notices.push(OVERRIDES_INSTANCE_NOTICE);
|
|
160
|
+
}
|
|
161
|
+
if (entry.locked) notices.push("Locked by the control plane.");
|
|
162
|
+
return notices;
|
|
163
|
+
}
|
|
164
|
+
function buildRow(entry) {
|
|
165
|
+
const state = stateOf(entry);
|
|
166
|
+
const writable = !props.readOnly && !entry.locked;
|
|
167
|
+
const needsCredential = entry.credentialKind !== "none";
|
|
168
|
+
const envName = envNameOfRef(entry.credentialRef);
|
|
169
|
+
return {
|
|
170
|
+
...entry,
|
|
171
|
+
state,
|
|
172
|
+
badge: state === "patched" ? "instance \xB7 overridden here" : state === "inherited" ? "instance" : props.layer,
|
|
173
|
+
inheritedNote: state === "inherited" ? "Inherited from the instance." : void 0,
|
|
174
|
+
editable: writable && state === "own",
|
|
175
|
+
canReset: writable && state === "patched",
|
|
176
|
+
canToggle: writable,
|
|
177
|
+
needsCredential,
|
|
178
|
+
canEditCredential: needsCredential && writable,
|
|
179
|
+
credentialValue: envName ?? entry.credentialRef ?? "",
|
|
180
|
+
credentialOptions: needsCredential ? credentialOptionsOf(entry) : [],
|
|
181
|
+
origin: entry.preset ?? entry.surface ?? entry.kind,
|
|
182
|
+
status: statusOf(entry, needsCredential),
|
|
183
|
+
overrideNotices: overrideNoticesOf(entry)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const rows = computed(() => providers.value.map(buildRow));
|
|
187
|
+
const dialogVisible = ref(false);
|
|
188
|
+
const editing = ref(null);
|
|
189
|
+
const form = reactive({ ...EMPTY_FORM });
|
|
190
|
+
const dialogError = ref(null);
|
|
191
|
+
const saving = ref(false);
|
|
192
|
+
const presetOptions = computed(() => [
|
|
193
|
+
...presets.value.map((preset) => ({ label: preset.label, value: preset.id })),
|
|
194
|
+
{ label: "Custom", value: CUSTOM_PRESET }
|
|
195
|
+
]);
|
|
196
|
+
const surfaceOptions = AI_PROVIDER_SURFACES.map((surface) => ({
|
|
197
|
+
label: surface,
|
|
198
|
+
value: surface
|
|
199
|
+
}));
|
|
200
|
+
const selectedPreset = computed(() => presets.value.find((preset) => preset.id === form.preset));
|
|
201
|
+
const isCustom = computed(() => form.preset === CUSTOM_PRESET);
|
|
202
|
+
const customKind = computed(() => editing.value?.kind ?? "llm");
|
|
203
|
+
const showSurface = computed(
|
|
204
|
+
() => isCustom.value && (customKind.value === "llm" || customKind.value === "embed")
|
|
205
|
+
);
|
|
206
|
+
const baseUrlPlaceholder = computed(
|
|
207
|
+
() => selectedPreset.value?.base_url ?? "https://api.example.com/v1"
|
|
208
|
+
);
|
|
209
|
+
const canSetValue = computed(() => Boolean(props.environmentUrl));
|
|
210
|
+
const showCredentialFields = computed(() => editing.value === null);
|
|
211
|
+
const idError = computed(() => {
|
|
212
|
+
const id = form.id.trim();
|
|
213
|
+
return id && !PROVIDER_ID_PATTERN.test(id) ? "Use lower-case letters, digits, '-' and '_', starting with a letter or digit." : null;
|
|
214
|
+
});
|
|
215
|
+
const dialogTitle = computed(
|
|
216
|
+
() => editing.value ? `Edit provider \u2014 ${editing.value.id}` : "Add provider"
|
|
217
|
+
);
|
|
218
|
+
function resetDialog(values, row) {
|
|
219
|
+
Object.assign(form, EMPTY_FORM, values);
|
|
220
|
+
editing.value = row;
|
|
221
|
+
dialogError.value = null;
|
|
222
|
+
dialogVisible.value = true;
|
|
223
|
+
}
|
|
224
|
+
function openAdd(presetId) {
|
|
225
|
+
if (props.readOnly) return;
|
|
226
|
+
resetDialog({}, null);
|
|
227
|
+
if (presetId) selectPreset(presetId);
|
|
228
|
+
}
|
|
229
|
+
function openEdit(row) {
|
|
230
|
+
if (!isEditable(row)) return;
|
|
231
|
+
const preset = presets.value.find((p) => p.id === row.preset);
|
|
232
|
+
resetDialog(
|
|
233
|
+
{
|
|
234
|
+
preset: preset ? preset.id : CUSTOM_PRESET,
|
|
235
|
+
id: row.id,
|
|
236
|
+
surface: row.surface ?? "",
|
|
237
|
+
// The preset's own endpoint shows as the placeholder, not as a pinned value.
|
|
238
|
+
baseUrl: row.baseUrl && row.baseUrl !== preset?.base_url ? row.baseUrl : ""
|
|
239
|
+
},
|
|
240
|
+
row
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
function selectPreset(value) {
|
|
244
|
+
const previous = selectedPreset.value;
|
|
245
|
+
const next = presets.value.find((preset) => preset.id === value);
|
|
246
|
+
form.preset = value;
|
|
247
|
+
if (!editing.value && (form.id === "" || form.id === previous?.id)) form.id = next?.id ?? "";
|
|
248
|
+
if (form.envName === "" || form.envName === previous?.credential.env)
|
|
249
|
+
form.envName = next?.credential.env ?? "";
|
|
250
|
+
if (value === CUSTOM_PRESET && form.surface === "") form.surface = previous?.surface ?? "";
|
|
251
|
+
}
|
|
252
|
+
function formRefusal() {
|
|
253
|
+
if (!form.preset) return "Choose a preset or Custom.";
|
|
254
|
+
if (!form.id.trim()) return "Enter a provider id.";
|
|
255
|
+
if (idError.value) return idError.value;
|
|
256
|
+
if (showSurface.value && !form.surface) return "Choose the surface a custom provider speaks.";
|
|
257
|
+
if (isCustom.value && !form.baseUrl.trim()) return "A custom provider needs a base URL.";
|
|
258
|
+
if (showCredentialFields.value) {
|
|
259
|
+
const envName = form.envName.trim();
|
|
260
|
+
if (envName) {
|
|
261
|
+
const refusal = validateEnvVarName(envName);
|
|
262
|
+
if (refusal) return refusal;
|
|
263
|
+
}
|
|
264
|
+
if (form.secret && !envName) return "Name the environment variable the value is stored in.";
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
function buildBody() {
|
|
269
|
+
const row = editing.value;
|
|
270
|
+
const body = { ...row?.layerFields ?? {} };
|
|
271
|
+
const preset = selectedPreset.value;
|
|
272
|
+
const kept = row && form.preset === (row.preset ?? CUSTOM_PRESET) ? row : null;
|
|
273
|
+
if (isCustom.value) {
|
|
274
|
+
body.kind = customKind.value;
|
|
275
|
+
if (showSurface.value) body.surface = form.surface;
|
|
276
|
+
} else {
|
|
277
|
+
body.preset = form.preset;
|
|
278
|
+
if (kept?.surface && kept.surface !== preset?.surface) body.surface = kept.surface;
|
|
279
|
+
}
|
|
280
|
+
const baseUrl = form.baseUrl.trim();
|
|
281
|
+
if (baseUrl) body.base_url = baseUrl;
|
|
282
|
+
const credentialKind = kept?.credentialKind ?? (isCustom.value ? "bearer" : preset?.credential.kind ?? "bearer");
|
|
283
|
+
const envName = row ? "" : form.envName.trim();
|
|
284
|
+
if (envName) body.credential = { kind: credentialKind, ref: `env:${envName}` };
|
|
285
|
+
else if (row?.credentialRef) body.credential = { kind: credentialKind, ref: row.credentialRef };
|
|
286
|
+
return body;
|
|
287
|
+
}
|
|
288
|
+
async function saveProvider() {
|
|
289
|
+
if (props.readOnly) return;
|
|
290
|
+
const refusal = formRefusal();
|
|
291
|
+
if (refusal) {
|
|
292
|
+
dialogError.value = refusal;
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const id = form.id.trim();
|
|
296
|
+
saving.value = true;
|
|
297
|
+
dialogError.value = null;
|
|
298
|
+
try {
|
|
299
|
+
if (showCredentialFields.value && form.secret && props.environmentUrl) {
|
|
300
|
+
const update = {
|
|
301
|
+
set: { [form.envName.trim()]: form.secret },
|
|
302
|
+
unset: []
|
|
303
|
+
};
|
|
304
|
+
await $fetch(props.environmentUrl, { method: "PUT", body: update });
|
|
305
|
+
form.secret = "";
|
|
306
|
+
}
|
|
307
|
+
await $fetch(`${props.providersWriteUrl}/${encodeURIComponent(id)}`, {
|
|
308
|
+
method: "PUT",
|
|
309
|
+
body: buildBody()
|
|
310
|
+
});
|
|
311
|
+
dialogVisible.value = false;
|
|
312
|
+
editing.value = null;
|
|
313
|
+
await afterWrite();
|
|
314
|
+
} catch (err) {
|
|
315
|
+
dialogError.value = requestErrorMessage(err, "Could not save the provider.");
|
|
316
|
+
} finally {
|
|
317
|
+
saving.value = false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const deleteTarget = ref(null);
|
|
321
|
+
const deleteError = ref(null);
|
|
322
|
+
const deleting = ref(false);
|
|
323
|
+
const deleteOpen = computed({
|
|
324
|
+
get: () => deleteTarget.value !== null,
|
|
325
|
+
set: (open) => {
|
|
326
|
+
if (!open) deleteTarget.value = null;
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
function requestDelete(row) {
|
|
330
|
+
if (!isEditable(row)) return;
|
|
331
|
+
deleteError.value = null;
|
|
332
|
+
deleteTarget.value = row;
|
|
333
|
+
}
|
|
334
|
+
async function confirmDelete() {
|
|
335
|
+
const target = deleteTarget.value;
|
|
336
|
+
if (!target || props.readOnly) return;
|
|
337
|
+
deleting.value = true;
|
|
338
|
+
deleteError.value = null;
|
|
339
|
+
try {
|
|
340
|
+
await $fetch(`${props.providersWriteUrl}/${encodeURIComponent(target.id)}`, {
|
|
341
|
+
method: "DELETE"
|
|
342
|
+
});
|
|
343
|
+
deleteTarget.value = null;
|
|
344
|
+
await afterWrite();
|
|
345
|
+
} catch (err) {
|
|
346
|
+
deleteError.value = requestErrorMessage(err, `Could not delete ${target.id}.`);
|
|
347
|
+
} finally {
|
|
348
|
+
deleting.value = false;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const detectedBusy = ref(null);
|
|
352
|
+
const detectedError = ref(null);
|
|
353
|
+
async function addDetected(preset) {
|
|
354
|
+
if (props.readOnly) return;
|
|
355
|
+
const taken = new Set(providers.value.map((entry) => entry.id));
|
|
356
|
+
const id = [preset.id, `${preset.id}-env`].find((candidate) => !taken.has(candidate));
|
|
357
|
+
const envName = preset.credential.env;
|
|
358
|
+
if (!id || !envName) {
|
|
359
|
+
openAdd(preset.id);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
detectedBusy.value = preset.id;
|
|
363
|
+
detectedError.value = null;
|
|
364
|
+
try {
|
|
365
|
+
await $fetch(`${props.providersWriteUrl}/${encodeURIComponent(id)}`, {
|
|
366
|
+
method: "PUT",
|
|
367
|
+
body: {
|
|
368
|
+
preset: preset.id,
|
|
369
|
+
credential: { kind: preset.credential.kind, ref: `env:${envName}` }
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
await afterWrite();
|
|
373
|
+
} catch (err) {
|
|
374
|
+
detectedError.value = requestErrorMessage(err, `Could not add ${preset.label}.`);
|
|
375
|
+
} finally {
|
|
376
|
+
detectedBusy.value = null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const rowBusy = ref(null);
|
|
380
|
+
const rowError = ref(null);
|
|
381
|
+
function ownBody(row) {
|
|
382
|
+
const body = { ...row.layerFields ?? {} };
|
|
383
|
+
const preset = presets.value.find((p) => p.id === row.preset);
|
|
384
|
+
if (row.preset) {
|
|
385
|
+
body.preset = row.preset;
|
|
386
|
+
if (row.surface && row.surface !== preset?.surface) body.surface = row.surface;
|
|
387
|
+
} else {
|
|
388
|
+
body.kind = row.kind;
|
|
389
|
+
if (row.surface) body.surface = row.surface;
|
|
390
|
+
}
|
|
391
|
+
if (row.baseUrl && row.baseUrl !== preset?.base_url) body.base_url = row.baseUrl;
|
|
392
|
+
if (row.credentialRef) {
|
|
393
|
+
body.credential = {
|
|
394
|
+
kind: row.credentialKind ?? FALLBACK_CREDENTIAL_KIND,
|
|
395
|
+
ref: row.credentialRef
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return body;
|
|
399
|
+
}
|
|
400
|
+
async function putRowChange(row, change) {
|
|
401
|
+
const body = row.state === "own" ? { ...ownBody(row), ...change } : change;
|
|
402
|
+
await $fetch(`${props.providersWriteUrl}/${encodeURIComponent(row.id)}`, {
|
|
403
|
+
method: "PUT",
|
|
404
|
+
body
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
function credentialFor(row, ref2) {
|
|
408
|
+
return { kind: row.credentialKind ?? FALLBACK_CREDENTIAL_KIND, ref: ref2 };
|
|
409
|
+
}
|
|
410
|
+
async function runRowWrite(id, work, fallback) {
|
|
411
|
+
rowBusy.value = id;
|
|
412
|
+
rowError.value = null;
|
|
413
|
+
try {
|
|
414
|
+
await work();
|
|
415
|
+
await afterWrite();
|
|
416
|
+
} catch (err) {
|
|
417
|
+
rowError.value = { id, message: requestErrorMessage(err, fallback) };
|
|
418
|
+
} finally {
|
|
419
|
+
rowBusy.value = null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async function setCredentialEnv(row, name) {
|
|
423
|
+
if (!row.canEditCredential) return;
|
|
424
|
+
const ref2 = `env:${name}`;
|
|
425
|
+
if (row.credentialRef === ref2) return;
|
|
426
|
+
await runRowWrite(
|
|
427
|
+
row.id,
|
|
428
|
+
() => putRowChange(row, { credential: credentialFor(row, ref2) }),
|
|
429
|
+
`Could not change the credential of ${row.id}.`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
async function setEnabled(row, enabled) {
|
|
433
|
+
if (!row.canToggle) return;
|
|
434
|
+
await runRowWrite(
|
|
435
|
+
row.id,
|
|
436
|
+
() => putRowChange(row, { enabled }),
|
|
437
|
+
`Could not ${enabled ? "enable" : "disable"} ${row.id}.`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
async function resetPatch(row) {
|
|
441
|
+
if (!row.canReset) return;
|
|
442
|
+
await runRowWrite(
|
|
443
|
+
row.id,
|
|
444
|
+
async () => {
|
|
445
|
+
await $fetch(`${props.providersWriteUrl}/${encodeURIComponent(row.id)}`, {
|
|
446
|
+
method: "DELETE"
|
|
447
|
+
});
|
|
448
|
+
},
|
|
449
|
+
`Could not reset ${row.id}.`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
const keyEditing = ref(null);
|
|
453
|
+
const keyForm = reactive({ name: "", value: "" });
|
|
454
|
+
const keyError = ref(null);
|
|
455
|
+
const refEditing = ref(null);
|
|
456
|
+
const refForm = reactive({ ref: "" });
|
|
457
|
+
const refError = ref(null);
|
|
458
|
+
function cancelKeyEditor() {
|
|
459
|
+
keyEditing.value = null;
|
|
460
|
+
keyForm.name = "";
|
|
461
|
+
keyForm.value = "";
|
|
462
|
+
keyError.value = null;
|
|
463
|
+
}
|
|
464
|
+
function cancelRefEditor() {
|
|
465
|
+
refEditing.value = null;
|
|
466
|
+
refForm.ref = "";
|
|
467
|
+
refError.value = null;
|
|
468
|
+
}
|
|
469
|
+
function openKeyEditor(row) {
|
|
470
|
+
if (!row.canEditCredential || !canEnterKey.value) return;
|
|
471
|
+
cancelRefEditor();
|
|
472
|
+
cancelKeyEditor();
|
|
473
|
+
const presetEnv = presets.value.find((p) => p.id === row.preset)?.credential.env;
|
|
474
|
+
keyForm.name = envNameOfRef(row.credentialRef) ?? presetEnv ?? "";
|
|
475
|
+
keyEditing.value = row.id;
|
|
476
|
+
}
|
|
477
|
+
async function saveKeyEditor() {
|
|
478
|
+
const row = rows.value.find((r) => r.id === keyEditing.value);
|
|
479
|
+
if (!row?.canEditCredential || !props.environmentUrl) return;
|
|
480
|
+
const name = keyForm.name.trim();
|
|
481
|
+
const refusal = validateEnvVarName(name);
|
|
482
|
+
if (refusal) {
|
|
483
|
+
keyError.value = refusal;
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (!keyForm.value) {
|
|
487
|
+
keyError.value = "Enter the key.";
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
rowBusy.value = row.id;
|
|
491
|
+
keyError.value = null;
|
|
492
|
+
let wroteEnv = false;
|
|
493
|
+
try {
|
|
494
|
+
const update = { set: { [name]: keyForm.value }, unset: [] };
|
|
495
|
+
await $fetch(props.environmentUrl, { method: "PUT", body: update });
|
|
496
|
+
wroteEnv = true;
|
|
497
|
+
const ref2 = `env:${name}`;
|
|
498
|
+
if (row.credentialRef !== ref2) {
|
|
499
|
+
await putRowChange(row, { credential: credentialFor(row, ref2) });
|
|
500
|
+
}
|
|
501
|
+
cancelKeyEditor();
|
|
502
|
+
} catch (err) {
|
|
503
|
+
keyError.value = requestErrorMessage(err, "Could not save the key.");
|
|
504
|
+
} finally {
|
|
505
|
+
rowBusy.value = null;
|
|
506
|
+
}
|
|
507
|
+
if (wroteEnv) await afterWrite();
|
|
508
|
+
}
|
|
509
|
+
function openRefEditor(row) {
|
|
510
|
+
if (!row.canEditCredential) return;
|
|
511
|
+
cancelKeyEditor();
|
|
512
|
+
cancelRefEditor();
|
|
513
|
+
refForm.ref = row.credentialRef && !envNameOfRef(row.credentialRef) ? row.credentialRef : "";
|
|
514
|
+
refEditing.value = row.id;
|
|
515
|
+
}
|
|
516
|
+
async function saveRefEditor() {
|
|
517
|
+
const row = rows.value.find((r) => r.id === refEditing.value);
|
|
518
|
+
if (!row?.canEditCredential) return;
|
|
519
|
+
const ref2 = refForm.ref.trim();
|
|
520
|
+
const refusal = ref2 ? validateCredentialRef(ref2) : "Enter a reference.";
|
|
521
|
+
if (refusal) {
|
|
522
|
+
refError.value = refusal;
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
rowBusy.value = row.id;
|
|
526
|
+
refError.value = null;
|
|
527
|
+
try {
|
|
528
|
+
await putRowChange(row, { credential: credentialFor(row, ref2) });
|
|
529
|
+
cancelRefEditor();
|
|
530
|
+
await afterWrite();
|
|
531
|
+
} catch (err) {
|
|
532
|
+
refError.value = requestErrorMessage(err, "Could not save the reference.");
|
|
533
|
+
} finally {
|
|
534
|
+
rowBusy.value = null;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function onCredentialSelect(row, value) {
|
|
538
|
+
if (value === CREDENTIAL_ENTER_KEY) openKeyEditor(row);
|
|
539
|
+
else if (value === CREDENTIAL_OTHER_REF) openRefEditor(row);
|
|
540
|
+
else if (value !== row.credentialValue) void setCredentialEnv(row, value);
|
|
541
|
+
}
|
|
542
|
+
const envValues = reactive({});
|
|
543
|
+
const envBusy = ref(false);
|
|
544
|
+
async function writeEnvironment(update, fallback) {
|
|
545
|
+
if (!props.environmentUrl || !envEditable.value) return false;
|
|
546
|
+
envBusy.value = true;
|
|
547
|
+
envError.value = null;
|
|
548
|
+
try {
|
|
549
|
+
await $fetch(props.environmentUrl, { method: "PUT", body: update });
|
|
550
|
+
} catch (err) {
|
|
551
|
+
envError.value = requestErrorMessage(err, fallback);
|
|
552
|
+
return false;
|
|
553
|
+
} finally {
|
|
554
|
+
envBusy.value = false;
|
|
555
|
+
}
|
|
556
|
+
await afterWrite();
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
async function setEnvValue(name) {
|
|
560
|
+
const value = envValues[name];
|
|
561
|
+
if (!value) return;
|
|
562
|
+
if (await writeEnvironment({ set: { [name]: value }, unset: [] }, `Could not save ${name}.`)) {
|
|
563
|
+
delete envValues[name];
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
// table
|
|
568
|
+
rows,
|
|
569
|
+
loading,
|
|
570
|
+
loadError,
|
|
571
|
+
refresh,
|
|
572
|
+
// dialog
|
|
573
|
+
dialogVisible,
|
|
574
|
+
dialogTitle,
|
|
575
|
+
editing,
|
|
576
|
+
form,
|
|
577
|
+
dialogError,
|
|
578
|
+
saving,
|
|
579
|
+
presetOptions,
|
|
580
|
+
surfaceOptions,
|
|
581
|
+
isCustom,
|
|
582
|
+
showSurface,
|
|
583
|
+
baseUrlPlaceholder,
|
|
584
|
+
canSetValue,
|
|
585
|
+
showCredentialFields,
|
|
586
|
+
idError,
|
|
587
|
+
openAdd,
|
|
588
|
+
openEdit,
|
|
589
|
+
selectPreset,
|
|
590
|
+
saveProvider,
|
|
591
|
+
// delete
|
|
592
|
+
deleteTarget,
|
|
593
|
+
deleteOpen,
|
|
594
|
+
deleteError,
|
|
595
|
+
deleting,
|
|
596
|
+
requestDelete,
|
|
597
|
+
confirmDelete,
|
|
598
|
+
// detected
|
|
599
|
+
detected,
|
|
600
|
+
detectedBusy,
|
|
601
|
+
detectedError,
|
|
602
|
+
addDetected,
|
|
603
|
+
// row actions
|
|
604
|
+
onCredentialSelect,
|
|
605
|
+
setCredentialEnv,
|
|
606
|
+
setEnabled,
|
|
607
|
+
resetPatch,
|
|
608
|
+
rowBusy,
|
|
609
|
+
rowError,
|
|
610
|
+
// key editor
|
|
611
|
+
canEnterKey,
|
|
612
|
+
keyEditing,
|
|
613
|
+
keyForm,
|
|
614
|
+
keyError,
|
|
615
|
+
openKeyEditor,
|
|
616
|
+
cancelKeyEditor,
|
|
617
|
+
saveKeyEditor,
|
|
618
|
+
// reference editor
|
|
619
|
+
refEditing,
|
|
620
|
+
refForm,
|
|
621
|
+
refError,
|
|
622
|
+
openRefEditor,
|
|
623
|
+
cancelRefEditor,
|
|
624
|
+
saveRefEditor,
|
|
625
|
+
// environment
|
|
626
|
+
envDocument,
|
|
627
|
+
envEditable,
|
|
628
|
+
envValues,
|
|
629
|
+
envError,
|
|
630
|
+
envBusy,
|
|
631
|
+
setEnvValue
|
|
632
|
+
};
|
|
633
|
+
}
|
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NuxtModule } from '@nuxt/schema'
|
|
2
|
+
|
|
3
|
+
import type { default as Module } from './module.mjs'
|
|
4
|
+
|
|
5
|
+
export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
|
|
6
|
+
|
|
7
|
+
export { default } from './module.mjs'
|
|
8
|
+
|
|
9
|
+
export { type ForgeSettingsOptions } from './module.mjs'
|