@skaile/forge-settings 1.2.2 → 1.3.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +31 -2
  3. package/dist/module.d.mts +4 -2
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +5 -0
  6. package/dist/runtime/components/ForgeModelRolesPanel.d.vue.ts +34 -0
  7. package/dist/runtime/components/ForgeModelRolesPanel.vue +250 -0
  8. package/dist/runtime/components/ForgeModelRolesPanel.vue.d.ts +34 -0
  9. package/dist/runtime/components/ForgeProvidersPanel.d.vue.ts +18 -5
  10. package/dist/runtime/components/ForgeProvidersPanel.vue +32 -7
  11. package/dist/runtime/components/ForgeProvidersPanel.vue.d.ts +18 -5
  12. package/dist/runtime/server/index.d.ts +1 -0
  13. package/dist/runtime/server/index.js +1 -0
  14. package/dist/runtime/shared/model-roles.d.ts +99 -0
  15. package/dist/runtime/shared/model-roles.js +144 -0
  16. package/dist/runtime/vue/index.d.ts +3 -0
  17. package/dist/runtime/vue/index.js +3 -0
  18. package/dist/runtime/vue/model-roles-panel.d.ts +70 -0
  19. package/dist/runtime/vue/model-roles-panel.js +54 -0
  20. package/dist/runtime/vue/providers-client.d.ts +67 -0
  21. package/dist/runtime/vue/providers-client.js +28 -0
  22. package/dist/runtime/vue/providers-panel-model.d.ts +24 -4
  23. package/dist/runtime/vue/providers-panel-model.js +5 -0
  24. package/dist/runtime/vue/useProvidersPanel.d.ts +10 -2
  25. package/dist/runtime/vue/useProvidersPanel.js +67 -43
  26. package/package.json +1 -1
  27. package/src/runtime/components/ForgeModelRolesPanel.vue +312 -0
  28. package/src/runtime/components/ForgeProvidersPanel.vue +55 -20
  29. package/src/runtime/server/index.ts +1 -0
  30. package/src/runtime/shared/model-roles.ts +283 -0
  31. package/src/runtime/vue/index.ts +3 -0
  32. package/src/runtime/vue/model-roles-panel.ts +140 -0
  33. package/src/runtime/vue/providers-client.ts +100 -0
  34. package/src/runtime/vue/providers-panel-model.ts +34 -4
  35. package/src/runtime/vue/useProvidersPanel.ts +89 -45
@@ -14,7 +14,6 @@
14
14
  * value.
15
15
  */
16
16
 
