@skaile/forge-settings 0.0.0-stage → 1.0.0

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