@ddtcorex/dsh-maestro-config 0.1.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.
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Mark this plugin's row in the DSH settings navigation so a style block can
3
+ * replace the shell's fallback gear with the maestro glyph.
4
+ *
5
+ * DSH 0.1.x projects only `id`, `order`, and `label` from a `settings.section`
6
+ * registration and picks icons from a closed built-in list, so each external
7
+ * plugin identifies its own localized row by visible text after the dialog
8
+ * mounts. The marker owns no shell structure and is removed on disposal.
9
+ */
10
+
11
+ export const SETTINGS_NAV_MARKER = 'data-maestro-settings-nav'
12
+
13
+ /** Minimal DOM surface used here; tests inject a stub instead of `document`. */
14
+ /** The two DOM iteration shapes this module touches (querySelector results). */
15
+ type NodeSeq = Iterable<Element> & { forEach(fn: (el: Element) => void): unknown }
16
+
17
+ interface DomScope {
18
+ querySelectorAll(selector: string): NodeSeq
19
+ }
20
+
21
+ type ElementLike = Element & {
22
+ setAttribute(name: string, value: string): void
23
+ removeAttribute(name: string): void
24
+ }
25
+
26
+ export function registerSettingsNavIcon(
27
+ label: () => string,
28
+ root?: DomScope,
29
+ ): () => void {
30
+ if (typeof document === 'undefined' && root === undefined) {
31
+ // Node-side import safety (tests inject a stub root instead).
32
+ return () => {}
33
+ }
34
+ const scope = (root ?? document) as DomScope
35
+
36
+ let disposed = false
37
+
38
+ const sync = () => {
39
+ if (disposed) return
40
+ const currentLabel = label().trim()
41
+ const buttons = scope.querySelectorAll('[role="dialog"] nav button')
42
+ for (const button of Array.from(buttons)) {
43
+ const el = button as ElementLike
44
+ const matches =
45
+ currentLabel.length > 0 &&
46
+ button.textContent != null &&
47
+ button.textContent.trim() === currentLabel
48
+ if (matches) el.setAttribute(SETTINGS_NAV_MARKER, '')
49
+ else el.removeAttribute(SETTINGS_NAV_MARKER)
50
+ }
51
+ }
52
+
53
+ sync()
54
+
55
+ let observer: MutationObserver | null = null
56
+ if (typeof MutationObserver !== 'undefined') {
57
+ observer = new MutationObserver(sync)
58
+ observer.observe(document.body, { childList: true, subtree: true, characterData: true })
59
+ }
60
+
61
+ return () => {
62
+ disposed = true
63
+ if (observer !== null) observer.disconnect()
64
+ for (const element of Array.from(scope.querySelectorAll(`[${SETTINGS_NAV_MARKER}]`))) {
65
+ ;(element as ElementLike).removeAttribute(SETTINGS_NAV_MARKER)
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,13 @@
1
+ const BASE64URL_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
2
+
3
+ /** Generate an opaque 256-bit secret suitable for GitLab's webhook token. */
4
+ export function generateWebhookSecret(randomValues = crypto.getRandomValues.bind(crypto)) {
5
+ const bytes = randomValues(new Uint8Array(32))
6
+ return Array.from(bytes, (byte) => BASE64URL_ALPHABET[byte & 0b00111111]).join('')
7
+ }
8
+
9
+ /** GitLab's Merge Request webhook endpoint for the configured public hostname. */
10
+ export function gitlabWebhookUrl(hostname?: string) {
11
+ const authority = hostname?.trim().replace(/^https?:\/\//, '').replace(/\/+$/, '') || '<your-hostname>'
12
+ return `https://${authority}/hooks/gitlab-mr`
13
+ }
@@ -0,0 +1,58 @@
1
+ import type {} from '@deepseek-ai/dsh-client-connection'
2
+ import type { Context } from '@deepseek-ai/cordis'
3
+ import type { RpcErrorDetailsMap, RpcResult } from '@deepseek-ai/dsh-host-apiproxy/api'
4
+ import { createMaestroConfigService, type MaestroConfigService } from './service.ts'
5
+
6
+ export const name = 'maestro-config'
7
+ export const inject = ['connection']
8
+
9
+ const RPC_CHANNEL = '/dsh-maestro-config'
10
+
11
+ declare module '@deepseek-ai/cordis' {
12
+ interface Context {
13
+ maestroConfig: MaestroConfigService
14
+ }
15
+ }
16
+
17
+ function ok<T>(value: T): RpcResult<T> {
18
+ return { ok: true, value }
19
+ }
20
+
21
+ function fail(message: string): RpcResult<never> {
22
+ return {
23
+ ok: false,
24
+ error: {
25
+ code: 'bad-request',
26
+ message,
27
+ // Synthetic details: app-level validation error shoehorned into DSH's
28
+ // shared RPC error taxonomy (same approach across maestro packages).
29
+ details: { issues: [{ message }] } as RpcErrorDetailsMap['bad-request'],
30
+ },
31
+ }
32
+ }
33
+
34
+ /** Publish maestroConfig over the shared store + loopback RPC for clients. */
35
+ export function apply(ctx: Context): void {
36
+ const svc = createMaestroConfigService()
37
+ ctx.provide('maestroConfig', svc)
38
+ ctx.effect(() =>
39
+ ctx.connection.rpc.handle(RPC_CHANNEL, async (endpoint: string, payload: unknown): Promise<RpcResult<unknown>> => {
40
+ const body = (payload ?? {}) as { domain?: string; patch?: object }
41
+ if (endpoint === 'list') {
42
+ return ok({ domains: await svc.listDomains() })
43
+ }
44
+ if (endpoint === 'get') {
45
+ if (typeof body.domain !== 'string') return fail('domain (string) is required')
46
+ return ok(await svc.get(body.domain))
47
+ }
48
+ if (endpoint === 'set') {
49
+ if (typeof body.domain !== 'string' || typeof body.patch !== 'object' || body.patch === null) {
50
+ return fail('domain (string) and patch (object) are required')
51
+ }
52
+ await svc.set(body.domain, body.patch)
53
+ return ok(null)
54
+ }
55
+ return fail(`unknown endpoint: ${String(endpoint)}`)
56
+ }, { authority: 'loopback' })
57
+ )
58
+ }
@@ -0,0 +1,33 @@
1
+ import * as lib from '@ddtcorex/dsh-maestro-config-lib'
2
+
3
+ export interface MaestroConfigService {
4
+ /** Union of schema-registered domains and domains present in the store file. */
5
+ listDomains(): Promise<string[]>
6
+ get(domain: string): Promise<unknown>
7
+ /** Deep-merges the patch into the domain (same semantics as the lib). */
8
+ set(domain: string, patch: object): Promise<void>
9
+ onChange(cb: (domain: string) => void): () => void
10
+ }
11
+
12
+ /**
13
+ * Thin facade over @ddtcorex/dsh-maestro-config-lib. `dshHome` defaults to the
14
+ * lib's resolution (explicit > DSH_HOME env > ~/.dsh); tests inject a tmpdir.
15
+ */
16
+ export function createMaestroConfigService(opts?: { dshHome?: string }): MaestroConfigService {
17
+ const libOpts = opts?.dshHome ? { dshHome: opts.dshHome } : undefined
18
+ return {
19
+ async listDomains() {
20
+ const doc = await lib.load(libOpts)
21
+ return [...new Set([...lib.definedDomains(), ...Object.keys(doc.domains)])]
22
+ },
23
+ async get(domain) {
24
+ return lib.get(domain, libOpts)
25
+ },
26
+ async set(domain, patch) {
27
+ await lib.set(domain, patch, libOpts)
28
+ },
29
+ onChange(cb) {
30
+ return lib.onChange(cb)
31
+ },
32
+ }
33
+ }