17
- import { $fetch } from "ofetch";
18
17
  import {
19
18
  type ComputedRef,
20
19
  computed,
@@ -39,11 +38,15 @@ import {
39
38
  type AIProviderSurface,
40
39
  validateCredentialRef,
41
40
  } from "../shared/provider-registry";
41
+ import {
42
+ type ProvidersPanelClient,
43
+ type ProvidersPanelTestResult,
44
+ urlProvidersClient,
45
+ } from "./providers-client";
42
46
  import {
43
47
  CREDENTIAL_ENTER_KEY,
44
48
  CREDENTIAL_OTHER_REF,
45
49
  CUSTOM_PRESET,
46
- DEFAULT_PRESETS_URL,
47
50
  EMPTY_FORM,
48
51
  ENV_SOURCE_LABEL,
49
52
  envNameOfRef,
@@ -52,8 +55,8 @@ import {
52
55
  OVERRIDES_INSTANCE_NOTICE,
53
56
  PROVIDER_ID_PATTERN,
54
57
  type ProvidersPanelCredentialOption,
55
- type ProvidersPanelEntry,
56
58
  type ProvidersPanelEmit,
59
+ type ProvidersPanelEntry,
57
60
  type ProvidersPanelForm,
58
61
  type ProvidersPanelPreset,
59
62
  type ProvidersPanelProps,
@@ -62,7 +65,6 @@ import {
62
65
  requestErrorMessage,
63
66
  } from "./providers-panel-model";
64
67
 
65
- export { CREDENTIAL_ENTER_KEY, CREDENTIAL_OTHER_REF, CUSTOM_PRESET } from "./providers-panel-model";
66
68
  export type {
67
69
  ProvidersPanelCredentialOption,
68
70
  ProvidersPanelEmit,
@@ -74,6 +76,7 @@ export type {
74
76
  ProvidersPanelRow,
75
77
  ProvidersPanelRowState,
76
78
  } from "./providers-panel-model";
79
+ export { CREDENTIAL_ENTER_KEY, CREDENTIAL_OTHER_REF, CUSTOM_PRESET } from "./providers-panel-model";
77
80
 
78
81
  /** What both `ForgeProvidersPanel` variants bind to. */
79
82
  export interface ProvidersPanelState {
@@ -134,6 +137,13 @@ export interface ProvidersPanelState {
134
137
  /** Row id with a write in flight. */
135
138
  rowBusy: Ref<string | null>;
136
139
  rowError: Ref<{ id: string; message: string } | null>;
140
+ /** The panel was given an explicit client: rows offer a connection test. */
141
+ canTest: ComputedRef<boolean>;
142
+ /** Row id with a test in flight. */
143
+ testBusy: Ref<string | null>;
144
+ /** The last test result per row id. */
145
+ testResults: Record<string, ProvidersPanelTestResult>;
146
+ testProvider: (row: ProvidersPanelEntry) => Promise<void>;
137
147
 
138
148
  // Inline key editor
139
149
  /** "Enter a key…" is offered: the scope has an editable `.env` route. */
@@ -180,16 +190,37 @@ export function useProvidersPanel(
180
190
  const loadError = ref<string | null>(null);
181
191
  const envError = ref<string | null>(null);
182
192
 
193
+ /** The explicit client, else the URL props. Throws when neither is given. */
194
+ function client(): ProvidersPanelClient {
195
+ if (props.client) return props.client;
196
+ if (!props.providersUrl || !props.providersWriteUrl) {
197
+ throw new Error(
198
+ "ForgeProvidersPanel needs a `client`, or `providersUrl` and `providersWriteUrl`.",
199
+ );
200
+ }
201
+ return urlProvidersClient({
202
+ providersUrl: props.providersUrl,
203
+ providersWriteUrl: props.providersWriteUrl,
204
+ presetsUrl: props.presetsUrl,
205
+ environmentUrl: props.environmentUrl,
206
+ });
207
+ }
208
+
209
+ /** The scope's environment is read and written: `environmentEnabled`, else a client, else `environmentUrl`. */
210
+ const environmentOn = computed(
211
+ () => props.environmentEnabled ?? (props.client ? true : Boolean(props.environmentUrl)),
212
+ );
213
+
183
214
  // ── Loading ────────────────────────────────────────────────────────────────
184
215
 
185
216
  /** Read the scope's `.env` document. A failure shows in the disclosure and blocks nothing else. */
186
217
  async function loadEnvironment() {
187
- if (!props.environmentUrl) {
218
+ if (!environmentOn.value) {
188
219
  envDocument.value = null;
189
220
  return;
190
221
  }
191
222
  try {
192
- envDocument.value = (await $fetch(props.environmentUrl)) as EnvSettingsDocument;
223
+ envDocument.value = await client().environment();
193
224
  envError.value = null;
194
225
  } catch (err) {
195
226
  envError.value = requestErrorMessage(err, "Could not load the environment variables.");
@@ -201,11 +232,8 @@ export function useProvidersPanel(
201
232
  loading.value = true;
202
233
  loadError.value = null;
203
234
  try {
204
- const [list, found] = await Promise.all([
205
- $fetch(props.providersUrl, { query: { all: "1" } }) as Promise<ProvidersPanelEntry[]>,
206
- $fetch(`${props.providersWriteUrl}/detected`) as Promise<ProvidersPanelPreset[]>,
207
- loadEnvironment(),
208
- ]);
235
+ const api = client();
236
+ const [list, found] = await Promise.all([api.list(), api.detected(), loadEnvironment()]);
209
237
  providers.value = Array.isArray(list) ? list : [];
210
238
  detected.value = Array.isArray(found) ? found : [];
211
239
  } catch (err) {
@@ -217,9 +245,7 @@ export function useProvidersPanel(
217
245
 
218
246
  async function loadPresets() {
219
247
  try {
220
- const list = (await $fetch(
221
- props.presetsUrl ?? DEFAULT_PRESETS_URL,
222
- )) as ProvidersPanelPreset[];
248
+ const list = await client().presets();
223
249
  presets.value = Array.isArray(list) ? list : [];
224
250
  } catch (err) {
225
251
  loadError.value = requestErrorMessage(err, "Could not load the provider presets.");
@@ -228,6 +254,8 @@ export function useProvidersPanel(
228
254
 
229
255
  /** After every successful write: reload list, banner and environment, tell the host. */
230
256
  async function afterWrite() {
257
+ // A test result describes the credential as it was; a write may have changed or removed it.
258
+ for (const key of Object.keys(testResults)) delete testResults[key];
231
259
  await refresh();
232
260
  emit("changed");
233
261
  }
@@ -238,7 +266,7 @@ export function useProvidersPanel(
238
266
  });
239
267
 
240
268
  watch(
241
- () => [props.providersUrl, props.providersWriteUrl, props.environmentUrl],
269
+ () => [props.providersUrl, props.providersWriteUrl, props.environmentUrl, props.client],
242
270
  () => void refresh(),
243
271
  );
244
272
 
@@ -252,9 +280,7 @@ export function useProvidersPanel(
252
280
 
253
281
  const envEditable = computed(() => envDocument.value?.editable === true);
254
282
 
255
- const canEnterKey = computed(
256
- () => Boolean(props.environmentUrl) && envDocument.value?.editable !== false,
257
- );
283
+ const canEnterKey = computed(() => environmentOn.value && envDocument.value?.editable !== false);
258
284
 
259
285
  /** Where an entry lives relative to this scope's own document. */
260
286
  function stateOf(entry: Pick<ProvidersPanelEntry, "layers">): ProvidersPanelRowState {
@@ -400,7 +426,7 @@ export function useProvidersPanel(
400
426
  );
401
427
 
402
428
  /** Whether "Set value now" is offered: the scope has an `.env` route. */
403
- const canSetValue = computed(() => Boolean(props.environmentUrl));
429
+ const canSetValue = computed(() => environmentOn.value);
404
430
 
405
431
  /** The dialog collects a credential for a new entry only. */
406
432
  const showCredentialFields = computed(() => editing.value === null);
@@ -525,18 +551,15 @@ export function useProvidersPanel(
525
551
  saving.value = true;
526
552
  dialogError.value = null;
527
553
  try {
528
- if (showCredentialFields.value && form.secret && props.environmentUrl) {
554
+ if (showCredentialFields.value && form.secret && environmentOn.value) {
529
555
  const update: EnvSettingsUpdate = {
530
556
  set: { [form.envName.trim()]: form.secret },
531
557
  unset: [],
532
558
  };
533
- await $fetch(props.environmentUrl, { method: "PUT", body: update });
559
+ await client().updateEnvironment(update);
534
560
  form.secret = "";
535
561
  }
536
- await $fetch(`${props.providersWriteUrl}/${encodeURIComponent(id)}`, {
537
- method: "PUT",
538
- body: buildBody(),
539
- });
562
+ await client().upsert(id, buildBody());
540
563
  dialogVisible.value = false;
541
564
  editing.value = null;
542
565
  await afterWrite();
@@ -573,9 +596,7 @@ export function useProvidersPanel(
573
596
  deleting.value = true;
574
597
  deleteError.value = null;
575
598
  try {
576
- await $fetch(`${props.providersWriteUrl}/${encodeURIComponent(target.id)}`, {
577
- method: "DELETE",
578
- });
599
+ await client().remove(target.id);
579
600
  deleteTarget.value = null;
580
601
  await afterWrite();
581
602
  } catch (err) {
@@ -609,12 +630,9 @@ export function useProvidersPanel(
609
630
  detectedBusy.value = preset.id;
610
631
  detectedError.value = null;
611
632
  try {
612
- await $fetch(`${props.providersWriteUrl}/${encodeURIComponent(id)}`, {
613
- method: "PUT",
614
- body: {
615
- preset: preset.id,
616
- credential: { kind: preset.credential.kind, ref: `env:${envName}` },
617
- },
633
+ await client().upsert(id, {
634
+ preset: preset.id,
635
+ credential: { kind: preset.credential.kind, ref: `env:${envName}` },
618
636
  });
619
637
  await afterWrite();
620
638
  } catch (err) {
@@ -664,10 +682,7 @@ export function useProvidersPanel(
664
682
  change: { credential?: { kind: AIProviderCredentialKind; ref: string }; enabled?: boolean },
665
683
  ) {
666
684
  const body = row.state === "own" ? { ...ownBody(row), ...change } : change;
667
- await $fetch(`${props.providersWriteUrl}/${encodeURIComponent(row.id)}`, {
668
- method: "PUT",
669
- body,
670
- });
685
+ await client().upsert(row.id, body);
671
686
  }
672
687
 
673
688
  function credentialFor(row: ProvidersPanelRow, ref: string) {
@@ -713,14 +728,39 @@ export function useProvidersPanel(
713
728
  await runRowWrite(
714
729
  row.id,
715
730
  async () => {
716
- await $fetch(`${props.providersWriteUrl}/${encodeURIComponent(row.id)}`, {
717
- method: "DELETE",
718
- });
731
+ await client().remove(row.id);
719
732
  },
720
733
  `Could not reset ${row.id}.`,
721
734
  );
722
735
  }
723
736
 
737
+ // ── Connection test ────────────────────────────────────────────────────────
738
+
739
+ /** Only an explicit client is known to serve the test; the URL routes are a host's concern. A read-only panel offers none. */
740
+ const canTest = computed(() => Boolean(props.client) && !props.readOnly);
741
+ const testBusy = ref<string | null>(null);
742
+ const testResults = reactive<Record<string, ProvidersPanelTestResult>>({});
743
+
744
+ /** Ask the client whether the entry's credential works; the answer shows in its row. */
745
+ async function testProvider(row: ProvidersPanelEntry) {
746
+ if (!props.client || props.readOnly || testBusy.value !== null) return;
747
+ testBusy.value = row.id;
748
+ delete testResults[row.id];
749
+ try {
750
+ testResults[row.id] = await props.client.test(row.id);
751
+ } catch (err) {
752
+ testResults[row.id] = {
753
+ ok: false,
754
+ message: requestErrorMessage(
755
+ err,
756
+ err instanceof Error && err.message ? err.message : `Could not test ${row.id}.`,
757
+ ),
758
+ };
759
+ } finally {
760
+ testBusy.value = null;
761
+ }
762
+ }
763
+
724
764
  // ── Inline key editor ──────────────────────────────────────────────────────
725
765
 
726
766
  const keyEditing = ref<string | null>(null);
@@ -761,7 +801,7 @@ export function useProvidersPanel(
761
801
  */
762
802
  async function saveKeyEditor() {
763
803
  const row = rows.value.find((r) => r.id === keyEditing.value);
764
- if (!row?.canEditCredential || !props.environmentUrl) return;
804
+ if (!row?.canEditCredential || !environmentOn.value) return;
765
805
  const name = keyForm.name.trim();
766
806
  const refusal = validateEnvVarName(name);
767
807
  if (refusal) {
@@ -777,7 +817,7 @@ export function useProvidersPanel(
777
817
  let wroteEnv = false;
778
818
  try {
779
819
  const update: EnvSettingsUpdate = { set: { [name]: keyForm.value }, unset: [] };
780
- await $fetch(props.environmentUrl, { method: "PUT", body: update });
820
+ await client().updateEnvironment(update);
781
821
  wroteEnv = true;
782
822
  const ref = `env:${name}`;
783
823
  if (row.credentialRef !== ref) {
@@ -837,11 +877,11 @@ export function useProvidersPanel(
837
877
 
838
878
  /** Write one change to the scope's `.env`, then reload and tell the host. */
839
879
  async function writeEnvironment(update: EnvSettingsUpdate, fallback: string): Promise<boolean> {
840
- if (!props.environmentUrl || !envEditable.value) return false;
880
+ if (!environmentOn.value || !envEditable.value) return false;
841
881
  envBusy.value = true;
842
882
  envError.value = null;
843
883
  try {
844
- await $fetch(props.environmentUrl, { method: "PUT", body: update });
884
+ await client().updateEnvironment(update);
845
885
  } catch (err) {
846
886
  envError.value = requestErrorMessage(err, fallback);
847
887
  return false;
@@ -904,6 +944,10 @@ export function useProvidersPanel(
904
944
  resetPatch,
905
945
  rowBusy,
906
946
  rowError,
947
+ canTest,
948
+ testBusy,
949
+ testResults,
950
+ testProvider,
907
951
  // key editor
908
952
  canEnterKey,
909
953
  keyEditing,