dsh-slash-command 0.0.0-stage → 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,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/client/index.tsx", "../src/client/locales.ts", "../src/client/panel.tsx"],
4
+ "sourcesContent": ["/** dsh-slash-command client \u534A\u8FB9\uFF1A\u8BBE\u7F6E\u5F39\u7A97\u91CC\u7684\u300C\u81EA\u5B9A\u4E49\u547D\u4EE4\u300D\u5206\u533A\uFF08settings.section\uFF09\u3002 */\nimport type { Context } from '@deepseek-ai/cordis'\nimport { en, NS, zh, type LocaleKey } from './locales.ts'\nimport { SlashCommandSection } from './panel.tsx'\n\nexport const name = 'dsh-slash-command'\nexport const inject = ['slots', 'locale']\n\ntype Translate = (key: LocaleKey, params?: Record<string, unknown>) => string\n\ninterface ClientContext extends Context {\n slots: {\n inject(key: string, callback: () => unknown): () => void\n register(options: { name: string; id: string; order: number; locale: string; label: () => string }, component: unknown): () => void\n }\n locale: {\n register(ns: string, dictionaries: { zh: Record<string, string>; en: Record<string, string> }): () => void\n bind(ns: string): Translate\n }\n}\n\n// \u53EA\u7528\u539F\u751F --dsw-* token\uFF0C\u53D6\u503C\u5BF9\u9F50 ui-settings-models / ui-primitives \u7684\u8BBE\u7F6E\u9875\u6837\u5F0F\u3002\nconst css = `\n.dsc-section{display:flex;flex-direction:column;gap:12px;max-width:720px;color:var(--dsw-alias-label-primary)}\n.dsc-title{margin:0;font-size:16px;line-height:24px;font-weight:500}\n.dsc-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}\n.dsc-body{display:flex;flex-direction:column;gap:8px;margin-top:12px}\n.dsc-rows{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}\n.dsc-body>.dsc-editor{margin-top:0}\n.dsc-card{border:.5px solid var(--dsw-alias-settings-card-stroke);background:var(--dsw-alias-settings-card-fill);border-radius:var(--dsw-radius-xl);padding:12px 14px;display:flex;flex-direction:column;gap:6px;}\n.dsc-head{display:flex;align-items:center;gap:10px;min-height:28px}\n.dsc-identity{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;min-width:0}\n.dsc-name{font-family:var(--ds-font-family-code);font-size:14px;line-height:22px;font-weight:500}\n.dsc-tag{flex:none;padding:1px 6px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-xs);font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary)}\ncode.dsc-tag{font-family:var(--ds-font-family-code)}\n.dsc-desc{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}\n.dsc-actions{display:inline-flex;align-items:center;gap:4px;margin-left:auto}\n.dsc-btn{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:6px;height:36px;padding:0 14px;border:none;border-radius:var(--dsw-radius-md);background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:14px;line-height:22px;cursor:pointer}\n.dsc-btn:disabled{opacity:.4;cursor:default}\n.dsc-btn:focus-visible,.dsc-help-btn:focus-visible,.dsc-segment:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary))}\n.dsc-compact{height:28px;padding:0 10px;border-radius:var(--dsw-radius-sm);font-size:12px;line-height:18px}\n.dsc-primary{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}\n.dsc-secondary{border:.5px solid var(--dsw-alias-border-l3)}\n.dsc-danger{color:var(--dsw-alias-state-error-primary)}\n.dsc-danger-fill{background:var(--dsw-alias-state-error-primary);color:#fff}\n.dsc-add{width:100%;height:44px;border:1px dashed var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-lg)}\n@media (hover:hover){\n.dsc-primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}\n.dsc-secondary:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid)}\n.dsc-add:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}\n.dsc-danger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}\n.dsc-danger-fill:hover:not(:disabled){filter:brightness(.92)}\n.dsc-segment:hover{color:var(--dsw-alias-label-primary)}\n.dsc-help:hover .dsc-tip{display:block}\n.dsc-help-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-tertiary)}\n}\n.dsc-confirm{display:flex;align-items:center;flex-wrap:wrap;gap:8px 12px;margin-top:4px;padding:10px 12px;border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-module-platform);font-size:13px;line-height:20px}\n.dsc-editor{display:flex;flex-direction:column;gap:14px;margin-top:6px;padding:14px 16px;border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-module-platform)}\n.dsc-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px}\n.dsc-field{display:flex;flex-direction:column;gap:6px;min-width:0}\n.dsc-label{display:inline-flex;align-items:center;gap:8px;font-size:12px;line-height:18px;font-weight:500;color:var(--dsw-alias-label-secondary)}\n.dsc-input,.dsc-affix{box-sizing:border-box;width:100%;min-height:32px;padding:4px 8px;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:14px;line-height:22px;outline:none}\n.dsc-input:focus,.dsc-affix:focus-within{border-color:var(--dsw-alias-state-business-primary)}\n.dsc-input[aria-invalid],.dsc-affix:has([aria-invalid]){border-color:var(--dsw-alias-state-error-primary)}\n.dsc-input::placeholder{color:var(--dsw-alias-label-dimmed)}\n.dsc-area{resize:vertical;min-height:72px}\n.dsc-mono{font-family:var(--ds-font-family-code);font-size:13px}\n.dsc-affix{display:flex;align-items:center;gap:2px;font-family:var(--ds-font-family-code);color:var(--dsw-alias-label-tertiary)}\n.dsc-bare{flex:1;min-width:0;padding:0;border:none;outline:none;background:transparent;color:var(--dsw-alias-label-primary);font-size:14px;line-height:22px}\n.dsc-segmented{position:relative;display:inline-grid;grid-auto-flow:column;grid-auto-columns:1fr;gap:2px;align-self:flex-start;padding:4px;border-radius:var(--dsw-radius-md);background:var(--dsw-alias-interactive-bg-hover)}\n.dsc-indicator{position:absolute;top:4px;left:4px;width:calc((100% - 8px - 2px * (var(--dsc-count) - 1)) / var(--dsc-count));height:calc(100% - 8px);border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-elevation-soft);transform:translateX(calc(var(--dsc-index) * (100% + 2px)));transition:transform 160ms ease;pointer-events:none}\n.dsc-segment{position:relative;z-index:1;box-sizing:border-box;height:28px;padding:0 16px;border:0;border-radius:var(--dsw-radius-sm);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;line-height:20px;font-weight:500;cursor:pointer}\n.dsc-segment[aria-pressed=true]{color:var(--dsw-alias-label-primary)}\n.dsc-help{position:relative;display:inline-flex}\n.dsc-help-btn{box-sizing:border-box;width:16px;height:16px;padding:0;border:.5px solid var(--dsw-alias-border-l3);border-radius:50%;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:11px;line-height:15px;text-align:center;cursor:help}\n.dsc-tip{display:none;position:absolute;z-index:10;left:-4px;top:calc(100% + 6px);width:max-content;max-width:280px;padding:3px 7px;border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-tooltip-bg);color:var(--dsw-static-neutral-bluish-00,#fff);font-size:13px;line-height:20px;font-weight:400;pointer-events:none}\n.dsc-help-btn:focus-visible+.dsc-tip{display:block}\n.dsc-warn{margin:-4px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-state-warn-label)}\n.dsc-error{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-state-error-primary)}\n.dsc-notice{display:flex;align-items:center;gap:12px;font-size:13px;line-height:20px;color:var(--dsw-alias-state-error-primary)}\n.dsc-editor-actions{display:flex;justify-content:flex-end;gap:8px}\n@media (prefers-reduced-motion:reduce){.dsc-indicator{transition:none}}\n`\n\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-slash-command: dictionaries')\n const t = ctx.locale.bind(NS)\n if (typeof document !== 'undefined') {\n document.querySelectorAll('style[data-plugin=\"dsh-slash-command\"]').forEach(node => node.remove())\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-slash-command'\n tag.textContent = css\n document.head.appendChild(tag)\n }\n\n ctx.slots.inject('settings.section', () => ctx.slots.register(\n { name: 'settings.section', id: 'slash-commands', order: 25, locale: NS, label: () => t('title') },\n SlashCommandSection,\n ))\n}\n", "/** \u8BBE\u7F6E\u9875\u300C\u81EA\u5B9A\u4E49\u547D\u4EE4\u300D\u6587\u6848\u5B57\u5178\uFF08zh/en\uFF09\uFF1B`/` \u83DC\u5355\u6587\u6848\u6765\u81EA\u914D\u7F6E\u6587\u4EF6\u672C\u8EAB\uFF0C\u8FD9\u91CC\u4E0D\u7BA1\u3002 */\nexport const NS = 'dsh-slash-command'\n\nexport const zh = {\n 'title': '\u81EA\u5B9A\u4E49\u547D\u4EE4',\n 'add': '\u65B0\u589E\u547D\u4EE4',\n 'edit': '\u7F16\u8F91',\n 'editOf': '\u7F16\u8F91 /{name}',\n 'delete': '\u5220\u9664',\n 'deleteOf': '\u5220\u9664 /{name}',\n 'deleteConfirm': '\u5220\u9664 /{name}\uFF1F\u7ACB\u5373\u751F\u6548\uFF0C\u65E0\u6CD5\u64A4\u9500\u3002',\n 'cancel': '\u53D6\u6D88',\n 'save': '\u4FDD\u5B58',\n 'saving': '\u4FDD\u5B58\u4E2D\u2026',\n 'saved': '\u5DF2\u4FDD\u5B58',\n 'retry': '\u91CD\u8BD5',\n 'loadFailed': '\u8BFB\u53D6\u5931\u8D25\uFF1A{detail}',\n 'saveFailed': '\u4FDD\u5B58\u5931\u8D25\uFF1A{detail}',\n 'help': '{field}\u8BF4\u660E',\n 'field.type': '\u7C7B\u578B',\n 'field.name': '\u540D\u79F0',\n 'field.aliases': '\u522B\u540D',\n 'field.description': '\u63CF\u8FF0',\n 'field.prompt': 'Prompt',\n 'field.command': '\u547D\u4EE4',\n 'field.target': '\u76EE\u6807\u547D\u4EE4',\n 'field.cwd': '\u5DE5\u4F5C\u76EE\u5F55',\n 'field.timeoutMs': '\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09',\n 'type.prompt': 'Prompt',\n 'type.exec': 'Shell',\n 'type.alias': '\u8F6C\u53D1',\n 'tip.prompt': '$ARGUMENTS \u66FF\u6362\u4E3A\u547D\u4EE4\u540E\u7684\u53C2\u6570\uFF1B\u672A\u4F7F\u7528\u65F6\u53C2\u6570\u8FFD\u52A0\u5230\u672B\u5C3E\u3002',\n 'tip.command': '$ARGUMENTS \u4E3A\u5B8C\u6574\u53C2\u6570\uFF0C$1 $2\u2026 \u4E3A\u6309\u7A7A\u683C\u62C6\u5206\u7684\u53C2\u6570\uFF1B\u53C2\u6570\u53EA\u4F5C\u53D8\u91CF\u4F20\u5165\uFF0C\u4E0D\u4F1A\u88AB\u5F53\u4F5C\u547D\u4EE4\u6267\u884C\u3002',\n 'tip.target': '\u8F6C\u53D1\u5230\u53E6\u4E00\u6761\u659C\u6760\u547D\u4EE4\uFF0C\u53EF\u5E26\u56FA\u5B9A\u53C2\u6570\u3002$ARGUMENTS \u66FF\u6362\u4E3A\u53C2\u6570\uFF0C\u672A\u4F7F\u7528\u65F6\u53C2\u6570\u63A5\u5728\u672B\u5C3E\u3002',\n 'tip.cwd': 'workspace \u4E3A\u5F53\u524D\u4F1A\u8BDD\u76EE\u5F55\uFF0C\u4E5F\u53EF\u586B\u7EDD\u5BF9\u8DEF\u5F84\u3002',\n 'execWarn': '\u4EE5 dsh \u8FDB\u7A0B\u6743\u9650\u76F4\u63A5\u8FD0\u884C\uFF0C\u4E0D\u7ECF\u5BA1\u6279\u3002',\n 'err.type': '\u7C7B\u578B\u4E0D\u5BF9',\n 'err.required': '\u5FC5\u586B',\n 'err.pattern': '\u5C0F\u5199\u5B57\u6BCD\u5F00\u5934\uFF0C\u4EC5\u542B a-z 0-9 - _',\n 'err.duplicate': '\u5DF2\u88AB\u5360\u7528\u6216\u6307\u5411\u81EA\u8EAB',\n 'err.enum': '\u586B workspace \u6216\u7EDD\u5BF9\u8DEF\u5F84',\n 'err.range': '1\u2013600000 \u4E4B\u95F4\u7684\u6574\u6570',\n} as const\n\nexport type LocaleKey = keyof typeof zh\n\nexport const en: Record<LocaleKey, string> = {\n 'title': 'Custom commands',\n 'add': 'Add command',\n 'edit': 'Edit',\n 'editOf': 'Edit /{name}',\n 'delete': 'Delete',\n 'deleteOf': 'Delete /{name}',\n 'deleteConfirm': 'Delete /{name}? Takes effect immediately and cannot be undone.',\n 'cancel': 'Cancel',\n 'save': 'Save',\n 'saving': 'Saving\u2026',\n 'saved': 'Saved',\n 'retry': 'Retry',\n 'loadFailed': 'Load failed: {detail}',\n 'saveFailed': 'Save failed: {detail}',\n 'help': 'About {field}',\n 'field.type': 'Type',\n 'field.name': 'Name',\n 'field.aliases': 'Aliases',\n 'field.description': 'Description',\n 'field.prompt': 'Prompt',\n 'field.command': 'Command',\n 'field.target': 'Target command',\n 'field.cwd': 'Working directory',\n 'field.timeoutMs': 'Timeout (ms)',\n 'type.prompt': 'Prompt',\n 'type.exec': 'Shell',\n 'type.alias': 'Alias',\n 'tip.prompt': '$ARGUMENTS is replaced with the text after the command; if absent, that text is appended.',\n 'tip.command': '$ARGUMENTS is the full argument text; $1 $2\u2026 are the space-separated words. Arguments are passed as variables, never run as code.',\n 'tip.target': 'Forwards to another slash command, optionally with fixed arguments. $ARGUMENTS is replaced with the arguments; if absent they are appended.',\n 'tip.cwd': 'workspace is the current session directory; an absolute path also works.',\n 'execWarn': 'Runs directly with the dsh process\u2019s privileges, with no approval.',\n 'err.type': 'Wrong type',\n 'err.required': 'Required',\n 'err.pattern': 'Start with a lowercase letter; only a-z 0-9 - _',\n 'err.duplicate': 'Already used, or points to itself',\n 'err.enum': 'Use workspace or an absolute path',\n 'err.range': 'An integer from 1 to 600000',\n}\n", "/** \u8BBE\u7F6E\u9875\u300C\u81EA\u5B9A\u4E49\u547D\u4EE4\u300D\u5206\u533A\uFF1A\u547D\u4EE4\u5361\u7247\u5217\u8868 + \u5355\u5361\u884C\u5185\u7F16\u8F91\uFF1B\u6BCF\u6B21\u4FDD\u5B58/\u5220\u9664\u90FD\u6574\u8868\u5199\u56DE\u670D\u52A1\u7AEF\u3002 */\nimport { useEffect, useId, useState, type CSSProperties, type ReactElement, type ReactNode } from 'react'\nimport type { LocaleKey } from './locales.ts'\n\ntype T = (key: LocaleKey, params?: Record<string, unknown>) => string\n\ninterface Draft {\n name: string\n aliases: string\n description: string\n type: 'prompt' | 'exec' | 'alias'\n prompt: string\n target: string\n command: string\n cwd: string\n timeoutMs: string\n}\n\ninterface ApiError { index?: number; field?: string; code: string; message: string }\n\nconst blank = (): Draft => ({ name: '', aliases: '', description: '', type: 'prompt', prompt: '', target: '', command: '', cwd: '', timeoutMs: '' })\n\nconst toDraft = (c: Record<string, unknown>): Draft => ({\n name: String(c.name ?? ''),\n aliases: Array.isArray(c.aliases) ? c.aliases.join(', ') : '',\n description: String(c.description ?? ''),\n type: c.type === 'exec' || c.type === 'alias' ? c.type : 'prompt',\n prompt: String(c.prompt ?? ''),\n target: String(c.target ?? ''),\n command: String(c.command ?? ''),\n cwd: c.cwd === undefined || c.cwd === 'workspace' ? '' : String(c.cwd),\n timeoutMs: c.timeoutMs === undefined || c.timeoutMs === 30000 ? '' : String(c.timeoutMs),\n})\n\n/** \u7A7A\u5B57\u7B26\u4E32\u7701\u7565\uFF0C\u4EA4\u7ED9\u670D\u52A1\u7AEF\u8865\u9ED8\u8BA4\u503C\uFF1BtimeoutMs \u975E\u6570\u5B57\u539F\u6837\u53D1\u51FA\uFF0C\u7531\u670D\u52A1\u7AEF\u62A5 range\u3002 */\nfunction toWire(d: Draft): Record<string, unknown> {\n const out: Record<string, unknown> = {\n name: d.name.trim(),\n aliases: d.aliases.split(',').map(s => s.trim()).filter(s => s !== ''),\n description: d.description,\n type: d.type,\n }\n if (d.type === 'prompt') out.prompt = d.prompt\n else if (d.type === 'alias') out.target = d.target.trim()\n else {\n out.command = d.command\n if (d.cwd.trim() !== '') out.cwd = d.cwd.trim()\n if (d.timeoutMs.trim() !== '') out.timeoutMs = Number(d.timeoutMs)\n }\n return out\n}\n\nconst aliasList = (d: Draft): string[] => d.aliases.split(',').map(s => s.trim()).filter(s => s !== '')\n\ntype SaveResult = { ok: true } | { ok: false; errors: ApiError[]; text?: string }\n\nasync function saveAll(commands: Draft[], t: T): Promise<SaveResult> {\n try {\n const res = await fetch('/api/dsh-slash-command/save', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ commands: commands.map(toWire) }),\n })\n const body = await res.json() as { ok: boolean; errors?: ApiError[] }\n if (res.ok && body.ok) return { ok: true }\n if (res.status === 400) return { ok: false, errors: body.errors ?? [] }\n return { ok: false, errors: [], text: t('saveFailed', { detail: `HTTP ${res.status}` }) }\n } catch (e) {\n return { ok: false, errors: [], text: t('saveFailed', { detail: e instanceof Error ? e.message : String(e) }) }\n }\n}\n\nconst TYPES = ['prompt', 'exec', 'alias'] as const\n\n/** \u6253\u5F00\u4E2D\u7684\u7F16\u8F91\u5361\uFF1A`index` \u4E3A undefined \u8868\u793A\u65B0\u589E\u3002 */\ninterface Editing { index: number | undefined; draft: Draft }\n\nexport function SlashCommandSection({ t }: { t: T }): ReactElement {\n const [commands, setCommands] = useState<Draft[] | null>(null)\n const [loadError, setLoadError] = useState<string | undefined>()\n const [editing, setEditing] = useState<Editing | undefined>()\n const [deleting, setDeleting] = useState<number | undefined>()\n const [busy, setBusy] = useState(false)\n const [errors, setErrors] = useState<ApiError[]>([])\n const [failure, setFailure] = useState<string | undefined>()\n const [announce, setAnnounce] = useState('')\n\n const load = (): void => {\n setLoadError(undefined)\n fetch('/api/dsh-slash-command/read', { headers: { accept: 'application/json' }, cache: 'no-store' })\n .then(async (res) => {\n const body = await res.json() as { ok: boolean; commands?: Record<string, unknown>[]; errors?: ApiError[] }\n if (!res.ok || !body.ok) throw new Error(body.errors?.[0]?.message ?? `HTTP ${res.status}`)\n setCommands((body.commands ?? []).map(toDraft))\n })\n .catch((e: unknown) => { setLoadError(t('loadFailed', { detail: e instanceof Error ? e.message : String(e) })) })\n }\n useEffect(load, [])\n\n const reset = (): void => { setErrors([]); setFailure(undefined); setAnnounce('') }\n const openEditor = (next: Editing | undefined): void => { reset(); setDeleting(undefined); setEditing(next) }\n\n const commit = async (next: Draft[], target: number): Promise<boolean> => {\n setBusy(true)\n reset()\n const result = await saveAll(next, t)\n setBusy(false)\n if (result.ok) {\n setCommands(next)\n setAnnounce(t('saved'))\n return true\n }\n // \u53EA\u6709\u5F53\u524D\u5361\u7247\u7684\u5B57\u6BB5\u9519\u8BEF\u80FD\u5C31\u5730\u663E\u793A\uFF1B\u5176\u4F59\uFF08\u7406\u8BBA\u4E0A\u4E0D\u4F1A\u51FA\u73B0\uFF09\u5E76\u5165\u5361\u7247\u9876\u90E8\u7684\u5931\u8D25\u884C\u3002\n setErrors(result.errors.filter(e => e.index === target && e.field !== undefined))\n const stray = result.errors.filter(e => e.index !== target || e.field === undefined)\n setFailure(result.text ?? (stray.length > 0 ? t('saveFailed', { detail: stray.map(e => e.message).join('; ') }) : undefined))\n return false\n }\n\n const saveEditing = async (): Promise<void> => {\n if (commands === null || editing === undefined) return\n const index = editing.index ?? commands.length\n const next = editing.index === undefined\n ? [...commands, editing.draft]\n : commands.map((c, i) => (i === editing.index ? editing.draft : c))\n if (await commit(next, index)) setEditing(undefined)\n }\n\n const confirmDelete = async (index: number): Promise<void> => {\n if (commands === null) return\n if (await commit(commands.filter((_, i) => i !== index), -1)) setDeleting(undefined)\n }\n\n const editor = (key: string): ReactNode => editing && (\n <Editor\n key={key}\n t={t}\n draft={editing.draft}\n errors={errors}\n failure={failure}\n busy={busy}\n onChange={p => { setEditing({ ...editing, draft: { ...editing.draft, ...p } }); setErrors(es => es.filter(e => !(e.field! in p))) }}\n onCancel={() => openEditor(undefined)}\n onSave={() => { void saveEditing() }}\n />\n )\n\n return (\n <div className=\"dsc-section\">\n <h2 className=\"dsc-title\">{t('title')}</h2>\n <span className=\"dsc-sr\" role=\"status\">{announce}</span>\n {loadError !== undefined && (\n <div className=\"dsc-notice\" role=\"alert\">\n <span>{loadError}</span>\n <button type=\"button\" className=\"dsc-btn dsc-secondary dsc-compact\" onClick={load}>{t('retry')}</button>\n </div>\n )}\n {commands !== null && (\n <div className=\"dsc-body\">\n {commands.length > 0 && (\n <ul className=\"dsc-rows\">\n {commands.map((c, i) => {\n const open = editing?.index === i\n return (\n <li key={i} className=\"dsc-card\">\n <div className=\"dsc-head\">\n <span className=\"dsc-identity\">\n <code className=\"dsc-name\">/{c.name}</code>\n {aliasList(c).map(a => <code key={a} className=\"dsc-tag\">/{a}</code>)}\n {c.type === 'exec' && <span className=\"dsc-tag\">{t('type.exec')}</span>}\n {c.type === 'alias' && <code className=\"dsc-tag\">\u2192 {c.target.startsWith('/') ? c.target : `/${c.target}`}</code>}\n </span>\n {!open && deleting !== i && (\n <span className=\"dsc-actions\">\n <button type=\"button\" className=\"dsc-btn dsc-secondary dsc-compact\" aria-label={t('editOf', { name: c.name })}\n disabled={busy} onClick={() => openEditor({ index: i, draft: c })}>{t('edit')}</button>\n <button type=\"button\" className=\"dsc-btn dsc-danger dsc-compact\" aria-label={t('deleteOf', { name: c.name })}\n disabled={busy} onClick={() => { openEditor(undefined); setDeleting(i) }}>{t('delete')}</button>\n </span>\n )}\n </div>\n {!open && <p className=\"dsc-desc\">{c.description}</p>}\n {deleting === i && (\n <div className=\"dsc-confirm\" role=\"alertdialog\" aria-label={t('deleteOf', { name: c.name })}>\n <span>{t('deleteConfirm', { name: c.name })}</span>\n {failure !== undefined && <span className=\"dsc-error\" role=\"alert\">{failure}</span>}\n <span className=\"dsc-actions\">\n <button type=\"button\" className=\"dsc-btn dsc-secondary dsc-compact\" disabled={busy} autoFocus\n onClick={() => { setDeleting(undefined); reset() }}>{t('cancel')}</button>\n <button type=\"button\" className=\"dsc-btn dsc-danger-fill dsc-compact\" disabled={busy}\n onClick={() => { void confirmDelete(i) }}>{busy ? t('saving') : t('delete')}</button>\n </span>\n </div>\n )}\n {open && editor(`edit-${i}`)}\n </li>\n )\n })}\n </ul>\n )}\n {editing?.index === undefined && editing !== undefined\n ? editor('add')\n : (\n <button type=\"button\" className=\"dsc-btn dsc-add\" disabled={busy} onClick={() => openEditor({ index: undefined, draft: blank() })}>\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" aria-hidden=\"true\"><path d=\"M7 2v10M2 7h10\" stroke=\"currentColor\" strokeWidth=\"1.4\" strokeLinecap=\"round\" /></svg>\n {t('add')}\n </button>\n )}\n </div>\n )}\n </div>\n )\n}\n\ninterface EditorProps {\n t: T\n draft: Draft\n errors: ApiError[]\n failure: string | undefined\n busy: boolean\n onChange: (p: Partial<Draft>) => void\n onCancel: () => void\n onSave: () => void\n}\n\nfunction Editor({ t, draft, errors, failure, busy, onChange, onCancel, onSave }: EditorProps): ReactElement {\n const base = useId()\n const errorOf = (field: keyof Draft): ApiError | undefined => errors.find(e => e.field === field)\n const field = (key: keyof Draft, control: (props: { id: string; 'aria-invalid'?: true; 'aria-describedby'?: string }) => ReactNode, tip?: LocaleKey): ReactElement => {\n const id = `${base}-${key}`\n const err = errorOf(key)\n const label = t(`field.${key}` as LocaleKey)\n return (\n <div className=\"dsc-field\">\n <span className=\"dsc-label\">\n <label htmlFor={id}>{label}</label>\n {tip && <Help label={t('help', { field: label })} text={t(tip)} />}\n </span>\n {control({ id, ...err ? { 'aria-invalid': true, 'aria-describedby': `${id}-err` } : {} })}\n {err && <span id={`${id}-err`} className=\"dsc-error\">{t(`err.${err.code}` as LocaleKey)}</span>}\n </div>\n )\n }\n const text = (key: keyof Draft, extra: { placeholder?: string; inputMode?: 'numeric'; mono?: boolean } = {}) =>\n (p: { id: string }): ReactNode => (\n <input {...p} className={`dsc-input${extra.mono ? ' dsc-mono' : ''}`} value={draft[key]} placeholder={extra.placeholder}\n inputMode={extra.inputMode} spellCheck={false} autoComplete=\"off\" onChange={e => onChange({ [key]: e.target.value })} />\n )\n const area = (key: 'prompt' | 'command', rows: number) => (p: { id: string }): ReactNode => (\n <textarea {...p} className={`dsc-input dsc-area${key === 'command' ? ' dsc-mono' : ''}`} rows={rows} value={draft[key]}\n spellCheck={false} onChange={e => onChange({ [key]: e.target.value })} />\n )\n\n return (\n <form className=\"dsc-editor\" onSubmit={(e) => { e.preventDefault(); onSave() }}>\n <div className=\"dsc-segmented\" role=\"group\" aria-label={t('field.type')} style={{ '--dsc-index': TYPES.indexOf(draft.type), '--dsc-count': TYPES.length } as CSSProperties}>\n <span className=\"dsc-indicator\" aria-hidden=\"true\" />\n {TYPES.map(v => (\n <button key={v} type=\"button\" aria-pressed={draft.type === v} className=\"dsc-segment\" onClick={() => onChange({ type: v })}>\n {t(`type.${v}`)}\n </button>\n ))}\n </div>\n <div className=\"dsc-grid\">\n {field('name', p => (\n <span className=\"dsc-affix\">\n <span aria-hidden=\"true\">/</span>\n <input {...p} className=\"dsc-bare dsc-mono\" value={draft.name} spellCheck={false} autoComplete=\"off\" autoFocus\n onChange={e => onChange({ name: e.target.value })} />\n </span>\n ))}\n {field('aliases', text('aliases', { placeholder: 'rv, r', mono: true }))}\n </div>\n {field('description', text('description'))}\n {draft.type === 'prompt' && field('prompt', area('prompt', 5), 'tip.prompt')}\n {draft.type === 'alias' && field('target', text('target', { placeholder: '/compact', mono: true }), 'tip.target')}\n {draft.type === 'exec' && (\n <>\n {field('command', area('command', 3), 'tip.command')}\n <div className=\"dsc-grid\">\n {field('cwd', text('cwd', { placeholder: 'workspace', mono: true }), 'tip.cwd')}\n {field('timeoutMs', text('timeoutMs', { placeholder: '30000', inputMode: 'numeric' }))}\n </div>\n <p className=\"dsc-warn\">{t('execWarn')}</p>\n </>\n )}\n {failure !== undefined && <p className=\"dsc-error\" role=\"alert\">{failure}</p>}\n <div className=\"dsc-editor-actions\">\n <button type=\"button\" className=\"dsc-btn dsc-secondary\" disabled={busy} onClick={onCancel}>{t('cancel')}</button>\n <button type=\"submit\" className=\"dsc-btn dsc-primary\" disabled={busy || draft.name.trim() === ''}>{busy ? t('saving') : t('save')}</button>\n </div>\n </form>\n )\n}\n\n/** \u539F\u751F\u8BBE\u7F6E\u9875\u540C\u6B3E\u300C?\u300D\u8BF4\u660E\u94AE\uFF1A\u60AC\u505C\u6216\u952E\u76D8\u805A\u7126\u65F6\u663E\u793A\u6C14\u6CE1\uFF0C\u8BFB\u5C4F\u7ECF aria-describedby \u8BFB\u51FA\u3002 */\nfunction Help({ label, text }: { label: string; text: string }): ReactElement {\n const id = useId()\n return (\n <span className=\"dsc-help\">\n <button type=\"button\" className=\"dsc-help-btn\" aria-label={label} aria-describedby={id}>?</button>\n <span id={id} role=\"tooltip\" className=\"dsc-tip\">{text}</span>\n </span>\n )\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["field"]
7
+ }
package/lib/core.js ADDED
@@ -0,0 +1,127 @@
1
+ // src/core.ts
2
+ var NAME_RE = /^[a-z][a-z0-9_-]*$/;
3
+ var DEFAULT_TIMEOUT_MS = 3e4;
4
+ var MAX_TIMEOUT_MS = 6e5;
5
+ var OUTPUT_LIMIT = 16 * 1024;
6
+ var TARGET_RE = /^\/?([a-z][a-z0-9_-]*)(?:\s[\s\S]*)?$/;
7
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
8
+ var nonEmpty = (v) => typeof v === "string" && v.trim() !== "";
9
+ function validateConfig(raw) {
10
+ const commands = [];
11
+ const errors = [];
12
+ if (!isObj(raw) || !Array.isArray(raw.commands)) {
13
+ return { commands, errors: [{ code: "type", field: "commands", message: "commands must be an array" }] };
14
+ }
15
+ const seen = /* @__PURE__ */ new Set();
16
+ raw.commands.forEach((item, index) => {
17
+ const bad = [];
18
+ const fail = (field, code, message) => {
19
+ bad.push({ index, field, code, message });
20
+ };
21
+ if (!isObj(item)) {
22
+ errors.push({ index, code: "type", message: "command must be an object" });
23
+ return;
24
+ }
25
+ const names = [];
26
+ if (typeof item.name !== "string" || item.name === "") fail("name", "required", "name is required");
27
+ else if (!NAME_RE.test(item.name)) fail("name", "pattern", `name "${item.name}" must match ${String(NAME_RE)}`);
28
+ else names.push(item.name);
29
+ const aliases = [];
30
+ if (item.aliases !== void 0) {
31
+ if (!Array.isArray(item.aliases)) fail("aliases", "type", "aliases must be an array of strings");
32
+ else {
33
+ for (const a of item.aliases) {
34
+ if (typeof a !== "string" || !NAME_RE.test(a)) fail("aliases", "pattern", `alias "${String(a)}" must match ${String(NAME_RE)}`);
35
+ else aliases.push(a);
36
+ }
37
+ }
38
+ }
39
+ const own = [...names, ...aliases];
40
+ own.forEach((n, i) => {
41
+ if (seen.has(n) || own.indexOf(n) !== i) fail(n === item.name ? "name" : "aliases", "duplicate", `"${n}" is already used`);
42
+ });
43
+ if (!nonEmpty(item.description)) fail("description", "required", "description is required");
44
+ if (item.type !== "prompt" && item.type !== "exec" && item.type !== "alias") fail("type", "enum", 'type must be "prompt", "exec" or "alias"');
45
+ else if (item.type === "prompt" && !nonEmpty(item.prompt)) fail("prompt", "required", "prompt is required for prompt commands");
46
+ else if (item.type === "exec" && !nonEmpty(item.command)) fail("command", "required", "command is required for exec commands");
47
+ else if (item.type === "alias") {
48
+ const m = typeof item.target === "string" ? TARGET_RE.exec(item.target.trim()) : null;
49
+ if (!nonEmpty(item.target)) fail("target", "required", "target is required for alias commands");
50
+ else if (m === null) fail("target", "pattern", `target "${String(item.target)}" must be a slash command like /compact`);
51
+ else if (own.includes(m[1])) fail("target", "duplicate", `alias /${m[1]} cannot target itself`);
52
+ }
53
+ if (item.cwd !== void 0 && !(typeof item.cwd === "string" && (item.cwd === "workspace" || item.cwd.startsWith("/")))) {
54
+ fail("cwd", "enum", 'cwd must be "workspace" or an absolute path');
55
+ }
56
+ if (item.timeoutMs !== void 0 && !(typeof item.timeoutMs === "number" && Number.isInteger(item.timeoutMs) && item.timeoutMs >= 1 && item.timeoutMs <= MAX_TIMEOUT_MS)) {
57
+ fail("timeoutMs", "range", `timeoutMs must be an integer in 1..${MAX_TIMEOUT_MS}`);
58
+ }
59
+ if (bad.length > 0) {
60
+ errors.push(...bad);
61
+ return;
62
+ }
63
+ for (const n of own) seen.add(n);
64
+ const cmd = {
65
+ name: item.name,
66
+ aliases,
67
+ description: item.description.trim(),
68
+ type: item.type
69
+ };
70
+ if (cmd.type === "prompt") cmd.prompt = item.prompt;
71
+ else if (cmd.type === "alias") cmd.target = normalizeTarget(item.target);
72
+ else {
73
+ cmd.command = item.command;
74
+ cmd.cwd = item.cwd ?? "workspace";
75
+ cmd.timeoutMs = item.timeoutMs ?? DEFAULT_TIMEOUT_MS;
76
+ }
77
+ commands.push(cmd);
78
+ });
79
+ return { commands, errors };
80
+ }
81
+ var expandNames = (cmd) => [cmd.name, ...cmd.aliases];
82
+ function composePrompt(prompt, rawInput) {
83
+ const args = rawInput.trim();
84
+ if (prompt.includes("$ARGUMENTS")) return prompt.replaceAll("$ARGUMENTS", () => args);
85
+ return args === "" ? prompt : `${prompt}
86
+
87
+ ${args}`;
88
+ }
89
+ var normalizeTarget = (target) => {
90
+ const t = target.trim();
91
+ return t.startsWith("/") ? t : `/${t}`;
92
+ };
93
+ function composeAlias(target, rawInput) {
94
+ const args = rawInput.trim();
95
+ if (target.includes("$ARGUMENTS")) return target.replaceAll("$ARGUMENTS", () => args).trim();
96
+ return args === "" ? target : `${target} ${args}`;
97
+ }
98
+ function execArgs(rawInput) {
99
+ const args = rawInput.trim();
100
+ return { env: { ARGUMENTS: args }, argv: args === "" ? [] : args.split(/\s+/) };
101
+ }
102
+ function clip(text, limit) {
103
+ const buf = Buffer.from(text, "utf8");
104
+ if (buf.length <= limit) return text;
105
+ return `${buf.subarray(0, limit).toString("utf8").replace(/�$/, "")}
106
+ \u2026[truncated]`;
107
+ }
108
+ function formatExecResult(o) {
109
+ const output = clip(`${o.stdout}${o.stderr}`.replace(/\s+$/, ""), OUTPUT_LIMIT);
110
+ if (o.exit === "0") return output === "" ? "exit=0" : output;
111
+ return output === "" ? `exit=${o.exit}` : `${output}
112
+ exit=${o.exit}`;
113
+ }
114
+ export {
115
+ DEFAULT_TIMEOUT_MS,
116
+ MAX_TIMEOUT_MS,
117
+ NAME_RE,
118
+ OUTPUT_LIMIT,
119
+ composeAlias,
120
+ composePrompt,
121
+ execArgs,
122
+ expandNames,
123
+ formatExecResult,
124
+ normalizeTarget,
125
+ validateConfig
126
+ };
127
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/core.ts"],
4
+ "sourcesContent": ["/** \u914D\u7F6E\u89E3\u6790/\u6821\u9A8C\u4E0E\u6587\u672C\u62FC\u88C5\uFF1A\u7EAF\u51FD\u6570\uFF0C\u65E0 host \u4F9D\u8D56\uFF0C\u6D4B\u8BD5\u76F4\u63A5\u5F15\u7528\u3002 */\n\nexport const NAME_RE = /^[a-z][a-z0-9_-]*$/\nexport const DEFAULT_TIMEOUT_MS = 30_000\nexport const MAX_TIMEOUT_MS = 600_000\nexport const OUTPUT_LIMIT = 16 * 1024\n\nexport interface SlashCommand {\n name: string\n aliases: string[]\n description: string\n type: 'prompt' | 'exec' | 'alias'\n prompt?: string\n command?: string\n /** alias \u578B\uFF1A\u53E6\u4E00\u6761\u659C\u6760\u547D\u4EE4\uFF0C\u5982 `/compact` \u6216 `/review \u53EA\u770B src`\u3002 */\n target?: string\n cwd?: string\n timeoutMs?: number\n}\n\n/** `index` \u6307\u5411 commands \u6570\u7EC4\u4E0B\u6807\uFF1B\u7F3A\u7701\u8868\u793A\u9876\u5C42\u9519\u8BEF\u3002 */\nexport interface FieldError {\n index?: number\n field?: string\n code: 'type' | 'required' | 'pattern' | 'duplicate' | 'enum' | 'range'\n message: string\n}\n\n/** alias \u76EE\u6807\uFF1A`/name` \u540E\u53EF\u8DDF\u56FA\u5B9A\u53C2\u6570\uFF1B\u5F00\u5934\u7684 `/` \u53EF\u7701\u7565\u3002 */\nconst TARGET_RE = /^\\/?([a-z][a-z0-9_-]*)(?:\\s[\\s\\S]*)?$/\n\nconst isObj = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v)\nconst nonEmpty = (v: unknown): v is string => typeof v === 'string' && v.trim() !== ''\n\n/**\n * \u6821\u9A8C\u5E76\u89C4\u8303\u5316\u914D\u7F6E\u3002\u975E\u6CD5\u6761\u76EE\u4E0D\u4F1A\u51FA\u73B0\u5728 `commands` \u91CC\uFF08\u6574\u6761\u4E22\u5F03\uFF09\uFF0C\u539F\u56E0\u5199\u5165 `errors`\u3002\n * \u540D\u5B57\u4E0E\u522B\u540D\u5171\u7528\u540C\u4E00\u547D\u540D\u7A7A\u95F4\uFF0C\u5148\u51FA\u73B0\u8005\u80DC\uFF0C\u540E\u8005\u62A5 duplicate\u3002\n */\nexport function validateConfig(raw: unknown): { commands: SlashCommand[]; errors: FieldError[] } {\n const commands: SlashCommand[] = []\n const errors: FieldError[] = []\n if (!isObj(raw) || !Array.isArray(raw.commands)) {\n return { commands, errors: [{ code: 'type', field: 'commands', message: 'commands must be an array' }] }\n }\n const seen = new Set<string>()\n raw.commands.forEach((item: unknown, index) => {\n const bad: FieldError[] = []\n const fail = (field: string, code: FieldError['code'], message: string): void => {\n bad.push({ index, field, code, message })\n }\n if (!isObj(item)) {\n errors.push({ index, code: 'type', message: 'command must be an object' })\n return\n }\n const names: string[] = []\n if (typeof item.name !== 'string' || item.name === '') fail('name', 'required', 'name is required')\n else if (!NAME_RE.test(item.name)) fail('name', 'pattern', `name \"${item.name}\" must match ${String(NAME_RE)}`)\n else names.push(item.name)\n const aliases: string[] = []\n if (item.aliases !== undefined) {\n if (!Array.isArray(item.aliases)) fail('aliases', 'type', 'aliases must be an array of strings')\n else {\n for (const a of item.aliases as unknown[]) {\n if (typeof a !== 'string' || !NAME_RE.test(a)) fail('aliases', 'pattern', `alias \"${String(a)}\" must match ${String(NAME_RE)}`)\n else aliases.push(a)\n }\n }\n }\n const own = [...names, ...aliases]\n own.forEach((n, i) => {\n if (seen.has(n) || own.indexOf(n) !== i) fail(n === item.name ? 'name' : 'aliases', 'duplicate', `\"${n}\" is already used`)\n })\n if (!nonEmpty(item.description)) fail('description', 'required', 'description is required')\n if (item.type !== 'prompt' && item.type !== 'exec' && item.type !== 'alias') fail('type', 'enum', 'type must be \"prompt\", \"exec\" or \"alias\"')\n else if (item.type === 'prompt' && !nonEmpty(item.prompt)) fail('prompt', 'required', 'prompt is required for prompt commands')\n else if (item.type === 'exec' && !nonEmpty(item.command)) fail('command', 'required', 'command is required for exec commands')\n else if (item.type === 'alias') {\n const m = typeof item.target === 'string' ? TARGET_RE.exec(item.target.trim()) : null\n if (!nonEmpty(item.target)) fail('target', 'required', 'target is required for alias commands')\n else if (m === null) fail('target', 'pattern', `target \"${String(item.target)}\" must be a slash command like /compact`)\n else if (own.includes(m[1]!)) fail('target', 'duplicate', `alias /${m[1]} cannot target itself`)\n }\n if (item.cwd !== undefined && !(typeof item.cwd === 'string' && (item.cwd === 'workspace' || item.cwd.startsWith('/')))) {\n fail('cwd', 'enum', 'cwd must be \"workspace\" or an absolute path')\n }\n if (item.timeoutMs !== undefined\n && !(typeof item.timeoutMs === 'number' && Number.isInteger(item.timeoutMs) && item.timeoutMs >= 1 && item.timeoutMs <= MAX_TIMEOUT_MS)) {\n fail('timeoutMs', 'range', `timeoutMs must be an integer in 1..${MAX_TIMEOUT_MS}`)\n }\n if (bad.length > 0) {\n errors.push(...bad)\n return\n }\n for (const n of own) seen.add(n)\n const cmd: SlashCommand = {\n name: item.name as string,\n aliases,\n description: (item.description as string).trim(),\n type: item.type as 'prompt' | 'exec',\n }\n if (cmd.type === 'prompt') cmd.prompt = item.prompt as string\n else if (cmd.type === 'alias') cmd.target = normalizeTarget(item.target as string)\n else {\n cmd.command = item.command as string\n cmd.cwd = (item.cwd as string | undefined) ?? 'workspace'\n cmd.timeoutMs = (item.timeoutMs as number | undefined) ?? DEFAULT_TIMEOUT_MS\n }\n commands.push(cmd)\n })\n return { commands, errors }\n}\n\n/** \u540D\u5B57 + \u522B\u540D\u5C55\u5F00\u6210\u8981\u6CE8\u518C\u7684\u5168\u90E8\u5165\u53E3\u540D\u3002 */\nexport const expandNames = (cmd: SlashCommand): string[] => [cmd.name, ...cmd.aliases]\n\n/** `$ARGUMENTS` \u539F\u4F4D\u66FF\u6362\uFF1B\u6CA1\u6709\u5360\u4F4D\u7B26\u4E14\u6709\u53C2\u6570\u65F6\u4EE5\u7A7A\u884C\u62FC\u5230\u672B\u5C3E\u3002 */\nexport function composePrompt(prompt: string, rawInput: string): string {\n const args = rawInput.trim()\n if (prompt.includes('$ARGUMENTS')) return prompt.replaceAll('$ARGUMENTS', () => args)\n return args === '' ? prompt : `${prompt}\\n\\n${args}`\n}\n\n/** \u7EDF\u4E00\u6210 `/name args` \u5F62\u5F0F\u3002 */\nexport const normalizeTarget = (target: string): string => {\n const t = target.trim()\n return t.startsWith('/') ? t : `/${t}`\n}\n\n/** alias \u5C55\u5F00\uFF1A`$ARGUMENTS` \u539F\u4F4D\u66FF\u6362\uFF1B\u5426\u5219\u53C2\u6570\u4EE5\u7A7A\u683C\u63A5\u5728\u76EE\u6807\u547D\u4EE4\u540E\u3002 */\nexport function composeAlias(target: string, rawInput: string): string {\n const args = rawInput.trim()\n if (target.includes('$ARGUMENTS')) return target.replaceAll('$ARGUMENTS', () => args).trim()\n return args === '' ? target : `${target} ${args}`\n}\n\n/**\n * exec \u53C2\u6570\u4EE5\u53D8\u91CF\u5F62\u5F0F\u4F20\u5165\u800C\u975E\u62FC\u8FDB\u811A\u672C\uFF08\u4E0D\u4F1A\u88AB\u5F53\u6210\u4EE3\u7801\u6267\u884C\uFF09\uFF1A\n * `$ARGUMENTS` = \u5B8C\u6574\u53C2\u6570\uFF0C`$1 $2 \u2026` / `\"$@\"` = \u6309\u7A7A\u767D\u62C6\u5206\u7684\u53C2\u6570\u3002\n */\nexport function execArgs(rawInput: string): { env: Record<string, string>; argv: string[] } {\n const args = rawInput.trim()\n return { env: { ARGUMENTS: args }, argv: args === '' ? [] : args.split(/\\s+/) }\n}\n\n/** \u6309 UTF-8 \u5B57\u8282\u622A\u65AD\uFF08\u4E0D\u62C6\u5B57\u7B26\uFF09\u3002 */\nfunction clip(text: string, limit: number): string {\n const buf = Buffer.from(text, 'utf8')\n if (buf.length <= limit) return text\n return `${buf.subarray(0, limit).toString('utf8').replace(/\uFFFD$/, '')}\\n\u2026[truncated]`\n}\n\nexport interface ExecOutcome {\n exit: string\n stdout: string\n stderr: string\n}\n\n/**\n * exec \u7ED3\u679C\u76F4\u63A5\u56DE\u663E\u5728\u547D\u4EE4\u5361\u7247\u91CC\uFF08\u4E0D\u8FDB\u6A21\u578B\uFF09\uFF1Astdout + stderr \u5408\u8BA1\u622A\u65AD\u5230 16KB\uFF1B\n * \u975E\u96F6\u9000\u51FA/\u8D85\u65F6\u5728\u672B\u5C3E\u9644 `exit=N`\uFF0C\u65E0\u8F93\u51FA\u65F6\u53EA\u7ED9 exit\u3002\n */\nexport function formatExecResult(o: ExecOutcome): string {\n const output = clip(`${o.stdout}${o.stderr}`.replace(/\\s+$/, ''), OUTPUT_LIMIT)\n if (o.exit === '0') return output === '' ? 'exit=0' : output\n return output === '' ? `exit=${o.exit}` : `${output}\\nexit=${o.exit}`\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
package/lib/index.js ADDED
@@ -0,0 +1,309 @@
1
+ // src/index.ts
2
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
3
+ import { execFile } from "node:child_process";
4
+ import { watch } from "node:fs";
5
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
6
+ import { homedir } from "node:os";
7
+ import { basename, dirname, join } from "node:path";
8
+
9
+ // src/core.ts
10
+ var NAME_RE = /^[a-z][a-z0-9_-]*$/;
11
+ var DEFAULT_TIMEOUT_MS = 3e4;
12
+ var MAX_TIMEOUT_MS = 6e5;
13
+ var OUTPUT_LIMIT = 16 * 1024;
14
+ var TARGET_RE = /^\/?([a-z][a-z0-9_-]*)(?:\s[\s\S]*)?$/;
15
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
16
+ var nonEmpty = (v) => typeof v === "string" && v.trim() !== "";
17
+ function validateConfig(raw) {
18
+ const commands = [];
19
+ const errors = [];
20
+ if (!isObj(raw) || !Array.isArray(raw.commands)) {
21
+ return { commands, errors: [{ code: "type", field: "commands", message: "commands must be an array" }] };
22
+ }
23
+ const seen = /* @__PURE__ */ new Set();
24
+ raw.commands.forEach((item, index) => {
25
+ const bad = [];
26
+ const fail = (field, code, message) => {
27
+ bad.push({ index, field, code, message });
28
+ };
29
+ if (!isObj(item)) {
30
+ errors.push({ index, code: "type", message: "command must be an object" });
31
+ return;
32
+ }
33
+ const names = [];
34
+ if (typeof item.name !== "string" || item.name === "") fail("name", "required", "name is required");
35
+ else if (!NAME_RE.test(item.name)) fail("name", "pattern", `name "${item.name}" must match ${String(NAME_RE)}`);
36
+ else names.push(item.name);
37
+ const aliases = [];
38
+ if (item.aliases !== void 0) {
39
+ if (!Array.isArray(item.aliases)) fail("aliases", "type", "aliases must be an array of strings");
40
+ else {
41
+ for (const a of item.aliases) {
42
+ if (typeof a !== "string" || !NAME_RE.test(a)) fail("aliases", "pattern", `alias "${String(a)}" must match ${String(NAME_RE)}`);
43
+ else aliases.push(a);
44
+ }
45
+ }
46
+ }
47
+ const own = [...names, ...aliases];
48
+ own.forEach((n, i) => {
49
+ if (seen.has(n) || own.indexOf(n) !== i) fail(n === item.name ? "name" : "aliases", "duplicate", `"${n}" is already used`);
50
+ });
51
+ if (!nonEmpty(item.description)) fail("description", "required", "description is required");
52
+ if (item.type !== "prompt" && item.type !== "exec" && item.type !== "alias") fail("type", "enum", 'type must be "prompt", "exec" or "alias"');
53
+ else if (item.type === "prompt" && !nonEmpty(item.prompt)) fail("prompt", "required", "prompt is required for prompt commands");
54
+ else if (item.type === "exec" && !nonEmpty(item.command)) fail("command", "required", "command is required for exec commands");
55
+ else if (item.type === "alias") {
56
+ const m = typeof item.target === "string" ? TARGET_RE.exec(item.target.trim()) : null;
57
+ if (!nonEmpty(item.target)) fail("target", "required", "target is required for alias commands");
58
+ else if (m === null) fail("target", "pattern", `target "${String(item.target)}" must be a slash command like /compact`);
59
+ else if (own.includes(m[1])) fail("target", "duplicate", `alias /${m[1]} cannot target itself`);
60
+ }
61
+ if (item.cwd !== void 0 && !(typeof item.cwd === "string" && (item.cwd === "workspace" || item.cwd.startsWith("/")))) {
62
+ fail("cwd", "enum", 'cwd must be "workspace" or an absolute path');
63
+ }
64
+ if (item.timeoutMs !== void 0 && !(typeof item.timeoutMs === "number" && Number.isInteger(item.timeoutMs) && item.timeoutMs >= 1 && item.timeoutMs <= MAX_TIMEOUT_MS)) {
65
+ fail("timeoutMs", "range", `timeoutMs must be an integer in 1..${MAX_TIMEOUT_MS}`);
66
+ }
67
+ if (bad.length > 0) {
68
+ errors.push(...bad);
69
+ return;
70
+ }
71
+ for (const n of own) seen.add(n);
72
+ const cmd = {
73
+ name: item.name,
74
+ aliases,
75
+ description: item.description.trim(),
76
+ type: item.type
77
+ };
78
+ if (cmd.type === "prompt") cmd.prompt = item.prompt;
79
+ else if (cmd.type === "alias") cmd.target = normalizeTarget(item.target);
80
+ else {
81
+ cmd.command = item.command;
82
+ cmd.cwd = item.cwd ?? "workspace";
83
+ cmd.timeoutMs = item.timeoutMs ?? DEFAULT_TIMEOUT_MS;
84
+ }
85
+ commands.push(cmd);
86
+ });
87
+ return { commands, errors };
88
+ }
89
+ var expandNames = (cmd) => [cmd.name, ...cmd.aliases];
90
+ function composePrompt(prompt, rawInput) {
91
+ const args = rawInput.trim();
92
+ if (prompt.includes("$ARGUMENTS")) return prompt.replaceAll("$ARGUMENTS", () => args);
93
+ return args === "" ? prompt : `${prompt}
94
+
95
+ ${args}`;
96
+ }
97
+ var normalizeTarget = (target) => {
98
+ const t = target.trim();
99
+ return t.startsWith("/") ? t : `/${t}`;
100
+ };
101
+ function composeAlias(target, rawInput) {
102
+ const args = rawInput.trim();
103
+ if (target.includes("$ARGUMENTS")) return target.replaceAll("$ARGUMENTS", () => args).trim();
104
+ return args === "" ? target : `${target} ${args}`;
105
+ }
106
+ function execArgs(rawInput) {
107
+ const args = rawInput.trim();
108
+ return { env: { ARGUMENTS: args }, argv: args === "" ? [] : args.split(/\s+/) };
109
+ }
110
+ function clip(text, limit) {
111
+ const buf = Buffer.from(text, "utf8");
112
+ if (buf.length <= limit) return text;
113
+ return `${buf.subarray(0, limit).toString("utf8").replace(/�$/, "")}
114
+ \u2026[truncated]`;
115
+ }
116
+ function formatExecResult(o) {
117
+ const output = clip(`${o.stdout}${o.stderr}`.replace(/\s+$/, ""), OUTPUT_LIMIT);
118
+ if (o.exit === "0") return output === "" ? "exit=0" : output;
119
+ return output === "" ? `exit=${o.exit}` : `${output}
120
+ exit=${o.exit}`;
121
+ }
122
+
123
+ // src/index.ts
124
+ var name = "dsh-slash-command";
125
+ var inject = ["commands", "connection"];
126
+ var READ_PATH = "/api/dsh-slash-command/read";
127
+ var SAVE_PATH = "/api/dsh-slash-command/save";
128
+ var MAX_BUFFER = 4 * 1024 * 1024;
129
+ var MAX_BODY = 1024 * 1024;
130
+ var MAX_ALIAS_DEPTH = 8;
131
+ var ALIAS_DEPTH = Symbol("dsh-slash-command.aliasDepth");
132
+ var configPath = () => join(process.env.DSH_HOME ?? join(homedir(), ".dsh"), "slash-commands.json");
133
+ var json = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json", "cache-control": "no-store" } });
134
+ function steerText(agent, text) {
135
+ agent.steer(createUserMessage({ content: [{ type: "text", text }], source: { kind: "user" } }));
136
+ }
137
+ async function workspaceOf(ctx, agent, signal) {
138
+ try {
139
+ const wd = Reflect.get(ctx, "workingDirectory");
140
+ if (wd !== void 0) return await wd.ensure(agent, signal);
141
+ } catch {
142
+ }
143
+ return process.cwd();
144
+ }
145
+ function runExec(command, rawInput, cwd, timeout, signal) {
146
+ const { env, argv } = execArgs(rawInput);
147
+ return new Promise((resolve) => {
148
+ execFile("/bin/bash", ["-lc", command, "dsh-slash-command", ...argv], {
149
+ cwd,
150
+ timeout,
151
+ maxBuffer: MAX_BUFFER,
152
+ signal,
153
+ env: { ...process.env, ...env }
154
+ }, (err, stdout, stderr) => {
155
+ if (err === null) return resolve({ exit: "0", stdout, stderr });
156
+ const e = err;
157
+ if (typeof e.code === "number") return resolve({ exit: String(e.code), stdout, stderr });
158
+ const exit = e.killed === true ? `timeout(${e.signal ?? "killed"})` : String(e.code ?? e.signal ?? "error");
159
+ resolve({ exit, stdout, stderr: stderr + (stderr === "" || stderr.endsWith("\n") ? "" : "\n") + e.message });
160
+ });
161
+ });
162
+ }
163
+ function makeHandler(ctx, commands, cmd) {
164
+ if (cmd.type === "alias") {
165
+ return async (inv) => {
166
+ const depth = (inv[ALIAS_DEPTH] ?? 0) + 1;
167
+ if (depth > MAX_ALIAS_DEPTH) return { kind: "error", text: `/${cmd.name}: alias chain too deep` };
168
+ const line = composeAlias(cmd.target, inv.rawInput);
169
+ const m = /^\/([a-z][a-z0-9_-]*)/.exec(line);
170
+ const target = m === null ? void 0 : commands.find(inv.agent, m[1]);
171
+ if (target === void 0) return { kind: "error", text: `${line.split(/\s/)[0]}: unknown command` };
172
+ return target.handler({ ...inv, rawInput: line.slice(m[0].length), [ALIAS_DEPTH]: depth });
173
+ };
174
+ }
175
+ if (cmd.type === "prompt") {
176
+ return (inv) => {
177
+ steerText(inv.agent, composePrompt(cmd.prompt, inv.rawInput));
178
+ return Promise.resolve({ kind: "success", text: "\u5DF2\u53D1\u9001" });
179
+ };
180
+ }
181
+ return async (inv) => {
182
+ const cwd = cmd.cwd === void 0 || cmd.cwd === "workspace" ? await workspaceOf(ctx, inv.agent, inv.signal) : cmd.cwd;
183
+ const outcome = await runExec(cmd.command, inv.rawInput, cwd, cmd.timeoutMs, inv.signal);
184
+ const text = formatExecResult(outcome);
185
+ return outcome.exit === "0" ? { kind: "success", text } : { kind: "error", text };
186
+ };
187
+ }
188
+ function apply(ctx) {
189
+ const commands = Reflect.get(ctx, "commands");
190
+ const connection = Reflect.get(ctx, "connection");
191
+ const file = configPath();
192
+ let disposers = [];
193
+ let applied;
194
+ const unregisterAll = () => {
195
+ for (const d of disposers) d();
196
+ disposers = [];
197
+ };
198
+ async function reload() {
199
+ let text;
200
+ try {
201
+ text = await readFile(file, "utf8");
202
+ } catch (e) {
203
+ if (e.code !== "ENOENT") {
204
+ ctx.logger.warn(`dsh-slash-command: cannot read ${file}: ${String(e)}`);
205
+ return;
206
+ }
207
+ text = '{"commands":[]}';
208
+ }
209
+ if (text === applied) return;
210
+ let raw;
211
+ try {
212
+ raw = JSON.parse(text);
213
+ } catch (e) {
214
+ ctx.logger.warn(`dsh-slash-command: ${file} is not valid JSON, keeping previous config: ${String(e)}`);
215
+ return;
216
+ }
217
+ const { commands: valid, errors } = validateConfig(raw);
218
+ for (const err of errors) {
219
+ ctx.logger.warn(`dsh-slash-command: skipped commands[${err.index ?? "?"}]${err.field ? `.${err.field}` : ""}: ${err.message}`);
220
+ }
221
+ unregisterAll();
222
+ applied = text;
223
+ for (const cmd of valid) {
224
+ const handler = makeHandler(ctx, commands, cmd);
225
+ for (const n of expandNames(cmd)) {
226
+ try {
227
+ disposers.push(commands.register({ name: n, description: cmd.description, input: { hint: "[args]" }, handler }));
228
+ } catch (e) {
229
+ ctx.logger.warn(`dsh-slash-command: cannot register /${n}: ${e instanceof Error ? e.message : String(e)}`);
230
+ }
231
+ }
232
+ }
233
+ }
234
+ ctx.effect(() => {
235
+ let timer;
236
+ const trigger = () => {
237
+ clearTimeout(timer);
238
+ timer = setTimeout(() => {
239
+ void reload();
240
+ }, 300);
241
+ };
242
+ let watcher;
243
+ void mkdir(dirname(file), { recursive: true }).then(() => {
244
+ watcher = watch(dirname(file), (_event, filename) => {
245
+ if (filename === basename(file)) trigger();
246
+ });
247
+ watcher.on("error", (e) => ctx.logger.warn(`dsh-slash-command: watcher error: ${String(e)}`));
248
+ }).catch((e) => ctx.logger.warn(`dsh-slash-command: cannot watch ${file}: ${String(e)}`));
249
+ void reload();
250
+ return () => {
251
+ clearTimeout(timer);
252
+ watcher?.close();
253
+ unregisterAll();
254
+ };
255
+ }, "dsh-slash-command: config watcher");
256
+ connection.fetch.register({
257
+ path: READ_PATH,
258
+ methods: ["GET"],
259
+ requestBody: "buffered",
260
+ fetch: async () => {
261
+ try {
262
+ const raw = JSON.parse(await readFile(file, "utf8"));
263
+ const commands2 = raw.commands;
264
+ return json(200, { ok: true, commands: Array.isArray(commands2) ? commands2 : [] });
265
+ } catch (e) {
266
+ if (e.code === "ENOENT") return json(200, { ok: true, commands: [] });
267
+ return json(500, { ok: false, errors: [{ code: "type", message: `cannot read config: ${String(e)}` }] });
268
+ }
269
+ }
270
+ });
271
+ connection.fetch.register({
272
+ path: SAVE_PATH,
273
+ methods: ["POST"],
274
+ requestBody: "buffered",
275
+ fetch: async (request) => {
276
+ if (!(request.headers.get("content-type") ?? "").toLowerCase().startsWith("application/json")) {
277
+ return json(415, { ok: false, errors: [{ code: "type", message: "content-type must be application/json" }] });
278
+ }
279
+ const origin = request.headers.get("origin");
280
+ if (origin !== null && origin !== new URL(request.url).origin) {
281
+ return json(403, { ok: false, errors: [{ code: "type", message: "cross-origin request rejected" }] });
282
+ }
283
+ const body = await request.text();
284
+ if (body.length > MAX_BODY) return json(413, { ok: false, errors: [{ code: "range", message: "body too large" }] });
285
+ let raw;
286
+ try {
287
+ raw = JSON.parse(body);
288
+ } catch {
289
+ return json(400, { ok: false, errors: [{ code: "type", message: "body is not valid JSON" }] });
290
+ }
291
+ const { commands: valid, errors } = validateConfig(raw);
292
+ if (errors.length > 0) return json(400, { ok: false, errors });
293
+ const text = `${JSON.stringify({ commands: valid }, null, 2)}
294
+ `;
295
+ await mkdir(dirname(file), { recursive: true });
296
+ const tmp = `${file}.${process.pid}.tmp`;
297
+ await writeFile(tmp, text);
298
+ await rename(tmp, file);
299
+ await reload();
300
+ return json(200, { ok: true, commands: valid });
301
+ }
302
+ });
303
+ }
304
+ export {
305
+ apply,
306
+ inject,
307
+ name
308
+ };
309
+ //# sourceMappingURL=index.js.map