dsh-trellis 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":["useCallback","useSyncExternalStore","useState","useMemo"],"sources":["../src/client/form.js","../src/client/TrellisSettingsCard.jsx","../src/client/compat.js","../src/client/locales.js","../src/client/styles.js","../src/client/index.jsx"],"sourcesContent":["export const DEFAULTS = Object.freeze({\r\n enabled: true,\r\n maxBytes: 4096,\r\n projectRootMarkers: Object.freeze([\".git\"]),\r\n skipKeyword: \"no-trellis\",\r\n pythonCmd: \"\",\r\n commandsEnabled: true,\r\n});\r\n\r\nexport const FIELD_NAMES = Object.freeze(Object.keys(DEFAULTS));\r\n\r\nfunction record(value) {\r\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\r\n ? value\r\n : {};\r\n}\r\n\r\nexport function sameValue(left, right) {\r\n return JSON.stringify(left) === JSON.stringify(right);\r\n}\r\n\r\nexport function fallbackValue(snapshot, field) {\r\n const base = record(snapshot.base);\r\n return Object.hasOwn(base, field) ? base[field] : DEFAULTS[field];\r\n}\r\n\r\nexport function makeDraft(snapshot) {\r\n const value = record(snapshot.value);\r\n return Object.fromEntries(\r\n FIELD_NAMES.map((field) => [\r\n field,\r\n { mode: \"clean\", value: value[field] ?? DEFAULTS[field] },\r\n ]),\r\n );\r\n}\r\n\r\nexport function editDraft(draft, field, value) {\r\n return { ...draft, [field]: { mode: \"set\", value } };\r\n}\r\n\r\nexport function resetDraft(snapshot, draft, field) {\r\n return {\r\n ...draft,\r\n [field]: { mode: \"unset\", value: fallbackValue(snapshot, field) },\r\n };\r\n}\r\n\r\nexport function parseMarkers(text) {\r\n return [...new Set(text.split(/\\r?\\n/).map((entry) => entry.trim()).filter(Boolean))];\r\n}\r\n\r\nexport function parseDraft(draft) {\r\n const maxBytes = Number(String(draft.maxBytes.value).trim());\r\n return {\r\n invalid:\r\n !Number.isInteger(maxBytes) ||\r\n maxBytes < 0 ||\r\n typeof draft.enabled.value !== \"boolean\" ||\r\n typeof draft.commandsEnabled.value !== \"boolean\",\r\n value: {\r\n enabled: draft.enabled.value,\r\n maxBytes,\r\n projectRootMarkers: parseMarkers(String(draft.projectRootMarkers.value)),\r\n skipKeyword: String(draft.skipKeyword.value),\r\n pythonCmd: String(draft.pythonCmd.value),\r\n commandsEnabled: draft.commandsEnabled.value,\r\n },\r\n };\r\n}\r\n\r\nexport function planDraft(snapshot, draft) {\r\n const user = record(snapshot.user);\r\n const current = record(snapshot.value);\r\n const parsed = parseDraft(draft);\r\n const writes = [];\r\n for (const field of FIELD_NAMES) {\r\n const staged = draft[field];\r\n if (staged.mode === \"clean\") continue;\r\n if (staged.mode === \"unset\") {\r\n if (Object.hasOwn(user, field)) writes.push({ field, kind: \"unset\" });\r\n continue;\r\n }\r\n const value = parsed.value[field];\r\n if (!sameValue(current[field], value)) {\r\n writes.push({ field, kind: \"set\", value });\r\n }\r\n }\r\n return { invalid: parsed.invalid, writes };\r\n}\r\n\r\nexport function isOverridden(snapshot, draft, field) {\r\n if (draft[field].mode === \"set\") return true;\r\n if (draft[field].mode === \"unset\") return false;\r\n return Object.hasOwn(record(snapshot.user), field);\r\n}\r\n\r\nexport function planLanded(snapshot, writes) {\r\n const user = record(snapshot.user);\r\n return writes.every((write) =>\r\n write.kind === \"unset\"\r\n ? !Object.hasOwn(user, write.field)\r\n : Object.hasOwn(user, write.field) && sameValue(user[write.field], write.value),\r\n );\r\n}\r\n","import {\r\n useCallback,\r\n useEffect,\r\n useMemo,\r\n useState,\r\n useSyncExternalStore,\r\n} from \"react\";\r\nimport {\r\n editDraft,\r\n isOverridden,\r\n makeDraft,\r\n parseDraft,\r\n planDraft,\r\n planLanded,\r\n resetDraft,\r\n} from \"./form.js\";\r\n\r\nfunction Reset({ visible, disabled, label, onClick }) {\r\n return visible ? (\r\n <span className=\"dsh-trellis-field-head\">\r\n <span className=\"dsh-trellis-badge\">{label.overridden}</span>\r\n <button className=\"dsh-trellis-reset\" type=\"button\" disabled={disabled} onClick={onClick}>\r\n {label.reset}\r\n </button>\r\n </span>\r\n ) : null;\r\n}\r\n\r\nfunction TextField({ id, label, hint, value, disabled, multiline = false, invalid = false, onChange, onReset, overridden, t }) {\r\n const Control = multiline ? \"textarea\" : \"input\";\r\n return (\r\n <div className=\"dsh-trellis-field\">\r\n <div className=\"dsh-trellis-field-head\">\r\n <label className=\"dsh-trellis-label\" htmlFor={id}>{label}</label>\r\n <Reset visible={overridden} disabled={disabled} label={{ overridden: t(\"overridden\"), reset: t(\"reset\") }} onClick={onReset} />\r\n </div>\r\n <Control\r\n id={id}\r\n className={multiline ? \"dsh-trellis-textarea\" : \"dsh-trellis-input\"}\r\n {...multiline ? {} : { type: \"text\" }}\r\n value={value}\r\n disabled={disabled}\r\n aria-invalid={invalid || undefined}\r\n onChange={(event) => onChange(event.target.value)}\r\n />\r\n <p className=\"dsh-trellis-hint\" data-invalid={invalid}>{invalid ? t(\"maxBytesInvalid\") : hint}</p>\r\n </div>\r\n );\r\n}\r\n\r\nfunction BooleanField({ id, label, hint, value, disabled, onChange, onReset, overridden, t }) {\r\n return (\r\n <div className=\"dsh-trellis-field\">\r\n <div className=\"dsh-trellis-field-head\">\r\n <label className=\"dsh-trellis-switch-row\" htmlFor={id}>\r\n <input id={id} type=\"checkbox\" checked={value} disabled={disabled} onChange={(event) => onChange(event.target.checked)} />\r\n <span className=\"dsh-trellis-switch-copy\">\r\n <span className=\"dsh-trellis-label\">{label}</span>\r\n <span className=\"dsh-trellis-hint\">{hint}</span>\r\n </span>\r\n </label>\r\n <Reset visible={overridden} disabled={disabled} label={{ overridden: t(\"overridden\"), reset: t(\"reset\") }} onClick={onReset} />\r\n </div>\r\n </div>\r\n );\r\n}\r\n\r\nexport function TrellisSettingsCard({ scope, t }) {\r\n const subscribe = useCallback((listener) => scope.subscribe(listener), [scope]);\r\n const getSnapshot = useCallback(() => scope.getSnapshot(), [scope]);\r\n const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\r\n const [open, setOpen] = useState(false);\r\n const [draft, setDraft] = useState(() => makeDraft(snapshot));\r\n const [saving, setSaving] = useState(false);\r\n const [failed, setFailed] = useState(false);\r\n\r\n const plan = useMemo(() => planDraft(snapshot, draft), [snapshot, draft]);\r\n const dirty = plan.writes.length > 0;\r\n useEffect(() => {\r\n if (!dirty && snapshot.status === \"ready\") setDraft(makeDraft(snapshot));\r\n }, [dirty, snapshot]);\r\n\r\n if (snapshot.status !== \"ready\") return null;\r\n const disabled = !snapshot.writable || saving;\r\n const parsed = parseDraft(draft);\r\n const field = (name) => ({\r\n overridden: isOverridden(snapshot, draft, name),\r\n onReset: () => {\r\n setDraft((current) => resetDraft(snapshot, current, name));\r\n setFailed(false);\r\n },\r\n });\r\n const edit = (name, value) => {\r\n setDraft((current) => editDraft(current, name, value));\r\n setFailed(false);\r\n };\r\n const discard = () => {\r\n setDraft(makeDraft(snapshot));\r\n setFailed(false);\r\n };\r\n const save = async () => {\r\n if (saving || plan.invalid || plan.writes.length === 0) return;\r\n setSaving(true);\r\n setFailed(false);\r\n for (const write of plan.writes) {\r\n if (write.kind === \"unset\") await scope.unset(write.field);\r\n else await scope.set(write.field, write.value);\r\n }\r\n const landed = planLanded(scope.getSnapshot(), plan.writes);\r\n if (landed) setDraft(makeDraft(scope.getSnapshot()));\r\n setFailed(!landed);\r\n setSaving(false);\r\n };\r\n\r\n return (\r\n <li className=\"dsh-trellis-card\" data-open={open}>\r\n <button\r\n type=\"button\"\r\n className=\"dsh-trellis-header\"\r\n aria-expanded={open}\r\n aria-label={`${t(open ? \"collapse\" : \"expand\")}: ${t(\"title\")}`}\r\n onClick={() => setOpen((value) => !value)}\r\n >\r\n <span className=\"dsh-trellis-head-text\">\r\n <span className=\"dsh-trellis-name\">{t(\"title\")}</span>\r\n <span className=\"dsh-trellis-description\">{t(\"description\")}</span>\r\n </span>\r\n {dirty ? <span className=\"dsh-trellis-badge\">{t(\"unsaved\")}</span> : null}\r\n <span className=\"dsh-trellis-chevron\" aria-hidden=\"true\">⌄</span>\r\n </button>\r\n {open ? (\r\n <div className=\"dsh-trellis-body\">\r\n {!snapshot.writable ? <p className=\"dsh-trellis-readonly\" role=\"status\">{t(\"readOnly\")}</p> : null}\r\n <BooleanField id=\"dsh-trellis-enabled\" label={t(\"enabled\")} hint={t(\"enabledHint\")} value={Boolean(draft.enabled.value)} disabled={disabled} onChange={(value) => edit(\"enabled\", value)} t={t} {...field(\"enabled\")} />\r\n <BooleanField id=\"dsh-trellis-commands\" label={t(\"commandsEnabled\")} hint={t(\"commandsEnabledHint\")} value={Boolean(draft.commandsEnabled.value)} disabled={disabled} onChange={(value) => edit(\"commandsEnabled\", value)} t={t} {...field(\"commandsEnabled\")} />\r\n <TextField id=\"dsh-trellis-max-bytes\" label={t(\"maxBytes\")} hint={t(\"maxBytesHint\")} value={String(draft.maxBytes.value)} disabled={disabled} invalid={parsed.invalid && (!Number.isInteger(parsed.value.maxBytes) || parsed.value.maxBytes < 0)} onChange={(value) => edit(\"maxBytes\", value)} t={t} {...field(\"maxBytes\")} />\r\n <TextField id=\"dsh-trellis-markers\" label={t(\"projectRootMarkers\")} hint={t(\"projectRootMarkersHint\")} value={Array.isArray(draft.projectRootMarkers.value) ? draft.projectRootMarkers.value.join(\"\\n\") : String(draft.projectRootMarkers.value)} disabled={disabled} multiline onChange={(value) => edit(\"projectRootMarkers\", value)} t={t} {...field(\"projectRootMarkers\")} />\r\n <TextField id=\"dsh-trellis-skip-keyword\" label={t(\"skipKeyword\")} hint={t(\"skipKeywordHint\")} value={String(draft.skipKeyword.value)} disabled={disabled} onChange={(value) => edit(\"skipKeyword\", value)} t={t} {...field(\"skipKeyword\")} />\r\n <TextField id=\"dsh-trellis-python\" label={t(\"pythonCmd\")} hint={t(\"pythonCmdHint\")} value={String(draft.pythonCmd.value)} disabled={disabled} onChange={(value) => edit(\"pythonCmd\", value)} t={t} {...field(\"pythonCmd\")} />\r\n <div className=\"dsh-trellis-footer\">\r\n {failed ? <p className=\"dsh-trellis-error\" role=\"status\">{t(\"saveFailed\")}</p> : null}\r\n <button className=\"dsh-trellis-button\" type=\"button\" disabled={!dirty || saving} onClick={discard}>{t(\"discard\")}</button>\r\n <button className=\"dsh-trellis-button\" data-primary=\"true\" type=\"button\" disabled={!snapshot.writable || plan.invalid || plan.writes.length === 0 || saving} onClick={() => void save()}>{t(saving ? \"saving\" : \"save\")}</button>\r\n </div>\r\n </div>\r\n ) : null}\r\n </li>\r\n );\r\n}\r\n","export function settingsCardIdentity(slotSpec, namespace) {\r\n return slotSpec?.kind === \"keyed\"\r\n ? { key: namespace }\r\n : { id: namespace };\r\n}\r\n","export const zh = {\r\n title: \"Trellis 工作流\",\r\n description: \"工作流状态面包屑、命令和 DSH 会话同步。\",\r\n expand: \"展开\",\r\n collapse: \"收起\",\r\n unsaved: \"未保存\",\r\n readOnly: \"当前设置文档为只读。\",\r\n saveFailed: \"保存未生效;配置可能已被其他窗口修改,请检查后重试。\",\r\n discard: \"放弃更改\",\r\n save: \"保存\",\r\n saving: \"保存中…\",\r\n overridden: \"已覆盖\",\r\n reset: \"恢复配置文件值\",\r\n enabled: \"启用插件\",\r\n enabledHint: \"关闭后立即卸载 Trellis 面包屑、命令、会话变量和等待工具。\",\r\n commandsEnabled: \"启用 /trellis 命令\",\r\n commandsEnabledHint: \"注册 /trellis-status 和只读的 /trellis-finish。\",\r\n maxBytes: \"面包屑字节上限\",\r\n maxBytesHint: \"UTF-8 字节数;0 表示不注入工作流状态。\",\r\n maxBytesInvalid: \"请输入大于等于 0 的整数。\",\r\n projectRootMarkers: \"项目根标记\",\r\n projectRootMarkersHint: \"每行一个目录项;向上查找时任意一项命中即视为项目根。\",\r\n skipKeyword: \"单轮跳过关键词\",\r\n skipKeywordHint: \"提示词中独立出现该关键词时跳过本轮面包屑;留空可关闭。\",\r\n pythonCmd: \"Python 3 命令\",\r\n pythonCmdHint: \"留空使用平台默认候选;仅影响 /trellis 命令。\",\r\n};\r\n\r\nexport const en = {\r\n title: \"Trellis workflow\",\r\n description: \"Workflow breadcrumbs, commands, and DSH session synchronization.\",\r\n expand: \"Expand\",\r\n collapse: \"Collapse\",\r\n unsaved: \"Unsaved\",\r\n readOnly: \"The current settings document is read-only.\",\r\n saveFailed: \"The save did not land. The configuration may have changed elsewhere; review and retry.\",\r\n discard: \"Discard\",\r\n save: \"Save\",\r\n saving: \"Saving…\",\r\n overridden: \"Overridden\",\r\n reset: \"Use composed value\",\r\n enabled: \"Enable plugin\",\r\n enabledHint: \"Turning this off immediately unloads breadcrumbs, commands, session facts, and the wait tool.\",\r\n commandsEnabled: \"Enable /trellis commands\",\r\n commandsEnabledHint: \"Register /trellis-status and the read-only /trellis-finish helper.\",\r\n maxBytes: \"Breadcrumb byte limit\",\r\n maxBytesHint: \"UTF-8 bytes; 0 disables workflow-state injection.\",\r\n maxBytesInvalid: \"Enter an integer greater than or equal to 0.\",\r\n projectRootMarkers: \"Project-root markers\",\r\n projectRootMarkersHint: \"One directory entry per line; any match identifies the root while walking upward.\",\r\n skipKeyword: \"Per-turn skip keyword\",\r\n skipKeywordHint: \"A standalone occurrence in the prompt skips this turn's breadcrumb; empty disables it.\",\r\n pythonCmd: \"Python 3 command\",\r\n pythonCmdHint: \"Empty uses platform-aware candidates; affects only /trellis commands.\",\r\n};\r\n","export const styles = String.raw`\r\n.dsh-trellis-card{list-style:none;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-3);transition:border-color .16s,background .16s}.dsh-trellis-card:hover{border-color:var(--dsw-alias-label-dimmed)}.dsh-trellis-card[data-open=true]{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.dsh-trellis-header{width:100%;appearance:none;border:0;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer;display:flex;align-items:center;gap:12px;padding:14px 16px;border-radius:12px}.dsh-trellis-header:focus-visible,.dsh-trellis-button:focus-visible,.dsh-trellis-reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.dsh-trellis-head-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}.dsh-trellis-name{font-size:15px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary)}.dsh-trellis-description{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}.dsh-trellis-chevron{flex:none;color:var(--dsw-alias-label-tertiary);font-size:14px;transition:transform .16s}.dsh-trellis-card[data-open=true] .dsh-trellis-chevron{transform:rotate(180deg)}.dsh-trellis-badge{flex:none;border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;font-weight:500;white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}.dsh-trellis-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.dsh-trellis-readonly,.dsh-trellis-error{margin:12px 0 0;font-size:12px;line-height:1.5}.dsh-trellis-readonly{color:var(--dsw-alias-label-tertiary)}.dsh-trellis-error{color:var(--dsw-alias-label-error)}.dsh-trellis-field{display:flex;flex-direction:column;gap:6px;padding:12px 0}.dsh-trellis-field+.dsh-trellis-field{border-top:1px solid var(--dsw-alias-border-l2)}.dsh-trellis-field-head{display:flex;align-items:center;gap:8px}.dsh-trellis-label{flex:1;min-width:0;font-size:13px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary)}.dsh-trellis-reset{border:0;background:none;padding:0;font:inherit;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);cursor:pointer}.dsh-trellis-input,.dsh-trellis-textarea{width:100%;box-sizing:border-box;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-3);font:inherit;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary)}.dsh-trellis-input{height:34px}.dsh-trellis-textarea{min-height:68px;padding-top:8px;padding-bottom:8px;resize:vertical}.dsh-trellis-input:focus-visible,.dsh-trellis-textarea:focus-visible{outline:none;border-color:var(--dsw-alias-brand-primary)}.dsh-trellis-input[aria-invalid=true]{border-color:var(--dsw-alias-label-error)}.dsh-trellis-hint{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}.dsh-trellis-hint[data-invalid=true]{color:var(--dsw-alias-label-error)}.dsh-trellis-switch-row{display:flex;flex:1;align-items:flex-start;gap:10px}.dsh-trellis-switch-row input{margin-top:3px;accent-color:var(--dsw-alias-brand-primary)}.dsh-trellis-switch-copy{display:flex;flex:1;min-width:0;flex-direction:column;gap:3px}.dsh-trellis-footer{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 0 4px;border-top:1px solid var(--dsw-alias-border-l2)}.dsh-trellis-footer .dsh-trellis-error{flex:1;margin:0}.dsh-trellis-button{appearance:none;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:5px 14px;font:inherit;font-size:13px;line-height:1.5;cursor:pointer;background:none;color:var(--dsw-alias-label-secondary)}.dsh-trellis-button[data-primary=true]{border-color:transparent;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.dsh-trellis-button:disabled,.dsh-trellis-reset:disabled,.dsh-trellis-input:disabled,.dsh-trellis-textarea:disabled{opacity:.45;cursor:default}\r\n`;\r\n","import { TrellisSettingsCard } from \"./TrellisSettingsCard.jsx\";\r\nimport { settingsCardIdentity } from \"./compat.js\";\r\nimport { en, zh } from \"./locales.js\";\r\nimport { styles } from \"./styles.js\";\r\n\r\nexport const SETTINGS_NAMESPACE = \"dsh-trellis\";\r\nexport const LOCALE_NAMESPACE = \"settings.dsh-trellis\";\r\nexport const inject = [\"slots\", \"locale\", \"settingsScope\"];\r\n\r\nexport function apply(ctx) {\r\n const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE });\r\n ctx.effect(\r\n () => ctx.locale.register(LOCALE_NAMESPACE, { zh, en }),\r\n \"dsh-trellis.client.locale\",\r\n );\r\n ctx.effect(() => {\r\n const selector = 'style[data-plugin-css=\"dsh-trellis/client\"]';\r\n if (document.querySelector(selector) !== null) return () => {};\r\n const tag = document.createElement(\"style\");\r\n tag.dataset.plugin = \"dsh-trellis\";\r\n tag.dataset.pluginCss = \"dsh-trellis/client\";\r\n tag.textContent = styles;\r\n document.head.appendChild(tag);\r\n return () => tag.remove();\r\n }, \"dsh-trellis.client.styles\");\r\n\r\n ctx.slots.inject(\"settings.plugin.item\", () =>\r\n ctx.slots.register(\r\n {\r\n name: \"settings.plugin.item\",\r\n ...settingsCardIdentity(\r\n ctx.slots.spec(\"settings.plugin.item\"),\r\n SETTINGS_NAMESPACE,\r\n ),\r\n locale: LOCALE_NAMESPACE,\r\n inject: () => ({ scope }),\r\n },\r\n TrellisSettingsCard,\r\n ),\r\n );\r\n}\r\n"],"mappings":"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"}
package/lib/commands.js CHANGED
@@ -1,195 +1,215 @@
1
- /**
2
- * Human-facing /trellis commands: status and finish.
3
- *
4
- * Command names follow the dsh grammar (lowercase letters, digits, `_`, `-`;
5
- * no `:`), and deliberately avoid the skill names (`trellis-start`,
6
- * `trellis-continue`, `trellis-finish-work`): a command sharing a skill name
7
- * would shadow the skill's slash-pipeline injection. The Trellis session
8
- * rituals stay on the skill surface; these commands cover the deterministic,
9
- * zero-token side — pointer inspection and wrap-up.
10
- *
11
- * @module dsh-trellis/commands
12
- */
13
- import { execFile } from "node:child_process";
14
- import { promisify } from "node:util";
15
- import { join } from "node:path";
16
- import { findProjectRoot } from "./workflow.js";
17
- import { nodeFs } from "./node-fs.js";
18
- import { dshContextKey } from "./session-env.js";
19
-
20
- const execFileAsync = promisify(execFile);
21
-
22
- /** Platform-aware Python launch candidates. An explicit config disables fallback. */
23
- export function pythonCandidates(config, platform = process.platform) {
24
- if (config.pythonCmd) return [{ command: config.pythonCmd, prefixArgs: [] }];
25
- if (platform === "win32") {
26
- return [
27
- { command: "py", prefixArgs: ["-3"] },
28
- { command: "python", prefixArgs: [] },
29
- ];
30
- }
31
- return [
32
- { command: "python3", prefixArgs: [] },
33
- { command: "python", prefixArgs: [] },
34
- ];
35
- }
36
-
37
- /** Run a project Trellis script with bounded time and output. */
38
- export async function runTrellisScript(
39
- config,
40
- root,
41
- sessionId,
42
- scriptArgs,
43
- dependencies = {},
44
- ) {
45
- const execute = dependencies.execFile ?? execFileAsync;
46
- const platform = dependencies.platform ?? process.platform;
47
- const signal = dependencies.signal;
48
- const contextKey = dshContextKey(sessionId);
49
- const env = {
50
- ...process.env,
51
- ...(contextKey
52
- ? {
53
- TRELLIS_CONTEXT_ID: contextKey,
54
- DSH_TRELLIS_CONTEXT_ID: contextKey,
55
- DSH_SHELL: "1",
56
- DSH_SESSION_ID: sessionId,
57
- }
58
- : {}),
59
- };
60
- const script = join(root, ".trellis", "scripts", "task.py");
61
- const candidates = pythonCandidates(config, platform);
62
- for (const [index, candidate] of candidates.entries()) {
63
- const args = [...candidate.prefixArgs, script, ...scriptArgs];
64
- try {
65
- const { stdout, stderr } = await execute(candidate.command, args, {
66
- cwd: root,
67
- env,
68
- windowsHide: true,
69
- timeout: 15_000,
70
- maxBuffer: 256 * 1024,
71
- ...(signal ? { signal } : {}),
72
- });
73
- const text = `${stdout}${stderr ? `\n${stderr}` : ""}`.trim();
74
- return { kind: "success", text };
75
- } catch (error) {
76
- if (signal?.aborted || error?.name === "AbortError") {
77
- return { kind: "error", text: "Trellis command cancelled." };
78
- }
79
- const missing = error?.code === "ENOENT";
80
- if (missing && index < candidates.length - 1) continue;
81
- const attempted = candidates
82
- .slice(0, index + 1)
83
- .map(({ command, prefixArgs }) => [command, ...prefixArgs].join(" "))
84
- .join(", ");
85
- const message = missing
86
- ? `Python command not found (tried: ${attempted}). Set the plugin's pythonCmd config to a Python 3 executable.`
87
- : `Trellis script failed via ${[candidate.command, ...candidate.prefixArgs].join(" ")}: ${error?.message ?? String(error)}`;
88
- return { kind: "error", text: message };
89
- }
90
- }
91
- return {
92
- kind: "error",
93
- text: "No Python command candidates are configured.",
94
- };
95
- }
96
-
97
- /**
98
- * Register the /trellis command family.
99
- * @param {object} ctx - cordis context exposing `ctx.commands`.
100
- * @param {object} resolved - normalized plugin config.
101
- * @returns {() => void} disposer.
102
- */
103
- export function registerCommands(ctx, resolved, dependencies = {}) {
104
- const fs = dependencies.fs ?? nodeFs;
105
- const execute = dependencies.execFile ?? execFileAsync;
106
- const platform = dependencies.platform ?? process.platform;
107
- const disposers = [];
108
-
109
- const withProject = async (invocation, fn) => {
110
- const cwd = invocation.agent?.session?.header?.cwd ?? process.cwd();
111
- const root = await findProjectRoot(cwd, resolved.projectRootMarkers, fs);
112
- if (root === null) {
113
- return {
114
- kind: "error",
115
- text: "No project root found from the configured markers.",
116
- };
117
- }
118
- const workflow = join(root, ".trellis", "workflow.md");
119
- const taskScript = join(root, ".trellis", "scripts", "task.py");
120
- if (!(await fs.exists(workflow)) || !(await fs.exists(taskScript))) {
121
- return {
122
- kind: "error",
123
- text: "No Trellis project found (expected .trellis/workflow.md and .trellis/scripts/task.py).",
124
- };
125
- }
126
- const sessionId = invocation.agent?.session?.header?.id ?? "";
127
- return fn(root, sessionId, invocation.signal);
128
- };
129
-
130
- disposers.push(
131
- ctx.commands.register({
132
- name: "trellis-status",
133
- description: "Show the Trellis active task and git status.",
134
- handler: (invocation) =>
135
- withProject(invocation, async (root, sessionId, signal) => {
136
- const task = await runTrellisScript(
137
- resolved,
138
- root,
139
- sessionId,
140
- ["current", "--source"],
141
- { execFile: execute, platform, signal },
142
- );
143
- if (signal?.aborted) return task;
144
- let git = "";
145
- try {
146
- const { stdout } = await execute(
147
- "git",
148
- ["status", "--short", "--branch"],
149
- {
150
- cwd: root,
151
- windowsHide: true,
152
- timeout: 10_000,
153
- maxBuffer: 128 * 1024,
154
- ...(signal ? { signal } : {}),
155
- },
156
- );
157
- git = `\n\nGit status:\n${stdout.trim()}`;
158
- } catch {
159
- git = "";
160
- }
161
- const hint =
162
- task.kind === "success"
163
- ? `\n\nSession rituals: type /trellis-start (start), /trellis-continue (resume), /trellis-finish-work (skill wrap-up).`
164
- : "";
165
- return { kind: task.kind, text: `${task.text}${git}${hint}` };
166
- }),
167
- }),
168
- );
169
-
170
- disposers.push(
171
- ctx.commands.register({
172
- name: "trellis-finish",
173
- description:
174
- "Inspect the active task and print the safe wrap-up checklist.",
175
- handler: (invocation) =>
176
- withProject(invocation, async (root, sessionId, signal) => {
177
- const result = await runTrellisScript(
178
- resolved,
179
- root,
180
- sessionId,
181
- ["current", "--source"],
182
- { execFile: execute, platform, signal },
183
- );
184
- if (signal?.aborted) return result;
185
- const checklist =
186
- 'Wrap-up checklist (this command did not clear or archive anything):\n 1. Commit this task\'s changes (workflow.md Phase 3.4; do not push).\n 2. Type /trellis-finish-work so the skill can archive the task before the pointer is removed and then record the session journal.\n\nIf you must do it manually, archive first with the configured Python 3 runtime: .trellis/scripts/task.py archive <task-name>; then run .trellis/scripts/add_session.py --title "..." --commit <hash> --summary "...".';
187
- return { kind: result.kind, text: `${result.text}\n\n${checklist}` };
188
- }),
189
- }),
190
- );
191
-
192
- return () => {
193
- for (const dispose of disposers) dispose();
194
- };
195
- }
1
+ /**
2
+ * Human-facing /trellis commands: status and finish.
3
+ *
4
+ * Command names follow the dsh grammar (lowercase letters, digits, `_`, `-`;
5
+ * no `:`), and deliberately avoid the skill names (`trellis-start`,
6
+ * `trellis-continue`, `trellis-finish-work`): a command sharing a skill name
7
+ * would shadow the skill's slash-pipeline injection. The Trellis session
8
+ * rituals stay on the skill surface; these commands cover the deterministic,
9
+ * zero-token side — pointer inspection and wrap-up.
10
+ *
11
+ * @module dsh-trellis/commands
12
+ */
13
+ import { execFile } from "node:child_process";
14
+ import { promisify } from "node:util";
15
+ import { join } from "node:path";
16
+ import { findProjectRoot } from "./workflow.js";
17
+ import { nodeFs } from "./node-fs.js";
18
+ import { dshContextKey } from "./session-env.js";
19
+
20
+ const execFileAsync = promisify(execFile);
21
+
22
+ /** Platform-aware Python launch candidates. An explicit config disables fallback. */
23
+ export function pythonCandidates(config, platform = process.platform) {
24
+ if (config.pythonCmd) return [{ command: config.pythonCmd, prefixArgs: [] }];
25
+ if (platform === "win32") {
26
+ return [
27
+ { command: "py", prefixArgs: ["-3"] },
28
+ { command: "python", prefixArgs: [] },
29
+ ];
30
+ }
31
+ return [
32
+ { command: "python3", prefixArgs: [] },
33
+ { command: "python", prefixArgs: [] },
34
+ ];
35
+ }
36
+
37
+ /** Run a project Trellis script with bounded time and output. */
38
+ export async function runTrellisScript(
39
+ config,
40
+ root,
41
+ sessionId,
42
+ scriptArgs,
43
+ dependencies = {},
44
+ ) {
45
+ const execute = dependencies.execFile ?? execFileAsync;
46
+ const platform = dependencies.platform ?? process.platform;
47
+ const signal = dependencies.signal;
48
+ const contextKey = dshContextKey(sessionId);
49
+ const env = {
50
+ ...process.env,
51
+ ...(contextKey
52
+ ? {
53
+ TRELLIS_CONTEXT_ID: contextKey,
54
+ DSH_TRELLIS_CONTEXT_ID: contextKey,
55
+ DSH_SHELL: "1",
56
+ DSH_SESSION_ID: sessionId,
57
+ }
58
+ : {}),
59
+ };
60
+ const script = join(root, ".trellis", "scripts", "task.py");
61
+ const candidates = pythonCandidates(config, platform);
62
+ for (const [index, candidate] of candidates.entries()) {
63
+ const args = [...candidate.prefixArgs, script, ...scriptArgs];
64
+ try {
65
+ const { stdout, stderr } = await execute(candidate.command, args, {
66
+ cwd: root,
67
+ env,
68
+ windowsHide: true,
69
+ timeout: 15_000,
70
+ maxBuffer: 256 * 1024,
71
+ ...(signal ? { signal } : {}),
72
+ });
73
+ const text = `${stdout}${stderr ? `\n${stderr}` : ""}`.trim();
74
+ return { kind: "success", text };
75
+ } catch (error) {
76
+ if (signal?.aborted || error?.name === "AbortError") {
77
+ return { kind: "error", text: "Trellis command cancelled." };
78
+ }
79
+ const missing = error?.code === "ENOENT";
80
+ if (missing && index < candidates.length - 1) continue;
81
+ const attempted = candidates
82
+ .slice(0, index + 1)
83
+ .map(({ command, prefixArgs }) => [command, ...prefixArgs].join(" "))
84
+ .join(", ");
85
+ const message = missing
86
+ ? `Python command not found (tried: ${attempted}). Set the plugin's pythonCmd config to a Python 3 executable.`
87
+ : `Trellis script failed via ${[candidate.command, ...candidate.prefixArgs].join(" ")}: ${error?.message ?? String(error)}`;
88
+ return { kind: "error", text: message };
89
+ }
90
+ }
91
+ return {
92
+ kind: "error",
93
+ text: "No Python command candidates are configured.",
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Register the /trellis command family.
99
+ * @param {object} ctx - cordis context exposing `ctx.commands`.
100
+ * @param {object} resolved - normalized plugin config.
101
+ * @returns {() => void} disposer.
102
+ */
103
+ export function registerCommands(ctx, resolved, dependencies = {}) {
104
+ const fs = dependencies.fs ?? nodeFs;
105
+ const execute = dependencies.execFile ?? execFileAsync;
106
+ const platform = dependencies.platform ?? process.platform;
107
+ const disposers = [];
108
+
109
+ const withoutInput = (invocation, fn) => {
110
+ const rawInput = invocation.rawInput ?? "";
111
+ const attachments = invocation.attachments ?? [];
112
+ if (rawInput.trim() !== "" || attachments.length > 0) {
113
+ return {
114
+ kind: "error",
115
+ text: "This Trellis command does not accept arguments or image attachments.",
116
+ };
117
+ }
118
+ return fn();
119
+ };
120
+
121
+ const withProject = async (invocation, fn) => {
122
+ const cwd = invocation.agent?.session?.header?.cwd ?? process.cwd();
123
+ const root = await findProjectRoot(cwd, resolved.projectRootMarkers, fs);
124
+ if (root === null) {
125
+ return {
126
+ kind: "error",
127
+ text: "No project root found from the configured markers.",
128
+ };
129
+ }
130
+ const workflow = join(root, ".trellis", "workflow.md");
131
+ const taskScript = join(root, ".trellis", "scripts", "task.py");
132
+ if (!(await fs.exists(workflow)) || !(await fs.exists(taskScript))) {
133
+ return {
134
+ kind: "error",
135
+ text: "No Trellis project found (expected .trellis/workflow.md and .trellis/scripts/task.py).",
136
+ };
137
+ }
138
+ const sessionId = invocation.agent?.session?.header?.id ?? "";
139
+ return fn(root, sessionId, invocation.signal);
140
+ };
141
+
142
+ disposers.push(
143
+ ctx.commands.register({
144
+ name: "trellis-status",
145
+ description: "Show the Trellis active task and git status.",
146
+ // DSH rc.8 records command lifecycle events. These commands have no
147
+ // payload of their own, so omit empty/invalid input from the durable log.
148
+ recordInput: false,
149
+ handler: (invocation) =>
150
+ withoutInput(invocation, () =>
151
+ withProject(invocation, async (root, sessionId, signal) => {
152
+ const task = await runTrellisScript(
153
+ resolved,
154
+ root,
155
+ sessionId,
156
+ ["current", "--source"],
157
+ { execFile: execute, platform, signal },
158
+ );
159
+ if (signal?.aborted) return task;
160
+ let git = "";
161
+ try {
162
+ const { stdout } = await execute(
163
+ "git",
164
+ ["status", "--short", "--branch"],
165
+ {
166
+ cwd: root,
167
+ windowsHide: true,
168
+ timeout: 10_000,
169
+ maxBuffer: 128 * 1024,
170
+ ...(signal ? { signal } : {}),
171
+ },
172
+ );
173
+ git = `\n\nGit status:\n${stdout.trim()}`;
174
+ } catch {
175
+ git = "";
176
+ }
177
+ const hint =
178
+ task.kind === "success"
179
+ ? `\n\nSession rituals: type /trellis-start (start), /trellis-continue (resume), /trellis-finish-work (skill wrap-up).`
180
+ : "";
181
+ return { kind: task.kind, text: `${task.text}${git}${hint}` };
182
+ }),
183
+ ),
184
+ }),
185
+ );
186
+
187
+ disposers.push(
188
+ ctx.commands.register({
189
+ name: "trellis-finish",
190
+ description:
191
+ "Inspect the active task and print the safe wrap-up checklist.",
192
+ recordInput: false,
193
+ handler: (invocation) =>
194
+ withoutInput(invocation, () =>
195
+ withProject(invocation, async (root, sessionId, signal) => {
196
+ const result = await runTrellisScript(
197
+ resolved,
198
+ root,
199
+ sessionId,
200
+ ["current", "--source"],
201
+ { execFile: execute, platform, signal },
202
+ );
203
+ if (signal?.aborted) return result;
204
+ const checklist =
205
+ 'Wrap-up checklist (this command did not clear or archive anything):\n 1. Commit this task\'s changes (workflow.md Phase 3.4; do not push).\n 2. Type /trellis-finish-work so the skill can archive the task before the pointer is removed and then record the session journal.\n\nIf you must do it manually, archive first with the configured Python 3 runtime: .trellis/scripts/task.py archive <task-name>; then run .trellis/scripts/add_session.py --title "..." --commit <hash> --summary "...".';
206
+ return { kind: result.kind, text: `${result.text}\n\n${checklist}` };
207
+ }),
208
+ ),
209
+ }),
210
+ );
211
+
212
+ return () => {
213
+ for (const dispose of disposers) dispose();
214
+ };
215
+ }
package/lib/config.js CHANGED
@@ -1,45 +1,77 @@
1
- /**
2
- * dsh-trellis plugin configuration (Schemastery).
3
- *
4
- * @module dsh-trellis/config
5
- */
6
- import z from "@deepseek-ai/schemastery";
7
-
8
- export const Config = z.object({
9
- /** Master switch: when false, the plugin injects nothing and registers nothing. */
10
- enabled: z.boolean().default(true),
11
- /** UTF-8 byte budget for the injected breadcrumb text. 0 disables injection. */
12
- maxBytes: z.number().step(1).min(0).default(4096),
13
- /** Directory markers that identify a project root (walked upward from the session cwd). */
14
- projectRootMarkers: z.array(z.string()).default([".git"]),
15
- /** Standalone word (case-insensitive) in the user prompt that skips breadcrumb injection for that turn. Empty disables. */
16
- skipKeyword: z.string().default("no-trellis"),
17
- /** Python executable used by /trellis commands. Empty selects a platform-aware fallback chain. */
18
- pythonCmd: z.string().default(""),
19
- /** Register the deterministic /trellis-status and read-only /trellis-finish commands. */
20
- commandsEnabled: z.boolean().default(true),
21
- });
22
-
23
- /**
24
- * Normalized runtime configuration.
25
- * @param {Partial<Record<string, unknown>>} config
26
- * @returns {{
27
- * enabled: boolean,
28
- * maxBytes: number,
29
- * projectRootMarkers: string[],
30
- * skipKeyword: string,
31
- * pythonCmd: string,
32
- * commandsEnabled: boolean,
33
- * }}
34
- */
35
- export function resolveConfig(config) {
36
- return {
37
- enabled: config.enabled !== false,
38
- maxBytes: config.maxBytes ?? 4096,
39
- projectRootMarkers: config.projectRootMarkers ?? [".git"],
40
- skipKeyword: config.skipKeyword ?? "no-trellis",
41
- pythonCmd:
42
- typeof config.pythonCmd === "string" ? config.pythonCmd.trim() : "",
43
- commandsEnabled: config.commandsEnabled !== false,
44
- };
45
- }
1
+ /**
2
+ * dsh-trellis plugin configuration (Schemastery).
3
+ *
4
+ * @module dsh-trellis/config
5
+ */
6
+ import z from "@deepseek-ai/schemastery";
7
+
8
+ export const Config = z.object({
9
+ /** Runtime master switch; the rc.8 settings namespace remains available for re-enabling. */
10
+ enabled: z
11
+ .boolean()
12
+ .default(true)
13
+ .description(
14
+ "Enable Trellis breadcrumbs, commands, session facts, and the wait tool for this profile.",
15
+ ),
16
+ /** UTF-8 byte budget for the injected breadcrumb text. 0 disables injection. */
17
+ maxBytes: z
18
+ .number()
19
+ .step(1)
20
+ .min(0)
21
+ .default(4096)
22
+ .description(
23
+ "Maximum UTF-8 bytes in a workflow-state breadcrumb; 0 disables breadcrumbs.",
24
+ ),
25
+ /** Directory markers that identify a project root (walked upward from the session cwd). */
26
+ projectRootMarkers: z
27
+ .array(z.string())
28
+ .default([".git"])
29
+ .description(
30
+ "Directory entries used while walking upward to find the project root.",
31
+ ),
32
+ /** Standalone word (case-insensitive) in the user prompt that skips breadcrumb injection for that turn. Empty disables. */
33
+ skipKeyword: z
34
+ .string()
35
+ .default("no-trellis")
36
+ .description(
37
+ "Standalone prompt keyword that skips one breadcrumb injection; empty disables the escape hatch.",
38
+ ),
39
+ /** Python executable used by /trellis commands. Empty selects a platform-aware fallback chain. */
40
+ pythonCmd: z
41
+ .string()
42
+ .default("")
43
+ .description(
44
+ "Python 3 executable for Trellis scripts; empty selects a platform-aware launcher chain.",
45
+ ),
46
+ /** Register the deterministic /trellis-status and read-only /trellis-finish commands. */
47
+ commandsEnabled: z
48
+ .boolean()
49
+ .default(true)
50
+ .description(
51
+ "Register /trellis-status and the read-only /trellis-finish helper.",
52
+ ),
53
+ });
54
+
55
+ /**
56
+ * Normalized runtime configuration.
57
+ * @param {Partial<Record<string, unknown>>} config
58
+ * @returns {{
59
+ * enabled: boolean,
60
+ * maxBytes: number,
61
+ * projectRootMarkers: string[],
62
+ * skipKeyword: string,
63
+ * pythonCmd: string,
64
+ * commandsEnabled: boolean,
65
+ * }}
66
+ */
67
+ export function resolveConfig(config) {
68
+ return {
69
+ enabled: config.enabled !== false,
70
+ maxBytes: config.maxBytes ?? 4096,
71
+ projectRootMarkers: config.projectRootMarkers ?? [".git"],
72
+ skipKeyword: config.skipKeyword ?? "no-trellis",
73
+ pythonCmd:
74
+ typeof config.pythonCmd === "string" ? config.pythonCmd.trim() : "",
75
+ commandsEnabled: config.commandsEnabled !== false,
76
+ };
77
+ }