@homeflare/alchemy 0.52.0 → 0.53.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.
- package/dist/grafana/folder-permission.d.ts +76 -0
- package/dist/grafana/folder-permission.d.ts.map +1 -0
- package/dist/grafana/index.d.ts +3 -1
- package/dist/grafana/index.d.ts.map +1 -1
- package/dist/grafana/index.js +133 -46
- package/dist/grafana/index.js.map +7 -5
- package/dist/grafana/providers.d.ts +1 -1
- package/dist/grafana/providers.d.ts.map +1 -1
- package/dist/grafana/provisioned.d.ts +1 -1
- package/dist/grafana/provisioned.d.ts.map +1 -1
- package/dist/grafana/team.d.ts +64 -0
- package/dist/grafana/team.d.ts.map +1 -0
- package/dist/litellm/fake-marketplace-litellm.d.ts +33 -0
- package/dist/litellm/fake-marketplace-litellm.d.ts.map +1 -0
- package/dist/litellm/index.d.ts +1 -0
- package/dist/litellm/index.d.ts.map +1 -1
- package/dist/litellm/index.js +530 -329
- package/dist/litellm/index.js.map +7 -4
- package/dist/litellm/marketplace-form.d.ts +21 -0
- package/dist/litellm/marketplace-form.d.ts.map +1 -0
- package/dist/litellm/marketplace-operations.d.ts +27 -0
- package/dist/litellm/marketplace-operations.d.ts.map +1 -0
- package/dist/litellm/marketplace-plugin.d.ts +57 -0
- package/dist/litellm/marketplace-plugin.d.ts.map +1 -0
- package/dist/litellm/marketplace-types.d.ts +55 -0
- package/dist/litellm/marketplace-types.d.ts.map +1 -0
- package/dist/litellm/providers.d.ts +1 -1
- package/dist/litellm/providers.d.ts.map +1 -1
- package/docs/litellm-marketplace.md +59 -0
- package/docs/litellm.md +1 -0
- package/package.json +1 -1
- package/src/grafana/folder-permission.test.ts +107 -0
- package/src/grafana/folder-permission.ts +128 -0
- package/src/grafana/index.ts +10 -1
- package/src/grafana/providers.ts +4 -0
- package/src/grafana/provisioned.ts +1 -1
- package/src/grafana/team.test.ts +108 -0
- package/src/grafana/team.ts +105 -0
- package/src/litellm/fake-marketplace-litellm.test.ts +51 -0
- package/src/litellm/fake-marketplace-litellm.ts +134 -0
- package/src/litellm/index.ts +8 -0
- package/src/litellm/marketplace-form.ts +160 -0
- package/src/litellm/marketplace-operations.ts +76 -0
- package/src/litellm/marketplace-plugin-replace.test.ts +68 -0
- package/src/litellm/marketplace-plugin.test.ts +181 -0
- package/src/litellm/marketplace-plugin.ts +156 -0
- package/src/litellm/marketplace-types.ts +53 -0
- package/src/litellm/providers.ts +2 -0
- package/src/litellm/registry-exports.test.ts +6 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire bodies, comparison and refusals for `LiteLLM.ClaudeCodeMarketplacePlugin`.
|
|
3
|
+
*
|
|
4
|
+
* ★ THE UPDATE IS A FULL PUT: `source` is required and `version` is "cleared if omitted"
|
|
5
|
+
* (`claude_code_marketplace.ts:563`), so an update carries every optional field the plugin
|
|
6
|
+
* already has; a field the declaration leaves out is re-sent as it is live, never wiped.
|
|
7
|
+
* ★ REGISTER IS CREATE-ONLY: `POST /claude-code/plugins` returns 409 if the name already exists
|
|
8
|
+
* (`claude_code_marketplace.ts:546`), so `reconcile` registers only after a list shows the name
|
|
9
|
+
* absent.
|
|
10
|
+
*/
|
|
11
|
+
import type * as cc from '@distilled.cloud/litellm/claude_code_marketplace';
|
|
12
|
+
import type { MarketplacePluginAttributes, MarketplacePluginProps } from './marketplace-types.ts';
|
|
13
|
+
/** The first reason a declaration is refused, or `undefined`. Runs before any request. */
|
|
14
|
+
export declare const firstProblem: (props: MarketplacePluginProps) => string | undefined;
|
|
15
|
+
/** One list item. `undefined` is "not a plugin row": the caller refuses the whole answer. */
|
|
16
|
+
export declare const toAttributes: (value: unknown) => MarketplacePluginAttributes | undefined;
|
|
17
|
+
/** Wire names where live and declared disagree. Optional fields count only when declared. */
|
|
18
|
+
export declare const differing: (live: MarketplacePluginAttributes, props: MarketplacePluginProps) => readonly string[];
|
|
19
|
+
export declare const registerBody: (props: MarketplacePluginProps) => cc.RegisterPluginClaudeCodePluginsPostRequest;
|
|
20
|
+
export declare const updateBody: (props: MarketplacePluginProps, live: MarketplacePluginAttributes) => Omit<cc.UpdatePluginClaudeCodePluginsPluginNamePutRequest, 'plugin_name'>;
|
|
21
|
+
//# sourceMappingURL=marketplace-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"marketplace-form.d.ts","sourceRoot":"","sources":["../../src/litellm/marketplace-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,KAAK,EAAE,MAAM,kDAAkD,CAAC;AAW5E,OAAO,KAAK,EAAE,2BAA2B,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAMlG,0FAA0F;AAC1F,eAAO,MAAM,YAAY,UAAW,sBAAsB,KAAG,MAAM,GAAG,SAyBrE,CAAC;AASF,6FAA6F;AAC7F,eAAO,MAAM,YAAY,UAAW,OAAO,KAAG,2BAA2B,GAAG,SAwB3E,CAAC;AAWF,6FAA6F;AAC7F,eAAO,MAAM,SAAS,SACd,2BAA2B,SAC1B,sBAAsB,KAC5B,SAAS,MAAM,EAiBjB,CAAC;AA6BF,eAAO,MAAM,YAAY,UAChB,sBAAsB,KAC5B,EAAE,CAAC,0CAGJ,CAAC;AAEH,eAAO,MAAM,UAAU,UACd,sBAAsB,QACvB,2BAA2B,KAChC,IAAI,CAAC,EAAE,CAAC,iDAAiD,EAAE,aAAa,CACpD,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The LiteLLM `/claude-code/plugins*` calls `LiteLLM.ClaudeCodeMarketplacePlugin` uses, through
|
|
3
|
+
* `@distilled.cloud/litellm/claude_code_marketplace` (1.103.0):
|
|
4
|
+
*
|
|
5
|
+
* list `listPluginsClaudeCodePluginsGet` GET /claude-code/plugins
|
|
6
|
+
* register `registerPluginClaudeCodePluginsPost` POST /claude-code/plugins
|
|
7
|
+
* update `updatePluginClaudeCodePluginsPluginNamePut` PUT /claude-code/plugins/{name}
|
|
8
|
+
* enable `enablePluginClaudeCodePluginsPluginNameEnablePost` POST …/{name}/enable
|
|
9
|
+
* disable `disablePluginClaudeCodePluginsPluginNameDisablePost` POST …/{name}/disable
|
|
10
|
+
* delete `deletePluginClaudeCodePluginsPluginNameDelete` DELETE /claude-code/plugins/{name}
|
|
11
|
+
*
|
|
12
|
+
* ⛔ WRITES NEED A PROXY ADMIN KEY. ⛔ NO MESSAGE SNIFFING: the by-name GET is not used, the list
|
|
13
|
+
* decides absence (registry-support.ts says why). ★ DELETE IS IDEMPOTENT BY A REAL READ: a failed
|
|
14
|
+
* DELETE is swallowed only when the list no longer has the name; otherwise the ORIGINAL error rises.
|
|
15
|
+
*/
|
|
16
|
+
import * as cc from '@distilled.cloud/litellm/claude_code_marketplace';
|
|
17
|
+
import * as Effect from 'effect/Effect';
|
|
18
|
+
import { type LitellmOpContext } from './operations.ts';
|
|
19
|
+
import { LitellmRegistryUnreadableError } from './registry-errors.ts';
|
|
20
|
+
import type { MarketplacePluginAttributes } from './marketplace-types.ts';
|
|
21
|
+
/** Every plugin, enabled or not (`enabled_only` unset). */
|
|
22
|
+
export declare const listPlugins: () => Effect.Effect<readonly MarketplacePluginAttributes[], cc.ListPluginsClaudeCodePluginsGetError | LitellmRegistryUnreadableError, LitellmOpContext>;
|
|
23
|
+
export declare const registerPlugin: (body: cc.RegisterPluginClaudeCodePluginsPostRequest) => Effect.Effect<void, cc.RegisterPluginClaudeCodePluginsPostError, import("@distilled.cloud/litellm/Credentials").Credentials>;
|
|
24
|
+
export declare const updatePlugin: (name: string, body: Omit<cc.UpdatePluginClaudeCodePluginsPluginNamePutRequest, 'plugin_name'>) => Effect.Effect<void, cc.UpdatePluginClaudeCodePluginsPluginNamePutError, import("@distilled.cloud/litellm/Credentials").Credentials>;
|
|
25
|
+
export declare const setEnabled: (name: string, enabled: boolean) => Effect.Effect<void, cc.DisablePluginClaudeCodePluginsPluginNameDisablePostError, import("@distilled.cloud/litellm/Credentials").Credentials>;
|
|
26
|
+
export declare const deletePlugin: (name: string) => Effect.Effect<void, cc.DeletePluginClaudeCodePluginsPluginNameDeleteError, LitellmOpContext>;
|
|
27
|
+
//# sourceMappingURL=marketplace-operations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"marketplace-operations.d.ts","sourceRoot":"","sources":["../../src/litellm/marketplace-operations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,MAAM,kDAAkD,CAAC;AACvE,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,KAAK,gBAAgB,EAAgB,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,8BAA8B,EAAE,MAAM,sBAAsB,CAAC;AAEtE,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC;AAK1E,2DAA2D;AAC3D,eAAO,MAAM,WAAW,QAAO,MAAM,CAAC,MAAM,CAC1C,SAAS,2BAA2B,EAAE,EACtC,EAAE,CAAC,oCAAoC,GAAG,8BAA8B,EACxE,gBAAgB,CAef,CAAC;AAEJ,eAAO,MAAM,cAAc,SAAU,EAAE,CAAC,0CAA0C,iIACF,CAAC;AAEjF,eAAO,MAAM,YAAY,SACjB,MAAM,QACN,IAAI,CAAC,EAAE,CAAC,iDAAiD,EAAE,aAAa,CAAC,wIAI9E,CAAC;AAEJ,eAAO,MAAM,UAAU,SAAU,MAAM,WAAW,OAAO,iJAKlC,CAAC;AAExB,eAAO,MAAM,YAAY,SAAU,MAAM,iGAWtC,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `LiteLLM.ClaudeCodeMarketplacePlugin` — one plugin registered in LiteLLM's Claude Code marketplace
|
|
3
|
+
* (`/claude-code/plugins`), which LiteLLM serves as `/claude-code/marketplace.json`. Types and what
|
|
4
|
+
* is left out: marketplace-types.ts.
|
|
5
|
+
*
|
|
6
|
+
* ★ ADOPT BY NAME. `name` is unique, so `pluginName` finds the one live row. A live row with no
|
|
7
|
+
* state is `Unowned`, so it needs `--adopt`.
|
|
8
|
+
* ★ `defaultRemovalPolicy: 'retain'`: deleting a plugin removes the install source every Claude Code
|
|
9
|
+
* client has pointed at. Opt in with `.pipe(RemovalPolicy.destroy())`.
|
|
10
|
+
* ★ A NEW NAME IS A REPLACE: the name is the path key of every call, so there is no rename. Under
|
|
11
|
+
* the default `retain` the old plugin stays registered and listed until it is removed by hand or
|
|
12
|
+
* the stack opts in with `.pipe(RemovalPolicy.destroy())`.
|
|
13
|
+
* ★ THE PROOF IS THE MARKETPLACE FILE: after a deploy, `GET /claude-code/marketplace.json` lists the
|
|
14
|
+
* plugin only while it is enabled, which is why `enabled` is part of the declaration.
|
|
15
|
+
* ⛔ NO LITELLM CREDENTIAL IS A PROP; `litellmProviders`' layer supplies `Credentials`.
|
|
16
|
+
*/
|
|
17
|
+
import { Resource } from 'alchemy';
|
|
18
|
+
import type { Input } from 'alchemy/Input';
|
|
19
|
+
import * as Provider from 'alchemy/Provider';
|
|
20
|
+
import * as Effect from 'effect/Effect';
|
|
21
|
+
import type { MarketplacePluginAttributes, MarketplacePluginProps } from './marketplace-types.ts';
|
|
22
|
+
import { LitellmRegistryAbsentAfterWriteError, LitellmRegistryNotConvergedError } from './registry-errors.ts';
|
|
23
|
+
export type { MarketplacePluginAttributes, MarketplacePluginProps };
|
|
24
|
+
export type { RegistryError as MarketplacePluginError } from './registry-errors.ts';
|
|
25
|
+
export interface LiteLLMClaudeCodeMarketplacePlugin extends Resource<'LiteLLM.ClaudeCodeMarketplacePlugin', MarketplacePluginProps, MarketplacePluginAttributes> {
|
|
26
|
+
}
|
|
27
|
+
export declare const LiteLLMClaudeCodeMarketplacePlugin: import("alchemy").ResourceClass<LiteLLMClaudeCodeMarketplacePlugin>;
|
|
28
|
+
export declare const isLiteLLMClaudeCodeMarketplacePlugin: (value: unknown) => value is LiteLLMClaudeCodeMarketplacePlugin;
|
|
29
|
+
/** ⛔ TEST-ONLY EXPORT. Not on index.ts's barrel. */
|
|
30
|
+
export declare const marketplacePluginHandlers: {
|
|
31
|
+
/** ⛔ With no `output` this is the adoption probe, so the refusals run here too. */
|
|
32
|
+
read: ({ olds, output, }: {
|
|
33
|
+
olds: MarketplacePluginProps;
|
|
34
|
+
output: MarketplacePluginAttributes | undefined;
|
|
35
|
+
}) => Effect.Effect<MarketplacePluginAttributes | undefined, import("./registry-errors.ts").LitellmRegistryUnreadableError | import("@distilled.cloud/litellm/claude_code_marketplace").ListPluginsClaudeCodePluginsGetError, import("@distilled.cloud/litellm/Protocol").LitellmOpContext>;
|
|
36
|
+
diff: ({ news, output, }: {
|
|
37
|
+
news: Input<MarketplacePluginProps>;
|
|
38
|
+
output: MarketplacePluginAttributes | undefined;
|
|
39
|
+
}) => Effect.Effect<{
|
|
40
|
+
readonly action: 'replace';
|
|
41
|
+
} | {
|
|
42
|
+
readonly action: 'noop';
|
|
43
|
+
} | {
|
|
44
|
+
readonly action: 'update';
|
|
45
|
+
} | undefined, import("./registry-errors.ts").LitellmRegistryInvalidError, never>;
|
|
46
|
+
reconcile: ({ news, output, }: {
|
|
47
|
+
news: MarketplacePluginProps;
|
|
48
|
+
output: MarketplacePluginAttributes | undefined;
|
|
49
|
+
}) => Effect.Effect<MarketplacePluginAttributes, import("@distilled.cloud/caddy").BadGateway | import("@distilled.cloud/caddy").ConfigError | import("@distilled.cloud/caddy").GatewayTimeout | import("effect/http/HttpClientError").HttpClientError | import("@distilled.cloud/caddy").InternalServerError | import("@distilled.cloud/litellm/Errors").LitellmParseError | LitellmRegistryAbsentAfterWriteError | import("./registry-errors.ts").LitellmRegistryInvalidError | LitellmRegistryNotConvergedError | import("./registry-errors.ts").LitellmRegistryUnreadableError | import("@distilled.cloud/caddy").ServiceUnavailable | import("@distilled.cloud/caddy").TooManyRequests | import("@distilled.cloud/caddy").Unauthorized | import("@distilled.cloud/litellm/Errors").UnknownLitellmError | import("@distilled.cloud/litellm/claude_code_marketplace").UnprocessableEntity, import("@distilled.cloud/litellm/Protocol").LitellmOpContext>;
|
|
50
|
+
delete: ({ output }: {
|
|
51
|
+
output: MarketplacePluginAttributes;
|
|
52
|
+
}) => Effect.Effect<void, import("@distilled.cloud/litellm/claude_code_marketplace").DeletePluginClaudeCodePluginsPluginNameDeleteError, import("@distilled.cloud/litellm/Protocol").LitellmOpContext>;
|
|
53
|
+
/** ⛔ EMPTY: adoption is an explicit act (`--adopt`), and no ownership mark exists to filter by. */
|
|
54
|
+
list: () => Effect.Effect<never[], never, never>;
|
|
55
|
+
};
|
|
56
|
+
export declare const LiteLLMClaudeCodeMarketplacePluginProvider: () => import("effect/Layer").Layer<Provider.Provider<LiteLLMClaudeCodeMarketplacePlugin>, never, import("@distilled.cloud/litellm/Protocol").LitellmOpContext>;
|
|
57
|
+
//# sourceMappingURL=marketplace-plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"marketplace-plugin.d.ts","sourceRoot":"","sources":["../../src/litellm/marketplace-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGnC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAUxC,OAAO,KAAK,EAAE,2BAA2B,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAClG,OAAO,EACL,oCAAoC,EACpC,gCAAgC,EACjC,MAAM,sBAAsB,CAAC;AAG9B,YAAY,EAAE,2BAA2B,EAAE,sBAAsB,EAAE,CAAC;AACpE,YAAY,EAAE,aAAa,IAAI,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAIpF,MAAM,WAAW,kCAAmC,SAAQ,QAAQ,CAClE,qCAAqC,EACrC,sBAAsB,EACtB,2BAA2B,CAC5B;CAAG;AAEJ,eAAO,MAAM,kCAAkC,qEAG9C,CAAC;AAEF,eAAO,MAAM,oCAAoC,UACxC,OAAO,KACb,KAAK,IAAI,kCACkF,CAAC;AAY/F,oDAAoD;AACpD,eAAO,MAAM,yBAAyB;IACpC,mFAAmF;IACnF,IAAI,sBAGD;QACD,IAAI,EAAE,sBAAsB,CAAC;QAC7B,MAAM,EAAE,2BAA2B,GAAG,SAAS,CAAC;KACjD;IAUD,IAAI,sBAGD;QACD,IAAI,EAAE,KAAK,CAAC,sBAAsB,CAAC,CAAC;QACpC,MAAM,EAAE,2BAA2B,GAAG,SAAS,CAAC;KACjD;yBAK+D,SAAS;;yBAEtD,MAAM;;yBACN,QAAQ;;IAG3B,SAAS,sBAGN;QACD,IAAI,EAAE,sBAAsB,CAAC;QAC7B,MAAM,EAAE,2BAA2B,GAAG,SAAS,CAAC;KACjD;IAkCD,MAAM,eAAe;QAAE,MAAM,EAAE,2BAA2B,CAAA;KAAE;IAE5D,mGAAmG;IACnG,IAAI;CACL,CAAC;AAEF,eAAO,MAAM,0CAA0C,gKAIpD,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a `LiteLLM.ClaudeCodeMarketplacePlugin` declares (props) and remembers (attributes).
|
|
3
|
+
*
|
|
4
|
+
* ★ A PLUGIN IS A MARKETPLACE ENTRY, NOT A SKILL. LiteLLM serves `/claude-code/marketplace.json`
|
|
5
|
+
* from the plugins registered under `/claude-code/plugins`; Claude Code installs from that
|
|
6
|
+
* marketplace. The plugin folders a seat loads stay what they are (Tim, 2026-10-01); this entry
|
|
7
|
+
* points the marketplace at the same folder through `source`.
|
|
8
|
+
* ★ THE NAME IS THE IDENTITY. `name` is the path key of every read, update and delete, so it cannot
|
|
9
|
+
* be renamed in place: changing `pluginName` is a replace, never an update.
|
|
10
|
+
* ⚠️ `enabled` DEFAULTS TO `true`. A registered but disabled plugin is absent from the marketplace
|
|
11
|
+
* file, so a declaration that means "listed" must also mean "enabled".
|
|
12
|
+
*/
|
|
13
|
+
export interface PluginAuthor {
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly email?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface MarketplacePluginProps {
|
|
18
|
+
/** kebab-case plugin name, e.g. `homeflare-workflows`. Also the way an existing row is adopted. */
|
|
19
|
+
readonly pluginName: string;
|
|
20
|
+
/**
|
|
21
|
+
* Where Claude Code fetches the plugin from, as LiteLLM takes it: `{ source: 'github', repo }`,
|
|
22
|
+
* `{ source: 'url', url }`, `{ source: 'git-subdir', url, path }` or `{ source: 'archive', url, sha256? }`.
|
|
23
|
+
* Always compared.
|
|
24
|
+
*/
|
|
25
|
+
readonly source: Readonly<Record<string, string>>;
|
|
26
|
+
/** Compared only when declared: adopting never clears text a person wrote. */
|
|
27
|
+
readonly description?: string;
|
|
28
|
+
readonly version?: string;
|
|
29
|
+
readonly domain?: string;
|
|
30
|
+
readonly namespace?: string;
|
|
31
|
+
readonly category?: string;
|
|
32
|
+
readonly homepage?: string;
|
|
33
|
+
readonly keywords?: readonly string[];
|
|
34
|
+
readonly author?: PluginAuthor;
|
|
35
|
+
/** Whether the marketplace lists it. Default `true`. */
|
|
36
|
+
readonly enabled?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface MarketplacePluginAttributes {
|
|
39
|
+
readonly pluginId: string;
|
|
40
|
+
readonly pluginName: string;
|
|
41
|
+
readonly source: Readonly<Record<string, string>>;
|
|
42
|
+
readonly enabled: boolean;
|
|
43
|
+
readonly description: string | null;
|
|
44
|
+
readonly version: string | null;
|
|
45
|
+
readonly domain: string | null;
|
|
46
|
+
readonly namespace: string | null;
|
|
47
|
+
readonly category: string | null;
|
|
48
|
+
readonly homepage: string | null;
|
|
49
|
+
readonly keywords: readonly string[];
|
|
50
|
+
readonly author: {
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly email: string | null;
|
|
53
|
+
} | null;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=marketplace-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"marketplace-types.d.ts","sourceRoot":"","sources":["../../src/litellm/marketplace-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,mGAAmG;IACnG,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD,8EAA8E;IAC9E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;IAC/B,wDAAwD;IACxD,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CAClF"}
|
|
@@ -20,5 +20,5 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { type Credentials } from '@distilled.cloud/litellm/Credentials';
|
|
22
22
|
import * as Layer from 'effect/Layer';
|
|
23
|
-
export declare const litellmProviders: (creds?: Layer.Layer<Credentials>) => Layer.Layer<Credentials | import("alchemy").Provider<import("./access-group.ts").LiteLLMAccessGroup> | import("alchemy").Provider<import("./budget.ts").LiteLLMBudget> | import("alchemy").Provider<import("./credential.ts").LiteLLMCredential> | import("alchemy").Provider<import("./key.ts").LiteLLMKey> | import("alchemy").Provider<import("./mcp-server.ts").LiteLLMMCPServer> | import("alchemy").Provider<import("./model.ts").LiteLLMModel> | import("alchemy").Provider<import("./pass-through-endpoint.ts").LiteLLMPassThroughEndpoint> | import("alchemy").Provider<import("./policy.ts").LiteLLMPolicy> | import("alchemy").Provider<import("./policy-attachment.ts").LiteLLMPolicyAttachment> | import("alchemy").Provider<import("./team.ts").LiteLLMTeam> | import("alchemy").Provider<import("./tool-policy.ts").LiteLLMToolPolicy> | import("alchemy").Provider<import("./toolset.ts").LiteLLMToolset>, never, import("effect/http/HttpClient").HttpClient | import("alchemy").Stack | import("alchemy").Stage>;
|
|
23
|
+
export declare const litellmProviders: (creds?: Layer.Layer<Credentials>) => Layer.Layer<Credentials | import("alchemy").Provider<import("./access-group.ts").LiteLLMAccessGroup> | import("alchemy").Provider<import("./budget.ts").LiteLLMBudget> | import("alchemy").Provider<import("./marketplace-plugin.ts").LiteLLMClaudeCodeMarketplacePlugin> | import("alchemy").Provider<import("./credential.ts").LiteLLMCredential> | import("alchemy").Provider<import("./key.ts").LiteLLMKey> | import("alchemy").Provider<import("./mcp-server.ts").LiteLLMMCPServer> | import("alchemy").Provider<import("./model.ts").LiteLLMModel> | import("alchemy").Provider<import("./pass-through-endpoint.ts").LiteLLMPassThroughEndpoint> | import("alchemy").Provider<import("./policy.ts").LiteLLMPolicy> | import("alchemy").Provider<import("./policy-attachment.ts").LiteLLMPolicyAttachment> | import("alchemy").Provider<import("./team.ts").LiteLLMTeam> | import("alchemy").Provider<import("./tool-policy.ts").LiteLLMToolPolicy> | import("alchemy").Provider<import("./toolset.ts").LiteLLMToolset>, never, import("effect/http/HttpClient").HttpClient | import("alchemy").Stack | import("alchemy").Stage>;
|
|
24
24
|
//# sourceMappingURL=providers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../src/litellm/providers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,KAAK,WAAW,EAAsB,MAAM,sCAAsC,CAAC;AAC5F,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../src/litellm/providers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,KAAK,WAAW,EAAsB,MAAM,sCAAsC,CAAC;AAC5F,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAetC,eAAO,MAAM,gBAAgB,WAAW,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,0kCAe7B,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# LiteLLM.ClaudeCodeMarketplacePlugin
|
|
2
|
+
|
|
3
|
+
`LiteLLM.ClaudeCodeMarketplacePlugin` registers one plugin in LiteLLM's Claude Code
|
|
4
|
+
marketplace. Plugins live under `/claude-code/plugins` and are served as
|
|
5
|
+
`/claude-code/marketplace.json`; Claude Code installs from that marketplace, but the
|
|
6
|
+
plugin folders a seat loads stay what they are (Tim, 2026-10-01). This entry points the
|
|
7
|
+
marketplace at the same folder through `source`.
|
|
8
|
+
|
|
9
|
+
Walked against LiteLLM **1.103.0** via `@homeflare/distilled-litellm@0.4.1`
|
|
10
|
+
(`packages/distilled-litellm/src/services/claude_code_marketplace.ts`).
|
|
11
|
+
|
|
12
|
+
## Identity
|
|
13
|
+
|
|
14
|
+
`name` is the resource identity. It is the path key of every read, update and delete, so
|
|
15
|
+
changing `pluginName` is a **replace**, not an update. Under the default `retain`, the
|
|
16
|
+
old name stays registered and listed until it is removed by hand or the stack opts in
|
|
17
|
+
with `.pipe(RemovalPolicy.destroy())`.
|
|
18
|
+
|
|
19
|
+
## Source kinds
|
|
20
|
+
|
|
21
|
+
`source.source` must be one of:
|
|
22
|
+
|
|
23
|
+
- `github` — `repo: owner/name`
|
|
24
|
+
- `url` — `url: <plugin.json URL>`
|
|
25
|
+
- `git-subdir` — `url: <git URL>`, `path: <subdir>`
|
|
26
|
+
- `archive` — `url: <archive URL>`, optional `sha256`
|
|
27
|
+
|
|
28
|
+
## Behaviour
|
|
29
|
+
|
|
30
|
+
- **Register** (`POST /claude-code/plugins`) is create-only and returns 409 if the name
|
|
31
|
+
already exists. The resource registers only after a list shows the name absent.
|
|
32
|
+
- **Update** (`PUT /claude-code/plugins/{name}`) is a full replace, not a merge: any
|
|
33
|
+
optional field the declaration leaves out is reset. The provider re-sends the live
|
|
34
|
+
value of omitted optional fields so a person-written description, keywords, etc. are
|
|
35
|
+
never wiped.
|
|
36
|
+
- **Enable/disable** is a separate route. The marketplace lists only enabled plugins,
|
|
37
|
+
so `enabled: true` (the default) is required for the plugin to be visible.
|
|
38
|
+
- **Adoption** is by name. A live row with no recorded state is `Unowned`; pass
|
|
39
|
+
`--adopt` to take it without writing anything.
|
|
40
|
+
- **Default removal policy is `retain`**. Deleting a plugin removes an install source
|
|
41
|
+
every Claude Code client has pointed at.
|
|
42
|
+
|
|
43
|
+
## Example
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { LiteLLMClaudeCodeMarketplacePlugin, litellmProviders } from '@homeflare/alchemy/litellm';
|
|
47
|
+
|
|
48
|
+
export class HomeflareWorkflows extends LiteLLMClaudeCodeMarketplacePlugin('HomeflareWorkflows', {
|
|
49
|
+
pluginName: 'homeflare-workflows',
|
|
50
|
+
source: { source: 'github', repo: 'homeflare/remote-claude-code' },
|
|
51
|
+
version: '0.1.0',
|
|
52
|
+
description: 'Homeflare workflows for Claude Code',
|
|
53
|
+
keywords: ['homeflare', 'workflows'],
|
|
54
|
+
author: { name: 'Homeflare', email: 'hello@homeflare.io' },
|
|
55
|
+
enabled: true,
|
|
56
|
+
}) {}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Then provide `litellmProviders()` alongside the stack's other providers.
|
package/docs/litellm.md
CHANGED
|
@@ -19,6 +19,7 @@ Measured at 1.100.0 (`schema.prisma`, `pass_through_endpoints.py`, and the mini'
|
|
|
19
19
|
| Family | Managed by this resource? |
|
|
20
20
|
| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
21
21
|
| Pass-through endpoints | Yes — this package |
|
|
22
|
+
| Claude Code marketplace plugins (`/claude-code/plugins`) | Yes — `LiteLLM.ClaudeCodeMarketplacePlugin`, [litellm-marketplace.md](./litellm-marketplace.md) |
|
|
22
23
|
| Budgets (`/budget/*`) | Yes — `LiteLLM.Budget`, [litellm-budget.md](./litellm-budget.md) |
|
|
23
24
|
| MCP servers (`/v1/mcp/server`) | Yes — `LiteLLM.MCPServer`, [litellm-mcp.md](./litellm-mcp.md) |
|
|
24
25
|
| Teams (`/team/*`) | Yes — `LiteLLM.Team`, [litellm-team.md](./litellm-team.md) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homeflare/alchemy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
|
|
5
5
|
"homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
|
|
6
6
|
"bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Grafana.FolderPermission` against a fake Grafana: only DIRECT team entries count, writes go through
|
|
3
|
+
* the per-team endpoint (never the whole-list replace), missing team fails loudly, delete sets "".
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, test } from 'bun:test';
|
|
6
|
+
import type { GrafanaOpContext } from '@distilled.cloud/grafana/Protocol';
|
|
7
|
+
import * as Effect from 'effect/Effect';
|
|
8
|
+
import { fakeFailure, fakeGrafana, fakeGrafanaLayer } from './fake-grafana.ts';
|
|
9
|
+
import { spec } from './folder-permission.ts';
|
|
10
|
+
import { grafanaOperations } from './resource.ts';
|
|
11
|
+
|
|
12
|
+
const TEAMS = '/api/teams/search';
|
|
13
|
+
const PERMS = '/api/access-control/folders/village-grafana';
|
|
14
|
+
const SET = '/api/access-control/folders/village-grafana/teams/7';
|
|
15
|
+
const teamRow = {
|
|
16
|
+
id: 7,
|
|
17
|
+
uid: 'aex7',
|
|
18
|
+
name: 'village-grafana',
|
|
19
|
+
isProvisioned: false,
|
|
20
|
+
memberCount: 0,
|
|
21
|
+
orgId: 1,
|
|
22
|
+
};
|
|
23
|
+
const props = {
|
|
24
|
+
folderUid: 'village-grafana',
|
|
25
|
+
teamName: 'village-grafana',
|
|
26
|
+
permission: 'Edit' as const,
|
|
27
|
+
};
|
|
28
|
+
const run = <A, E>(
|
|
29
|
+
fake: ReturnType<typeof fakeGrafana>,
|
|
30
|
+
effect: Effect.Effect<A, E, GrafanaOpContext>,
|
|
31
|
+
): Promise<A> => Effect.runPromise(effect.pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))));
|
|
32
|
+
|
|
33
|
+
const grafanaWith = (entries: () => unknown[], teams = [teamRow]) =>
|
|
34
|
+
fakeGrafana((m, url) => {
|
|
35
|
+
if (m === 'GET' && url.pathname === TEAMS)
|
|
36
|
+
return Response.json({ teams, totalCount: teams.length });
|
|
37
|
+
if (m === 'GET' && url.pathname === PERMS) return Response.json(entries());
|
|
38
|
+
if (m === 'POST' && url.pathname === SET)
|
|
39
|
+
return Response.json({ message: 'Permission updated' });
|
|
40
|
+
return fakeFailure(404, `unexpected ${m} ${url.pathname}`);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('Grafana.FolderPermission', () => {
|
|
44
|
+
test('an inherited entry for the team does not count as live', async () => {
|
|
45
|
+
const fake = grafanaWith(() => [
|
|
46
|
+
{ teamId: 7, permission: 'Edit', isInherited: true },
|
|
47
|
+
{ builtInRole: 'Admin', permission: 'Admin' },
|
|
48
|
+
]);
|
|
49
|
+
expect(await run(fake, spec.fetchLive(props))).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('a matching direct entry adopts with no write', async () => {
|
|
53
|
+
const fake = grafanaWith(() => [
|
|
54
|
+
{ builtInRole: 'Viewer', permission: 'View', isInherited: true },
|
|
55
|
+
{ teamId: 7, team: 'village-grafana', permission: 'Edit', isInherited: false },
|
|
56
|
+
]);
|
|
57
|
+
const out = await run(fake, grafanaOperations(spec).reconcile(props));
|
|
58
|
+
expect(out).toEqual({
|
|
59
|
+
folderUid: 'village-grafana',
|
|
60
|
+
teamId: 7,
|
|
61
|
+
teamName: 'village-grafana',
|
|
62
|
+
permission: 'Edit',
|
|
63
|
+
});
|
|
64
|
+
expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('create writes only through the per-team endpoint', async () => {
|
|
68
|
+
const state = { set: false };
|
|
69
|
+
const fake = fakeGrafana((m, url) => {
|
|
70
|
+
if (m === 'GET' && url.pathname === TEAMS)
|
|
71
|
+
return Response.json({ teams: [teamRow], totalCount: 1 });
|
|
72
|
+
if (m === 'GET' && url.pathname === PERMS)
|
|
73
|
+
return Response.json(
|
|
74
|
+
state.set ? [{ teamId: 7, permission: 'Edit', isInherited: false }] : [],
|
|
75
|
+
);
|
|
76
|
+
if (m === 'POST' && url.pathname === SET) {
|
|
77
|
+
state.set = true;
|
|
78
|
+
return Response.json({ message: 'Permission updated' });
|
|
79
|
+
}
|
|
80
|
+
return fakeFailure(404, `unexpected ${m} ${url.pathname}`);
|
|
81
|
+
});
|
|
82
|
+
await run(fake, grafanaOperations(spec).reconcile(props));
|
|
83
|
+
expect(fake.seen.filter((s) => s.method === 'POST')).toEqual([{ method: 'POST', path: SET }]);
|
|
84
|
+
expect(fake.bodies.filter((b) => b !== undefined)).toEqual([{ permission: 'Edit' }]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('a permission drift (View live, Edit declared) updates', async () => {
|
|
88
|
+
const fake = grafanaWith(() => [{ teamId: 7, permission: 'View', isInherited: false }]);
|
|
89
|
+
await run(fake, Effect.flip(grafanaOperations(spec).reconcile(props))).catch(() => undefined);
|
|
90
|
+
expect(fake.seen.some((s) => s.method === 'POST' && s.path === SET)).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('a missing team fails loudly, never folds to absent', async () => {
|
|
94
|
+
const fake = grafanaWith(() => [], []);
|
|
95
|
+
const failure = await run(fake, Effect.flip(spec.fetchLive(props)));
|
|
96
|
+
expect((failure as { _tag: string })._tag).toBe('GrafanaTeamNotFoundError');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('delete sets the empty permission, and is a no-op when already absent', async () => {
|
|
100
|
+
const live = grafanaWith(() => [{ teamId: 7, permission: 'Edit', isInherited: false }]);
|
|
101
|
+
await run(live, grafanaOperations(spec).destroy(props));
|
|
102
|
+
expect(live.bodies.filter((b) => b !== undefined)).toEqual([{ permission: '' }]);
|
|
103
|
+
const gone = grafanaWith(() => []);
|
|
104
|
+
await run(gone, grafanaOperations(spec).destroy(props));
|
|
105
|
+
expect(gone.seen.every((s) => s.method === 'GET')).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Grafana.FolderPermission` — ONE team's DIRECT permission on ONE folder.
|
|
3
|
+
*
|
|
4
|
+
* Walked against `@distilled.cloud/grafana` 0.2.1 (`@homeflare/distilled-grafana`):
|
|
5
|
+
* `getResourcePermissions` (`GET /access-control/folders/{uid}`) and `setResourcePermissionsForTeam`
|
|
6
|
+
* (`POST /access-control/folders/{uid}/teams/{teamId}`).
|
|
7
|
+
*
|
|
8
|
+
* ★ ADDITIVE BY CONSTRUCTION. The per-team endpoint touches only that team's managed entry; the
|
|
9
|
+
* folder's other entries (built-in roles, admin users, other teams, anything inherited from a parent
|
|
10
|
+
* folder) are never read into a write and never changed. The whole-list `setResourcePermissions`
|
|
11
|
+
* would replace them all — deliberately not used.
|
|
12
|
+
* ★ ONLY DIRECT ENTRIES COUNT. An entry inherited from a parent folder (`isInherited`) is not this
|
|
13
|
+
* resource's object: it can't be changed here, so it never satisfies a declaration either.
|
|
14
|
+
* ★ The team is named, not numbered. Team ids differ per instance, so `teamName` is resolved through
|
|
15
|
+
* `findTeamByName` on every read and write; a missing team fails loudly with
|
|
16
|
+
* `GrafanaTeamNotFoundError` (declare the `Grafana.Team` first) instead of folding to "absent".
|
|
17
|
+
* ★ delete = set the team's permission to "" (Grafana's documented way to remove a managed entry),
|
|
18
|
+
* idempotent: an already-absent entry is a no-op (S11).
|
|
19
|
+
*/
|
|
20
|
+
import { Resource } from 'alchemy';
|
|
21
|
+
import * as Provider from 'alchemy/Provider';
|
|
22
|
+
import * as grafana from '@distilled.cloud/grafana';
|
|
23
|
+
import * as Data from 'effect/Data';
|
|
24
|
+
import * as Effect from 'effect/Effect';
|
|
25
|
+
import { type GrafanaSpec, grafanaHandlers } from './resource.ts';
|
|
26
|
+
import { findTeamByName } from './team.ts';
|
|
27
|
+
|
|
28
|
+
export type FolderPermissionLevel = 'View' | 'Edit' | 'Admin';
|
|
29
|
+
|
|
30
|
+
export interface FolderPermissionProps {
|
|
31
|
+
folderUid: string;
|
|
32
|
+
teamName: string;
|
|
33
|
+
permission: FolderPermissionLevel;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface FolderPermissionAttributes {
|
|
37
|
+
folderUid: string;
|
|
38
|
+
teamId: number;
|
|
39
|
+
teamName: string;
|
|
40
|
+
permission: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface GrafanaFolderPermission extends Resource<
|
|
44
|
+
'Grafana.FolderPermission',
|
|
45
|
+
FolderPermissionProps,
|
|
46
|
+
FolderPermissionAttributes,
|
|
47
|
+
never
|
|
48
|
+
> {}
|
|
49
|
+
|
|
50
|
+
export const GrafanaFolderPermission = Resource<GrafanaFolderPermission>(
|
|
51
|
+
'Grafana.FolderPermission',
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export class GrafanaTeamNotFoundError extends Data.TaggedError('GrafanaTeamNotFoundError')<{
|
|
55
|
+
readonly message: string;
|
|
56
|
+
}> {}
|
|
57
|
+
|
|
58
|
+
interface LivePermission {
|
|
59
|
+
readonly teamId: number;
|
|
60
|
+
readonly permission: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const resolveTeamId = (teamName: string) =>
|
|
64
|
+
findTeamByName(teamName).pipe(
|
|
65
|
+
Effect.flatMap((team) =>
|
|
66
|
+
team === undefined
|
|
67
|
+
? Effect.fail(
|
|
68
|
+
new GrafanaTeamNotFoundError({
|
|
69
|
+
message: `Grafana.FolderPermission: team '${teamName}' does not exist. Declare it with Grafana.Team first.`,
|
|
70
|
+
}),
|
|
71
|
+
)
|
|
72
|
+
: Effect.succeed(team.id),
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const setTeam = (folderUid: string, teamId: number, permission: string) =>
|
|
77
|
+
grafana.setResourcePermissionsForTeam({
|
|
78
|
+
resource: 'folders',
|
|
79
|
+
resourceID: folderUid,
|
|
80
|
+
teamID: teamId,
|
|
81
|
+
permission,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export const spec: GrafanaSpec<
|
|
85
|
+
FolderPermissionProps,
|
|
86
|
+
LivePermission,
|
|
87
|
+
FolderPermissionAttributes,
|
|
88
|
+
| grafana.SearchTeamsError
|
|
89
|
+
| grafana.GetResourcePermissionsError
|
|
90
|
+
| grafana.SetResourcePermissionsForTeamError
|
|
91
|
+
| GrafanaTeamNotFoundError
|
|
92
|
+
> = {
|
|
93
|
+
attributes: (live, props) => ({
|
|
94
|
+
folderUid: props.folderUid,
|
|
95
|
+
permission: live.permission,
|
|
96
|
+
teamId: live.teamId,
|
|
97
|
+
teamName: props.teamName,
|
|
98
|
+
}),
|
|
99
|
+
create: (props) =>
|
|
100
|
+
resolveTeamId(props.teamName).pipe(
|
|
101
|
+
Effect.flatMap((teamId) => setTeam(props.folderUid, teamId, props.permission)),
|
|
102
|
+
),
|
|
103
|
+
destroy: (props, live) => setTeam(props.folderUid, live.teamId, ''),
|
|
104
|
+
fetchLive: (props) =>
|
|
105
|
+
Effect.gen(function* () {
|
|
106
|
+
const teamId = yield* resolveTeamId(props.teamName);
|
|
107
|
+
const entries = yield* grafana.getResourcePermissions({
|
|
108
|
+
resource: 'folders',
|
|
109
|
+
resourceID: props.folderUid,
|
|
110
|
+
});
|
|
111
|
+
const direct = (entries ?? []).find(
|
|
112
|
+
(entry) => entry.teamId === teamId && entry.isInherited !== true,
|
|
113
|
+
);
|
|
114
|
+
return direct === undefined
|
|
115
|
+
? undefined
|
|
116
|
+
: { permission: direct.permission ?? '', teamId: teamId };
|
|
117
|
+
}),
|
|
118
|
+
matches: (attributes, props) => attributes.permission === props.permission,
|
|
119
|
+
update: (props, live) => setTeam(props.folderUid, live.teamId, props.permission),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const handlers = grafanaHandlers(spec);
|
|
123
|
+
|
|
124
|
+
export const GrafanaFolderPermissionProvider = () =>
|
|
125
|
+
Provider.effect(
|
|
126
|
+
GrafanaFolderPermission,
|
|
127
|
+
Effect.succeed(GrafanaFolderPermission.Provider.of(handlers)),
|
|
128
|
+
);
|
package/src/grafana/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* `Grafana.Datasource`, `Grafana.Folder`, `Grafana.Dashboard`, `Grafana.ContactPoint`,
|
|
10
10
|
* `Grafana.MuteTiming`, `Grafana.MessageTemplate`, `Grafana.AlertRuleGroup` and
|
|
11
|
-
* `Grafana.NotificationPolicy` all ship here — see docs/grafana.md, docs/grafana-folder-dashboard.md,
|
|
11
|
+
* `Grafana.NotificationPolicy`, `Grafana.Team` and `Grafana.FolderPermission` all ship here — see docs/grafana.md, docs/grafana-folder-dashboard.md,
|
|
12
12
|
* docs/grafana-alerting.md, docs/grafana-alerting-rules.md and
|
|
13
13
|
* docs/grafana-notification-policy.md for what shipped.
|
|
14
14
|
*/
|
|
@@ -46,6 +46,15 @@ export {
|
|
|
46
46
|
GrafanaFolderProvider,
|
|
47
47
|
GrafanaFolderReparentError,
|
|
48
48
|
} from './folder.ts';
|
|
49
|
+
export { type TeamAttributes, type TeamProps, GrafanaTeam, GrafanaTeamProvider } from './team.ts';
|
|
50
|
+
export {
|
|
51
|
+
type FolderPermissionAttributes,
|
|
52
|
+
type FolderPermissionLevel,
|
|
53
|
+
type FolderPermissionProps,
|
|
54
|
+
GrafanaFolderPermission,
|
|
55
|
+
GrafanaFolderPermissionProvider,
|
|
56
|
+
GrafanaTeamNotFoundError,
|
|
57
|
+
} from './folder-permission.ts';
|
|
49
58
|
export {
|
|
50
59
|
type ContactPointAttributes,
|
|
51
60
|
type ContactPointProps,
|
package/src/grafana/providers.ts
CHANGED
|
@@ -18,6 +18,8 @@ import { type GrafanaTarget, grafanaCredentials } from './credentials.ts';
|
|
|
18
18
|
import { GrafanaDashboardProvider } from './dashboard.ts';
|
|
19
19
|
import { GrafanaDatasourceProvider } from './datasource.ts';
|
|
20
20
|
import { GrafanaFolderProvider } from './folder.ts';
|
|
21
|
+
import { GrafanaFolderPermissionProvider } from './folder-permission.ts';
|
|
22
|
+
import { GrafanaTeamProvider } from './team.ts';
|
|
21
23
|
import { GrafanaMessageTemplateProvider } from './message-template.ts';
|
|
22
24
|
import { GrafanaMuteTimingProvider } from './mute-timing.ts';
|
|
23
25
|
import { GrafanaNotificationPolicyProvider } from './notification-policy.ts';
|
|
@@ -26,6 +28,8 @@ export const grafanaProviders = (target: GrafanaTarget) =>
|
|
|
26
28
|
Layer.mergeAll(
|
|
27
29
|
GrafanaDatasourceProvider(),
|
|
28
30
|
GrafanaFolderProvider(),
|
|
31
|
+
GrafanaTeamProvider(),
|
|
32
|
+
GrafanaFolderPermissionProvider(),
|
|
29
33
|
GrafanaDashboardProvider(),
|
|
30
34
|
GrafanaContactPointProvider(),
|
|
31
35
|
GrafanaMuteTimingProvider(),
|
|
@@ -29,7 +29,7 @@ export class GrafanaProvisionedObjectError extends Data.TaggedError(
|
|
|
29
29
|
}> {}
|
|
30
30
|
|
|
31
31
|
export const refuseIfProvisioned = (
|
|
32
|
-
kind: 'Grafana.Folder' | 'Grafana.Dashboard',
|
|
32
|
+
kind: 'Grafana.Folder' | 'Grafana.Dashboard' | 'Grafana.Team',
|
|
33
33
|
uid: string,
|
|
34
34
|
owner: string | undefined,
|
|
35
35
|
): Effect.Effect<never, GrafanaProvisionedObjectError> => {
|