diff --git a/lib/client.js b/lib/client.js new file mode 100644 index 0000000..80d2d66 --- /dev/null +++ b/lib/client.js @@ -0,0 +1,342 @@ +/** + * dsh-github browser half — one card in the "Plugins" settings section. + * + * The card edits the `dsh-github` settings namespace and lets the user fill + * the GitHub token directly from the settings page. The token literal never + * rides the settings document: it is written through the credentials domain, + * addressed by the reference the namespace names (`tokenRef`, default + * `GITHUB_TOKEN`), which is exactly where the host half resolves it — per + * operation, no restart needed. + * + * The card is collapsible and mirrors the host's `PluginCard` chrome one for + * one (same design tokens, same layout), the way the market card does: the + * plugins tab only lays out a flex column and dispatches + * `settings.plugin.item`, so the container is ours to draw, but drawing it + * with the same tokens is what keeps it from looking like it wandered in from + * another product. Buttons and the chevron come from the shared primitives. + * + * This file is a self-contained bundle executed by the browser module loader. + */ +window.__ModuleLoader__.load({ + id: "@perrylink/dsh-github", + factory: (require) => { + var module = { exports: {} }; + var exports = module.exports; + Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); + let react = require("react"); + let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); + let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client"); + + // ── styles (mirrors the host PluginCard / market card tokens) ──────────── + + const css = [ + "/* @perrylink/dsh-github: token configuration card */", + ".ghc-card{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-layer-3,#fff);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}", + ".ghc-card:hover{border-color:var(--dsw-alias-label-dimmed,#c8ccd4)}", + ".ghc-cardOpen{background:var(--dsw-alias-bg-layer-2,#f7f8fa);border-color:var(--dsw-alias-label-dimmed,#c8ccd4)}", + ".ghc-header{-webkit-appearance:none;appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}", + ".ghc-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f6ef7);outline-offset:-2px}", + ".ghc-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}", + ".ghc-name{color:var(--dsw-alias-label-primary,#1f2328);font-size:15px;font-weight:600;line-height:1.4}", + ".ghc-description{color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:13px;line-height:1.5}", + ".ghc-chevron{color:var(--dsw-alias-label-tertiary,#8b93a1);flex:none;transition:transform .16s;display:inline-flex}", + ".ghc-chevronOpen{transform:rotate(180deg)}", + ".ghc-pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform,#eef0f4);color:var(--dsw-alias-label-secondary,#6b7280);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}", + ".ghc-body{border-top:1px solid var(--dsw-alias-border-l2,#e5e7eb);margin:0 16px;padding-bottom:8px}", + ".ghc-readOnly{color:var(--dsw-alias-label-tertiary,#8b93a1);margin:12px 0 0;font-size:12px;line-height:1.5}", + ".ghc-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}", + ".ghc-head{align-items:center;gap:8px;display:flex}", + ".ghc-label{min-width:0;color:var(--dsw-alias-label-primary,#1f2328);flex:1;font-size:13px;font-weight:500;line-height:1.5}", + ".ghc-badges{align-items:center;gap:8px;display:inline-flex}", + ".ghc-badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform,#eef0f4);color:var(--dsw-alias-label-secondary,#6b7280);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}", + ".ghc-badgeMuted{white-space:nowrap;color:var(--dsw-alias-label-tertiary,#8b93a1);border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px}", + ".ghc-input{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-layer-3,#fff);height:34px;font:inherit;color:var(--dsw-alias-label-primary,#1f2328);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}", + ".ghc-input:focus-visible{border-color:var(--dsw-alias-brand-primary,#4f6ef7);outline:none}", + ".ghc-input:disabled{color:var(--dsw-alias-label-tertiary,#8b93a1);cursor:default}", + ".ghc-hint{color:var(--dsw-alias-label-tertiary,#8b93a1);margin:0;font-size:12px;line-height:1.5}", + ".ghc-actions{border-top:1px solid var(--dsw-alias-border-l2,#e5e7eb);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}", + ".ghc-failed{min-width:0;color:var(--dsw-alias-label-error,#dc2626);flex:1;margin:0;font-size:12px;line-height:1.5}", + ".ghc-spin{display:inline-flex;animation:ghc-spin .8s linear infinite}", + "@keyframes ghc-spin{to{transform:rotate(360deg)}}", + ].join(""); + const tagId = "@perrylink/dsh-github/github-card.css"; + if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) { + const tag = document.createElement("style"); + tag.dataset.plugin = "@perrylink/dsh-github"; + tag.dataset.pluginCss = tagId; + tag.textContent = css; + document.head.appendChild(tag); + } + + // ── locale ─────────────────────────────────────────────────────────────── + + /** English copy for the card. */ + const en = { + githubTitle: "GitHub", + githubDescription: "GitHub pull requests, issues, and CI through the agent.", + tokenLabel: "GitHub token", + tokenHint: "Stored in the credentials file, not here. Applied immediately; leave blank to keep the current token.", + tokenSet: "A token is configured.", + tokenUnset: "No token is configured; GitHub tools are unavailable until one is.", + unsaved: "Unsaved", + readOnly: "This deployment stores settings read-only.", + save: "Save", + saving: "Saving…", + discard: "Discard", + saveFailed: "The deployment did not accept this value; it was left for you to correct.", + }; + /** Simplified Chinese copy. */ + const zh = { + githubTitle: "GitHub", + githubDescription: "通过 agent 操作 GitHub 的 PR、issue 与 CI。", + tokenLabel: "GitHub Token", + tokenHint: "写入凭证文件而非设置文件;保存后立即生效。留空表示保持现有令牌。", + tokenSet: "已配置令牌。", + tokenUnset: "未配置令牌;配置之前 GitHub 工具不可用。", + unsaved: "未保存", + readOnly: "本部署的设置为只读。", + save: "保存", + saving: "保存中…", + discard: "放弃修改", + saveFailed: "本部署没有接受该值,已保留供你修改。", + }; + + // ── controller ─────────────────────────────────────────────────────────── + + /** + * Namespace of the GitHub capability. Spelled here rather than imported: + * a client package must not depend on a Host package. + */ + const GITHUB_NS = "dsh-github"; + /** Credential reference the provider resolves when the section names none. */ + const DEFAULT_TOKEN_REF = "GITHUB_TOKEN"; + + /** + * Bridges the `dsh-github` scope and the credentials domain onto the + * card. The token is the one control that does not live in the section: + * its literal never rides a response, so the card learns only whether + * one is configured and writes it through the credentials domain, + * addressed by the reference the section names. + */ + var GithubCardController = class { + scope; + api; + store; + saving = false; + failed = false; + credential = { ref: "", configured: false, writable: true }; + constructor(scope, api) { + this.scope = scope; + this.api = api; + this.store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(this.projection()); + scope.subscribe(() => { + this.readCredential(); + this.publish(); + }); + this.readCredential(); + } + /** Project the card's full state for its snapshot store. */ + projection() { + const snapshot = this.scope.getSnapshot(); + return { + available: snapshot.status === "ready", + writable: snapshot.writable, + configured: this.credential.configured, + credentialWritable: this.credential.writable, + saving: this.saving, + failed: this.failed, + ref: this.credential.ref, + }; + } + /** The credential reference the section names, or the provider default. */ + refOf() { + const snapshot = this.scope.getSnapshot(); + const declared = snapshot.value?.tokenRef; + return declared !== void 0 && declared.length > 0 ? declared : DEFAULT_TOKEN_REF; + } + /** + * Ask the credentials domain about the reference the section names. + * A response is published only while it still answers for the + * reference in force, so two reads settling out of order cannot + * clobber each other. + */ + async readCredential() { + const ref = this.refOf(); + if (ref !== this.credential.ref) { + this.credential = { ref, configured: false, writable: true }; + this.publish(); + } + let response; + try { + response = await this.api.credentials.describe({ refs: [ref] }); + } catch (_credentialReadFailure) { + return; + } + if (!response.result.ok || ref !== this.refOf()) return; + const view = response.result.value.credentials[ref]; + const next = { ref, configured: view?.configured ?? false, writable: view?.writable ?? true }; + if (next.configured === this.credential.configured && next.writable === this.credential.writable) return; + this.credential = next; + this.publish(); + } + /** + * Re-read after the Host reports a change to the reference this card + * watches — a token can be written from elsewhere, and the section + * does not change when it is. + * @param ref - the reference the Host reports as changed. + */ + refreshCredential(ref) { + if (ref !== this.credential.ref) return; + this.readCredential(); + } + /** + * Write the staged token, then re-read whether the Host now holds + * one. A blank value writes nothing, which keeps the stored key. + * @param value - the staged credential literal. + * @returns whether the Host reports a configured credential afterwards. + */ + async save(value) { + const text = (value ?? "").trim(); + if (text === "" || this.saving) return false; + this.saving = true; + this.failed = false; + this.publish(); + try { + await this.api.credentials.set({ ref: this.refOf(), value: text }); + } catch (_credentialWriteFailure) { + } + await this.readCredential(); + const landed = this.credential.configured; + this.saving = false; + this.failed = !landed; + this.publish(); + return landed; + } + /** Build the face the card's slot registration injects. */ + inject() { + return { + hooks: { githubCard: this.store }, + submit: (value) => this.save(value), + }; + } + publish() { + this.store.set(this.projection()); + } + }; + + // ── card component ─────────────────────────────────────────────────────── + + /** + * Render the GitHub card: a collapsible header naming the plugin, and — + * once expanded — the token control and the save/discard row. + * Renders nothing while the namespace is unavailable. + */ + function GithubCard(props) { + const { t } = props; + const state = props.useGithubCard((snapshot) => snapshot); + const [open, setOpen] = react.useState(false); + const [draft, setDraft] = react.useState(""); + if (!state.available) return null; + const title = t("githubTitle"); + const dirty = draft.trim() !== ""; + const saveLabel = state.saving ? t("saving") : t("save"); + return react.createElement("li", { + className: "ghc-card" + (open ? " ghc-cardOpen" : ""), + }, + react.createElement("button", { + type: "button", + className: "ghc-header", + "aria-expanded": open, + "aria-label": (open ? "Collapse" : "Expand") + ": " + title, + onClick: () => setOpen(!open), + }, + react.createElement("span", { className: "ghc-headText" }, + react.createElement("span", { className: "ghc-name" }, title), + react.createElement("span", { className: "ghc-description" }, t("githubDescription")), + ), + dirty ? react.createElement("span", { className: "ghc-pending" }, t("unsaved")) : null, + react.createElement("span", { className: "ghc-chevron" + (open ? " ghc-chevronOpen" : "") }, + react.createElement(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 14 }), + ), + ), + open ? react.createElement("div", { className: "ghc-body" }, + !state.writable ? react.createElement("p", { className: "ghc-readOnly", role: "status" }, t("readOnly")) : null, + react.createElement("div", { className: "ghc-field" }, + react.createElement("div", { className: "ghc-head" }, + react.createElement("label", { className: "ghc-label", htmlFor: "plugin-config-github-token" }, t("tokenLabel")), + react.createElement("span", { className: "ghc-badges" }, + react.createElement("span", { className: state.configured ? "ghc-badge" : "ghc-badgeMuted" }, + state.configured ? t("tokenSet") : t("tokenUnset"), + ), + ), + ), + react.createElement("input", { + id: "plugin-config-github-token", + className: "ghc-input", + type: "password", + autoComplete: "off", + value: draft, + disabled: !state.credentialWritable, + onChange: (event) => setDraft(event.target.value), + }), + react.createElement("p", { className: "ghc-hint" }, t("tokenHint")), + ), + react.createElement("div", { className: "ghc-actions" }, + state.failed ? react.createElement("p", { className: "ghc-failed", role: "status" }, t("saveFailed")) : null, + react.createElement(_deepseek_ai_dsh_client_ui_primitives.Button, { + variant: "ghost", + size: "sm", + disabled: !dirty || state.saving, + onClick: () => setDraft(""), + }, t("discard")), + react.createElement(_deepseek_ai_dsh_client_ui_primitives.Button, { + variant: "primary", + size: "sm", + disabled: !state.writable || !dirty || state.saving, + icon: state.saving ? react.createElement("span", { className: "ghc-spin" }, + react.createElement(_deepseek_ai_dsh_client_ui_primitives.IconLoadingOutline16, { size: 16 }), + ) : void 0, + onClick: async () => { + const landed = await props.submit(draft); + if (landed) setDraft(""); + }, + }, saveLabel), + ), + ) : null, + ); + } + + // ── entry ──────────────────────────────────────────────────────────────── + + /** Dictionary namespace owned by this plugin. */ + const NS = "dsh-github"; + /** Required services (cordis fiber inject). */ + const inject = ["slots", "locale", "connection", "remote", "settingsScope"]; + + /** + * Mount the GitHub configuration card into the plugins settings section. + * @param ctx - the browser plugin context. + */ + function apply(ctx) { + const { api } = ctx.get("connection"); + const t = ctx.locale.bind(NS); + ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-github: card dictionaries"); + const github = new GithubCardController(ctx.settingsScope.bind({ namespace: NS }), api); + ctx.effect(() => ctx.remote.$on("credentials/reference-updated", (ref) => { + github.refreshCredential(ref); + }), "dsh-github: credential invalidations"); + ctx.slots.inject("settings.plugin.item", function* () { + yield ctx.slots.register({ + name: "settings.plugin.item", + key: NS, + locale: NS, + inject: () => github.inject(), + }, GithubCard); + }); + } + + exports.apply = apply; + exports.inject = inject; + return module.exports; + } +}); diff --git a/lib/index.d.ts b/lib/index.d.ts index bde0a18..444e83a 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -26,6 +26,7 @@ * @module dsh-github */ import type { Context } from '@deepseek-ai/cordis'; +import z from '@deepseek-ai/schemastery'; import { type GitRunner } from './git.js'; import { type GhRunner } from './credential.js'; import { Config, type Config as PluginConfig } from './config.js'; @@ -34,6 +35,25 @@ export declare const name = "dsh-github"; export declare const inject: string[]; export { Config }; export type { Config as PluginConfig }; +/** + * User-settings namespace owned by this plugin. The card in the "Plugins" + * settings section edits this namespace; the GitHub token itself travels + * through the credentials seam (never a settings field), addressed by + * {@link GithubSettingsSchema.tokenRef}. + */ +export declare const GITHUB_SETTINGS_NAMESPACE: import("@deepseek-ai/dsh-settings").SettingsNamespace; +/** + * Settings surface for the browser configuration card. Only the fields that + * make sense to edit from the settings page are exposed here; every other + * tunable stays a cordis.yml entry config field. + */ +export declare const GithubSettingsSchema: z; + tokenSource: z<"auto" | "credentials" | "env" | "gh", "auto" | "credentials" | "env" | "gh">; +}>, Schemastery.ObjectT<{ + tokenRef: z; + tokenSource: z<"auto" | "credentials" | "env" | "gh", "auto" | "credentials" | "env" | "gh">; +}>>; /** Environment-dependent runners, injectable for tests. */ export interface PluginDeps { runGit?: GitRunner; @@ -53,7 +73,8 @@ export interface PluginDeps { */ export declare function applyWithDeps(ctx: Context, config: PluginConfig, deps?: PluginDeps): void; /** - * Apply the plugin: register tools, commands, and the write-approval gate. + * Apply the plugin: register tools, commands, the write-approval gate, and the + * user-settings namespace backing the browser configuration card. * @param ctx - plugin context; the injected services are ready at this point. * @param config - validated Schemastery configuration (defaults applied). */ diff --git a/lib/index.d.ts.map b/lib/index.d.ts.map index a451b45..37a7787 100644 --- a/lib/index.d.ts.map +++ b/lib/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,UAAU,CAAA;AACpD,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAWzD,OAAO,EAAE,MAAM,EAAE,KAAK,MAAM,IAAI,YAAY,EAAE,MAAM,aAAa,CAAA;AAEjE,eAAO,MAAM,IAAI,eAAe,CAAA;AAEhC,iFAAiF;AACjF,eAAO,MAAM,MAAM,UAA2D,CAAA;AAE9E,OAAO,EAAE,MAAM,EAAE,CAAA;AACjB,YAAY,EAAE,MAAM,IAAI,YAAY,EAAE,CAAA;AAEtC,2DAA2D;AAC3D,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB,KAAK,CAAC,EAAE,QAAQ,CAAA;IAChB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,GAAE,UAAe,QAwBtF;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,QAEvD"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAExC,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,UAAU,CAAA;AACpD,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAWzD,OAAO,EAAE,MAAM,EAAE,KAAK,MAAM,IAAI,YAAY,EAAE,MAAM,aAAa,CAAA;AAEjE,eAAO,MAAM,IAAI,eAAe,CAAA;AAEhC,iFAAiF;AACjF,eAAO,MAAM,MAAM,UAA2D,CAAA;AAE9E,OAAO,EAAE,MAAM,EAAE,CAAA;AACjB,YAAY,EAAE,MAAM,IAAI,YAAY,EAAE,CAAA;AAEtC;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,uDAAkC,CAAA;AAExE;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;GAK/B,CAAA;AAEF,2DAA2D;AAC3D,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB,KAAK,CAAC,EAAE,QAAQ,CAAA;IAChB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,GAAE,UAAe,QAwBtF;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,QASvD"} \ No newline at end of file diff --git a/lib/index.js b/lib/index.js index 15f37ac..db5a88d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,3 +1,5 @@ +import { settingsNamespace } from '@deepseek-ai/dsh-settings'; +import z from '@deepseek-ai/schemastery'; import { createState } from "./state.js"; import { runGitCli } from "./git.js"; import { runGhCli } from "./credential.js"; @@ -11,6 +13,24 @@ export const name = 'dsh-github'; /** Services this plugin requires; all exist in any profile built on dsh-base. */ export const inject = ['tools', 'commands', 'jobs', 'approval', 'credentials']; export { Config }; +/** + * User-settings namespace owned by this plugin. The card in the "Plugins" + * settings section edits this namespace; the GitHub token itself travels + * through the credentials seam (never a settings field), addressed by + * {@link GithubSettingsSchema.tokenRef}. + */ +export const GITHUB_SETTINGS_NAMESPACE = settingsNamespace('dsh-github'); +/** + * Settings surface for the browser configuration card. Only the fields that + * make sense to edit from the settings page are exposed here; every other + * tunable stays a cordis.yml entry config field. + */ +export const GithubSettingsSchema = z.object({ + tokenRef: z.string().pattern(/^[A-Za-z_][A-Za-z0-9_]*$/).default('GITHUB_TOKEN') + .description('Credential reference / environment variable name holding the token.'), + tokenSource: z.union(['auto', 'credentials', 'env', 'gh']).default('auto') + .description('Token lookup source: auto tries credentials → env → gh in order.'), +}); /** * Apply the plugin with explicit environment runners. * @@ -46,11 +66,19 @@ export function applyWithDeps(ctx, config, deps = {}) { } } /** - * Apply the plugin: register tools, commands, and the write-approval gate. + * Apply the plugin: register tools, commands, the write-approval gate, and the + * user-settings namespace backing the browser configuration card. * @param ctx - plugin context; the injected services are ready at this point. * @param config - validated Schemastery configuration (defaults applied). */ export function apply(ctx, config) { applyWithDeps(ctx, config); + // Optional settings service (present in any profile built on dsh-base): + // register the namespace the "Plugins" settings card edits. The token + // literal itself never rides this namespace — it goes through the + // credentials seam, resolved per operation. + ctx.inject(['settings'], (settingsCtx) => { + settingsCtx.settings.register(GITHUB_SETTINGS_NAMESPACE, GithubSettingsSchema); + }); } //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/lib/index.js.map b/lib/index.js.map index 7cf618e..306ec1e 100644 --- a/lib/index.js.map +++ b/lib/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/lib/types/client.d.ts b/lib/types/client.d.ts new file mode 100644 index 0000000..a381b17 --- /dev/null +++ b/lib/types/client.d.ts @@ -0,0 +1,13 @@ +/** + * Browser half of `@perrylink/dsh-github` — the GitHub token configuration + * card contributed to the "Plugins" settings section. + * @module @perrylink/dsh-github/client + */ +import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'; +/** Required services (cordis fiber inject). */ +export declare const inject: string[]; +/** + * Mount the GitHub configuration card into the plugins settings section. + * @param ctx - the browser plugin context. + */ +export declare function apply(ctx: ClientContext): void; diff --git a/package.json b/package.json index 2fe984d..734be42 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,12 @@ ".": { "types": "./lib/index.d.ts", "import": "./lib/index.js" - } + }, + "./client": { + "types": "./lib/types/client.d.ts", + "default": "./lib/client.js" + }, + "./package.json": "./package.json" }, "files": [ "lib", @@ -60,6 +65,15 @@ "dsh": { "bundle": { "patch": "./cordis.patch.yml" + }, + "client": { + "inject": [ + "@deepseek-ai/dsh-client-connection", + "@deepseek-ai/dsh-client-locale", + "@deepseek-ai/dsh-client-runtime", + "@deepseek-ai/dsh-client-ui-slots" + ], + "platform": "web" } }, "dshWorkshop": { @@ -109,6 +123,7 @@ "@deepseek-ai/dsh-credentials": ">=0.1.0-rc.8 <0.2.0", "@deepseek-ai/dsh-llm": ">=0.1.0-rc.8 <0.2.0", "@deepseek-ai/dsh-session": ">=0.1.0-rc.8 <0.2.0", + "@deepseek-ai/dsh-settings": ">=0.1.0-rc.8 <0.2.0", "@deepseek-ai/dsh-tools": ">=0.1.0-rc.8 <0.2.0" }, "devDependencies": { @@ -117,6 +132,7 @@ "@deepseek-ai/dsh-credentials": "0.1.0-rc.8", "@deepseek-ai/dsh-llm": "0.1.0-rc.8", "@deepseek-ai/dsh-session": "0.1.0-rc.8", + "@deepseek-ai/dsh-settings": "0.1.0-rc.8", "@deepseek-ai/dsh-tools": "0.1.0-rc.8", "@deepseek-ai/schemastery": "3.18.1", "@types/node": "^22.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04c4294..0a6f970 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,9 @@ importers: '@deepseek-ai/dsh-session': specifier: 0.1.0-rc.8 version: 0.1.0-rc.8(fbdb1d988541b64ff002d18c4ded7293) + '@deepseek-ai/dsh-settings': + specifier: 0.1.0-rc.8 + version: 0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/schemastery@3.18.1) '@deepseek-ai/dsh-tools': specifier: 0.1.0-rc.8 version: 0.1.0-rc.8(ac9081d1e007de8a90a707560a8ae1c3) @@ -35,7 +38,7 @@ importers: version: 22.20.1 '@vitest/coverage-v8': specifier: 3.2.0 - version: 3.2.0(vitest@3.2.7(@types/node@22.20.1)) + version: 3.2.0(supports-color@7.2.0)(vitest@3.2.7(@types/node@22.20.1)(supports-color@7.2.0)) oxlint: specifier: 0.18.1 version: 0.18.1 @@ -44,7 +47,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.2.0 - version: 3.2.7(@types/node@22.20.1) + version: 3.2.7(@types/node@22.20.1)(supports-color@7.2.0) packages: @@ -164,6 +167,14 @@ packages: '@deepseek-ai/dsh-scope': ^0.1.0-rc.8 '@deepseek-ai/dsh-typert-protocol': ^0.1.0-rc.8 + '@deepseek-ai/dsh-settings@0.1.0-rc.8': + resolution: {integrity: sha512-pqbMoiSP+Ieww9/4gcHYXcBCDlno1zYDu2OPq+5xdZ+McnK+5YBtHsiRpnR9cHQxGTH7lkfIv+adkOueQW0utg==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-brand': ^0.1.0-rc.8 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.8 + '@deepseek-ai/schemastery': ^3.18.1 + '@deepseek-ai/dsh-system-prompt@0.1.0-rc.8': resolution: {integrity: sha512-yk8Z85rzX2EwpKNWA5i+K3I3mqsIIUc9niwbDpXO8vlHvpC2qR3RNHjJIJN2EZYDqfSzTVtkbxHFsuMzDXIbew==} peerDependencies: @@ -1143,6 +1154,13 @@ snapshots: '@deepseek-ai/dsh-scope': 0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)) '@deepseek-ai/dsh-typert-protocol': 0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-settings@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/schemastery@3.18.1)': + dependencies: + '@deepseek-ai/cordis': 4.0.1(@deepseek-ai/cordis-plugin-loader@1.0.2) + '@deepseek-ai/dsh-brand': 0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-invariants': 0.1.0-rc.8(@deepseek-ai/cordis@4.0.1) + '@deepseek-ai/schemastery': 3.18.1 + '@deepseek-ai/dsh-system-prompt@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-llm@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1))))(@deepseek-ai/dsh-scope@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.8(@deepseek-ai/cordis@4.0.1)))': dependencies: '@deepseek-ai/cordis': 4.0.1(@deepseek-ai/cordis-plugin-loader@1.0.2) @@ -1414,22 +1432,22 @@ snapshots: dependencies: undici-types: 6.21.0 - '@vitest/coverage-v8@3.2.0(vitest@3.2.7(@types/node@22.20.1))': + '@vitest/coverage-v8@3.2.0(supports-color@7.2.0)(vitest@3.2.7(@types/node@22.20.1)(supports-color@7.2.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 ast-v8-to-istanbul: 0.3.12 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 + istanbul-lib-source-maps: 5.0.6(supports-color@7.2.0) istanbul-reports: 3.2.0 magic-string: 0.30.21 magicast: 0.3.5 std-env: 3.10.0 test-exclude: 7.0.2 tinyrainbow: 2.0.0 - vitest: 3.2.7(@types/node@22.20.1) + vitest: 3.2.7(@types/node@22.20.1)(supports-color@7.2.0) transitivePeerDependencies: - supports-color @@ -1529,9 +1547,11 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - debug@4.4.3: + debug@4.4.3(supports-color@7.2.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 deep-eql@5.0.2: {} @@ -1615,10 +1635,10 @@ snapshots: make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@5.0.6: + istanbul-lib-source-maps@5.0.6(supports-color@7.2.0): dependencies: '@jridgewell/trace-mapping': 0.3.31 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -1807,10 +1827,10 @@ snapshots: undici-types@6.21.0: {} - vite-node@3.2.4(@types/node@22.20.1): + vite-node@3.2.4(@types/node@22.20.1)(supports-color@7.2.0): dependencies: cac: 6.7.14 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 7.3.6(@types/node@22.20.1) @@ -1840,7 +1860,7 @@ snapshots: '@types/node': 22.20.1 fsevents: 2.3.3 - vitest@3.2.7(@types/node@22.20.1): + vitest@3.2.7(@types/node@22.20.1)(supports-color@7.2.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 @@ -1851,7 +1871,7 @@ snapshots: '@vitest/spy': 3.2.7 '@vitest/utils': 3.2.7 chai: 5.3.3 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) expect-type: 1.4.0 magic-string: 0.30.21 pathe: 2.0.3 @@ -1863,7 +1883,7 @@ snapshots: tinypool: 1.1.1 tinyrainbow: 2.0.0 vite: 7.3.6(@types/node@22.20.1) - vite-node: 3.2.4(@types/node@22.20.1) + vite-node: 3.2.4(@types/node@22.20.1)(supports-color@7.2.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.20.1 diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..d7711c5 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,395 @@ +/** + * dsh-github browser half — one card in the "Plugins" settings section. + * + * The card edits the `dsh-github` settings namespace and lets the user fill + * the GitHub token directly from the settings page. The token literal never + * rides the settings document: it is written through the credentials domain, + * addressed by the reference the namespace names (`tokenRef`, default + * `GITHUB_TOKEN`), which is exactly where the host half resolves it — per + * operation, no restart needed. + * + * The card is collapsible and mirrors the host's `PluginCard` chrome one for + * one (same design tokens, same layout), the way the market card does: the + * plugins tab only lays out a flex column and dispatches + * `settings.plugin.item`, so the container is ours to draw, but drawing it + * with the same tokens is what keeps it from looking like it wandered in from + * another product. Buttons and the chevron come from the shared primitives. + * + * The shipped `lib/client.js` is the __ModuleLoader__ bundle built from this + * module (plain ESM here; the bundle wraps it in the loader factory). The + * browser module loader executes that bundle, not this file. + * @module @perrylink/dsh-github/client + */ +import { createElement, useState, type ReactNode } from 'react' +import { + Button, IconChevronDownOutline14, IconLoadingOutline16, +} from '@deepseek-ai/dsh-client-ui-primitives' +import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client' +import type { SettingsScope, SettingsScopeSnapshot } from '@deepseek-ai/dsh-client-runtime/client' + +/** Namespace of the GitHub capability. Spelled here rather than imported: a client package must not depend on a Host package. */ +export const GITHUB_NS = 'dsh-github' +/** Credential reference the provider resolves when the section names none. */ +export const DEFAULT_TOKEN_REF = 'GITHUB_TOKEN' + +/** Locale copy the card renders. */ +export interface GithubCardLocale { + githubTitle: string + githubDescription: string + tokenLabel: string + tokenHint: string + tokenSet: string + tokenUnset: string + unsaved: string + readOnly: string + save: string + saving: string + discard: string + saveFailed: string +} + +/** English copy. */ +export const en: GithubCardLocale = { + githubTitle: 'GitHub', + githubDescription: 'GitHub pull requests, issues, and CI through the agent.', + tokenLabel: 'GitHub token', + tokenHint: 'Stored in the credentials file, not here. Applied immediately; leave blank to keep the current token.', + tokenSet: 'A token is configured.', + tokenUnset: 'No token is configured; GitHub tools are unavailable until one is.', + unsaved: 'Unsaved', + readOnly: 'This deployment stores settings read-only.', + save: 'Save', + saving: 'Saving…', + discard: 'Discard', + saveFailed: 'The deployment did not accept this value; it was left for you to correct.', +} + +/** Simplified Chinese copy. */ +export const zh: GithubCardLocale = { + githubTitle: 'GitHub', + githubDescription: '通过 agent 操作 GitHub 的 PR、issue 与 CI。', + tokenLabel: 'GitHub Token', + tokenHint: '写入凭证文件而非设置文件;保存后立即生效。留空表示保持现有令牌。', + tokenSet: '已配置令牌。', + tokenUnset: '未配置令牌;配置之前 GitHub 工具不可用。', + unsaved: '未保存', + readOnly: '本部署的设置为只读。', + save: '保存', + saving: '保存中…', + discard: '放弃修改', + saveFailed: '本部署没有接受该值,已保留供你修改。', +} + +/** The card's full state, as projected into its snapshot store. */ +export interface GithubCardState { + /** False while the namespace is not served to this client; the card renders nothing. */ + available: boolean + /** Whether the Host settings document accepts writes. */ + writable: boolean + /** Whether the referenced credential is configured. */ + configured: boolean + /** Whether the credentials domain accepts writes. */ + credentialWritable: boolean + /** Whether a save is crossing the wire. */ + saving: boolean + /** Whether the last save did not land as staged. */ + failed: boolean + /** The credential reference the card addresses. */ + ref: string +} + +/** The credentials-domain wire face the card uses. */ +export interface CredentialsApi { + describe(request: { refs: string[] }): Promise<{ + result: { ok: boolean; value: { credentials: Record } } + }> + set(request: { ref: string; value: string }): Promise +} + +/** The section's settings value shape (subset of the Host schema). */ +interface GithubSettingsSection { + tokenRef?: string +} + +/** + * Bridges the `dsh-github` scope and the credentials domain onto the card. + * The token is the one control that does not live in the section: its literal + * never rides a response, so the card learns only whether one is configured + * and writes it through the credentials domain, addressed by the reference the + * section names. + */ +export class GithubCardController { + private readonly scope: SettingsScope + private readonly api: CredentialsApi + private readonly store: SnapshotStore + private saving = false + private failed = false + private credential: { ref: string; configured: boolean; writable: boolean } = { ref: '', configured: false, writable: true } + + constructor(scope: SettingsScope, api: CredentialsApi) { + this.scope = scope + this.api = api + this.store = createSnapshotStore(this.projection()) + scope.subscribe(() => { + void this.readCredential() + this.publish() + }) + void this.readCredential() + } + + /** Project the card's full state for its snapshot store. */ + projection(): GithubCardState { + const snapshot = this.scope.getSnapshot() + return { + available: snapshot.status === 'ready', + writable: snapshot.writable, + configured: this.credential.configured, + credentialWritable: this.credential.writable, + saving: this.saving, + failed: this.failed, + ref: this.credential.ref, + } + } + + /** The credential reference the section names, or the provider default. */ + refOf(): string { + const declared = this.scope.getSnapshot().value?.tokenRef + return declared !== undefined && declared.length > 0 ? declared : DEFAULT_TOKEN_REF + } + + /** + * Ask the credentials domain about the reference the section names. A + * response is published only while it still answers for the reference in + * force, so two reads settling out of order cannot clobber each other. + */ + async readCredential(): Promise { + const ref = this.refOf() + if (ref !== this.credential.ref) { + this.credential = { ref, configured: false, writable: true } + this.publish() + } + let response + try { + response = await this.api.describe({ refs: [ref] }) + } catch { + return + } + if (!response.result.ok || ref !== this.refOf()) return + const view = response.result.value.credentials[ref] + const next = { ref, configured: view?.configured ?? false, writable: view?.writable ?? true } + if (next.configured === this.credential.configured && next.writable === this.credential.writable) return + this.credential = next + this.publish() + } + + /** + * Re-read after the Host reports a change to the reference this card + * watches — a token can be written from elsewhere, and the section does not + * change when it is. + * @param ref - the reference the Host reports as changed. + */ + refreshCredential(ref: string): void { + if (ref !== this.credential.ref) return + void this.readCredential() + } + + /** + * Write the staged token, then re-read whether the Host now holds one. A + * blank value writes nothing, which keeps the stored key. + * @param value - the staged credential literal. + * @returns whether the Host reports a configured credential afterwards. + */ + async save(value: string): Promise { + const text = (value ?? '').trim() + if (text === '' || this.saving) return false + this.saving = true + this.failed = false + this.publish() + try { + await this.api.set({ ref: this.refOf(), value: text }) + } catch { + // handled below by re-reading the configured state + } + await this.readCredential() + const landed = this.credential.configured + this.saving = false + this.failed = !landed + this.publish() + return landed + } + + /** Build the face the card's slot registration injects. */ + inject(): { hooks: { githubCard: SnapshotStore }; submit: (value: string) => Promise } { + return { + hooks: { githubCard: this.store }, + submit: (value) => this.save(value), + } + } + + private publish(): void { + this.store.set(this.projection()) + } +} + +/** Props the slot system injects into the card component. */ +export interface GithubCardProps { + t: (key: keyof GithubCardLocale) => string + useGithubCard: (selector: (snapshot: GithubCardState) => GithubCardState) => GithubCardState + submit: (value: string) => Promise +} + +/** One class name builder (static strings only — no bundler CSS modules here). */ +const cx = (...parts: Array): string => parts.filter(Boolean).join(' ') + +/** + * Render the GitHub card: a collapsible header naming the plugin, and — once + * expanded — the token control and the save/discard row. Renders nothing + * while the namespace is unavailable. + */ +export function GithubCard(props: GithubCardProps): ReactNode { + const { t } = props + const state = props.useGithubCard((snapshot) => snapshot) + const [open, setOpen] = useState(false) + const [draft, setDraft] = useState('') + if (!state.available) return null + const title = t('githubTitle') + const dirty = draft.trim() !== '' + const saveLabel = state.saving ? t('saving') : t('save') + return createElement( + 'li', + { className: cx('ghc-card', open && 'ghc-cardOpen') }, + createElement( + 'button', + { + type: 'button', + className: 'ghc-header', + 'aria-expanded': open, + 'aria-label': `${open ? 'Collapse' : 'Expand'}: ${title}`, + onClick: () => setOpen(!open), + }, + createElement( + 'span', + { className: 'ghc-headText' }, + createElement('span', { className: 'ghc-name' }, title), + createElement('span', { className: 'ghc-description' }, t('githubDescription')), + ), + dirty ? createElement('span', { className: 'ghc-pending' }, t('unsaved')) : null, + createElement( + 'span', + { className: cx('ghc-chevron', open && 'ghc-chevronOpen') }, + createElement(IconChevronDownOutline14, { size: 14 }), + ), + ), + open + ? createElement( + 'div', + { className: 'ghc-body' }, + !state.writable + ? createElement('p', { className: 'ghc-readOnly', role: 'status' }, t('readOnly')) + : null, + createElement( + 'div', + { className: 'ghc-field' }, + createElement( + 'div', + { className: 'ghc-head' }, + createElement('label', { className: 'ghc-label', htmlFor: 'plugin-config-github-token' }, t('tokenLabel')), + createElement( + 'span', + { className: 'ghc-badges' }, + createElement( + 'span', + { className: state.configured ? 'ghc-badge' : 'ghc-badgeMuted' }, + state.configured ? t('tokenSet') : t('tokenUnset'), + ), + ), + ), + createElement('input', { + id: 'plugin-config-github-token', + className: 'ghc-input', + type: 'password', + autoComplete: 'off', + value: draft, + disabled: !state.credentialWritable, + onChange: (event) => setDraft(event.target.value), + }), + createElement('p', { className: 'ghc-hint' }, t('tokenHint')), + ), + createElement( + 'div', + { className: 'ghc-actions' }, + state.failed ? createElement('p', { className: 'ghc-failed', role: 'status' }, t('saveFailed')) : null, + createElement( + Button, + { + variant: 'ghost', + size: 'sm', + disabled: !dirty || state.saving, + onClick: () => setDraft(''), + }, + t('discard'), + ), + createElement( + Button, + { + variant: 'primary', + size: 'sm', + disabled: !state.writable || !dirty || state.saving, + icon: state.saving + ? createElement('span', { className: 'ghc-spin' }, createElement(IconLoadingOutline16, { size: 16 })) + : undefined, + onClick: async () => { + const landed = await props.submit(draft) + if (landed) setDraft('') + }, + }, + saveLabel, + ), + ), + ) + : null, + ) +} + +/** Dictionary namespace owned by this plugin. */ +export const NS = 'dsh-github' +/** Required services (cordis fiber inject). */ +export const inject = ['slots', 'locale', 'connection', 'remote', 'settingsScope'] + +export interface ClientContextLike { + get(service: string): unknown + effect(disposer: () => void, label: string): void + locale: { + bind(namespace: string): (key: keyof GithubCardLocale) => string + register(namespace: string, dictionaries: Record): void + } + remote: { $on(event: string, listener: (ref: string) => void): unknown } + settingsScope: { bind(spec: { namespace: string }): SettingsScope } + slots: { + inject(slot: string, factory: () => unknown): unknown + register(options: Record, component: unknown): unknown + } +} + +/** Mount the GitHub configuration card into the plugins settings section. */ +export function apply(ctx: ClientContextLike): void { + const connection = ctx.get('connection') as { api: CredentialsApi } + const t = ctx.locale.bind(NS) + ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-github: card dictionaries') + const github = new GithubCardController(ctx.settingsScope.bind({ namespace: NS }), connection.api) + ctx.effect( + () => ctx.remote.$on('credentials/reference-updated', (ref) => github.refreshCredential(ref)), + 'dsh-github: credential invalidations', + ) + ctx.slots.inject('settings.plugin.item', function* () { + yield ctx.slots.register( + { + name: 'settings.plugin.item', + key: NS, + locale: NS, + inject: () => github.inject(), + }, + GithubCard, + ) + }) +} diff --git a/src/index.ts b/src/index.ts index 05175e1..d8f9896 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,8 @@ * @module dsh-github */ import type { Context } from '@deepseek-ai/cordis' +import { settingsNamespace } from '@deepseek-ai/dsh-settings' +import z from '@deepseek-ai/schemastery' import { createState } from './state.ts' import { runGitCli, type GitRunner } from './git.ts' import { runGhCli, type GhRunner } from './credential.ts' @@ -49,6 +51,26 @@ export const inject = ['tools', 'commands', 'jobs', 'approval', 'credentials'] export { Config } export type { Config as PluginConfig } +/** + * User-settings namespace owned by this plugin. The card in the "Plugins" + * settings section edits this namespace; the GitHub token itself travels + * through the credentials seam (never a settings field), addressed by + * {@link GithubSettingsSchema.tokenRef}. + */ +export const GITHUB_SETTINGS_NAMESPACE = settingsNamespace('dsh-github') + +/** + * Settings surface for the browser configuration card. Only the fields that + * make sense to edit from the settings page are exposed here; every other + * tunable stays a cordis.yml entry config field. + */ +export const GithubSettingsSchema = z.object({ + tokenRef: z.string().pattern(/^[A-Za-z_][A-Za-z0-9_]*$/).default('GITHUB_TOKEN') + .description('Credential reference / environment variable name holding the token.'), + tokenSource: z.union(['auto', 'credentials', 'env', 'gh']).default('auto') + .description('Token lookup source: auto tries credentials → env → gh in order.'), +}) + /** Environment-dependent runners, injectable for tests. */ export interface PluginDeps { runGit?: GitRunner @@ -94,10 +116,18 @@ export function applyWithDeps(ctx: Context, config: PluginConfig, deps: PluginDe } /** - * Apply the plugin: register tools, commands, and the write-approval gate. + * Apply the plugin: register tools, commands, the write-approval gate, and the + * user-settings namespace backing the browser configuration card. * @param ctx - plugin context; the injected services are ready at this point. * @param config - validated Schemastery configuration (defaults applied). */ export function apply(ctx: Context, config: PluginConfig) { applyWithDeps(ctx, config) + // Optional settings service (present in any profile built on dsh-base): + // register the namespace the "Plugins" settings card edits. The token + // literal itself never rides this namespace — it goes through the + // credentials seam, resolved per operation. + ctx.inject(['settings'], (settingsCtx) => { + settingsCtx.settings.register(GITHUB_SETTINGS_NAMESPACE, GithubSettingsSchema) + }) } diff --git a/tsconfig.json b/tsconfig.json index 2895adf..3dc5684 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,5 +23,5 @@ "isolatedModules": true }, "include": ["src"], - "exclude": ["node_modules", "lib", "test"] + "exclude": ["node_modules", "lib", "test", "src/client.ts"] }