Imported from YrracOwl/dsh-hmos-sidebar (
packages/dsh-hmos-sidebar/AGENTS.md). Install upstream withnpx skills add YrracOwl/dsh-hmos-sidebar --skill dsh-hmos-sidebar. Copyright stays with the author.
dsh-hmos-sidebar Maintenance Guide
Purpose and Boundaries
Windows-only HarmonyOS developer workbench. The package has three faces:
lib/index.js: Host action-level/hmos/api/*RPC only, plus the optional official settings namespacehmos-sidebar.lib/client.js: self-contained Web floating ball/panel inshell.overlay, plus the official Settings card on BOTH settings seats (legacysettings.plugin.itemkeyedhmos-sidebarfor ≤ 0.1.5, rc.2 keyed row seatplugins.row.configkeyedROW_CONFIG_KEY).lib/dcli-tools.mjsvia export./tools:dcli__*model tools mounted separately by an agent preset.
Never register the model tools from the main bundle. Never accept arbitrary argv in Web RPC. Preserve the package's Windows-only contract (package.json#os, runtime guards, docs, tests).
Key Files
lib/environment.js: one source of truth for CLI, Studio, hdc, hvigor, json5, project roots, and dynamic environment resolution.lib/index.js: Host route fence, body limit, action validation, path containment, device/build/deploy operations.lib/client.js: Shadow-DOM workbench, current-session cwd handling, bounded project discovery, persisted geometry.lib/dcli-tools.mjs: tool definitions and tool implementations.bin/dsh-hmos-sidebar.mjs: explicitinstall-presetsCLI. It resolves@deepseek-ai/dsh-agent-presetfrom the target profile to pick the payload shape: on ≤ 0.1.5 it copiespresets/<id>/into<DSH_HOME>/.agent-presets/<id>/; on ≥ 0.1.7-rc.1 it merges onecordis:includerow per preset into a marked block of the profile'scordis.patch.yml. Conflict protection and timestamped backups apply to both shapes, and text outside the marked block is never rewritten.--mode auto|directory|declarativeforces the shape;--profile-dirnames the profile (default: cwd).presets/native-harmonyos/agent.cordis.yml: the directory payload (≤ 0.1.5) of the direct PTC presentation preset.presets/native-harmonyos.declarative.yml: the same preset as an@deepseek-ai/dsh-agent-presetdeclaration (≥ 0.1.7-rc.1), mounted bycordis:include.presets/liangshen-native-harmonyos/agent.cordis.ymlandpresets/liangshen-native-harmonyos.declarative.yml: deferred Liangshen promotion and its per-session PTC switch, in both payload shapes.tool-bootstrap.mjs,custom-bash.mjs, anddsh-compat.mjsstay in the directory and are reached from the declaration through the./presets/liangshen-tool-bootstrapand./presets/liangshen-custom-bashsubpath exports. These files are the authoritative sources for both npm-bundled presets; user-level copies are deployment artifacts.lib/dual-signing.js: preview-first dual-signing merge and backup behavior.lib/validate.js: shared validation helpers.cordis.patch.yml: main Host+Client row only; no personal paths and no tools row.test/: environment, RPC security, tools, signing, generated AGENTS, and client-source regressions.
Invariants
- Environment discovery is dynamic per call: config → environment variables → common Windows install locations. Do not cache paths in a way that requires restarting DSH after installing CLI/Studio. CLI entry discovery must stay layout-independent:
cliCandidates()resolves the real entry from the installed package's ownpackage.json#binunder eachnpmGlobalRoots()root —@deveco/deveco-cli@1.3.4ships{ "devecocli": "cli.js" }and no longer contains anydist/, so never hard-code<root>\node_modules\@deveco\deveco-cli\dist\cli.jsas the entry. The legacydist/cli.jscandidate survives only as an ordered, de-duplicated fallback, and the whole resolution fails soft (missing/unreadable manifest → legacy candidates, never a throw; it runs during host startup and inside every tool call).json5Candidates(cliPath)locates the CLI package root by walking up from the resolved entry to the nearest ancestor whosepackage.json#nameis@deveco/deveco-cli— correct for both layouts — and keeps the npm-global-root candidates as extra fallbacks.test/environment.test.mjspins three fixtures: the newcli.jslayout, the legacydist/cli.jslayout, and an absent/unreadable manifest. - RPC is POST-only, same-origin/loopback fenced, action-level, and capped at 64 KiB.
- Filesystem actions must stay inside explicitly trusted roots when configured; preserve realpath/nearest-existing-parent handling against
.., UNC, junction, and reparse-point escapes. - Do not return secrets. Absolute paths and device serials are intentionally disclosed only to the same-origin local page.
dcli__configure_dual_signingis preview-first (apply=false), creates one backup, validates material, and never echoes passwords.dcli__agents_mdis preview-first (apply=false), owns only its unique managed-marker block, preserves all text outside it, rejects malformed/duplicate markers, and uses a one-time backup plus atomic replacement when applying.- The UI is independent of better-sidebar and must remain usable when DevEco/CLI is absent; errors must be actionable.
@modelcontextprotocol/sdkis an optionalpeerDependency, never adependenciesentry:lib/dcli-tools.mjsneeds it only fordcli__lsp_check/dcli__lsp_restart, and a private copy makes pnpm materialize its own isolated store inside the package (node_modules/dsh-hmos-sidebar/node_modules/.pnpm/…), which DSH's package-closure walk cannotrealpathon Windows (EPERM: operation not permitted, realpath) — the whole Web boot dies before it starts. Declare the same range as a peer pluspeerDependenciesMeta.optional: trueso the plugin resolves to the profile's hoisted copy and a host without it is not an install error. For the same reason the SDK must never be a staticimportinlib/dcli-tools.mjs(that also takes downlib/index.js, which statically imports the tools module): keep it a guarded dynamicawait import(…)that leaves both constructorsundefinedon failure — never two adjacent({ X } = await import(…))statements, which ASI parses as one call expression — and letrequireMcpSdk()raise the actionable “install the optional peer” message at the LSP tools instead of aTypeError.test/package-contract.test.mjspins the manifest half andtest/tools.test.mjsthe import and error-path half.- Settings namespace
hmos-sidebarowns exactly two booleans,popup.keepCollapsedandball.hideWithoutProject, both defaulting totrue(quiet mode: no auto-expand popup; until a HarmonyOS project is probed the ball renders as a dimmed idle ball withopacity:.38/scale(.72)that still opens the panel on click — quiet mode must NEVER hide it, because the ball is the panel's only entry point and a probe chain that goes unavailable must not make the workbench unreachable in every workspace; the oldballVisible-style hidden ball is gone). The two hosts reach it differently: ≤ 0.1.5 registers the namespace withctx.settings.register('hmos-sidebar', …)and the client bindssettingsScope.bind({ namespace: 'hmos-sidebar' }), while ≥ 0.1.7 has noregisterat all — the namespace IS the entry's exportedConfig, keyed by the loader entry iddsh-hmos-sidebar(the client's first lookup), with both leaves marked.volatile()andctx.settings.configure({ auto: false }, ctx.fiber)declaring that this plugin renders its own card instead of a generated page. The marker is applied by capability because the 0.1.5 schemastery has no.volatile; an unconditional call would throw at module load. The host half never reads these values (the client owns quiet mode), and the client falls back to identical defaults when the service is absent or not ready. Never add a second persistence path for these flags. The entryConfigadditionally declares ONE optional volatilecliPathstring (thedeveco-clientry-file override for a row/profile-patchconfig.cliPathand for the preset's./toolsrow); without that declaration the host rejects the whole section (Config field "cliPath" is not volatile). It is deliberately NOT rendered by the card UI — the card keeps showing only the two switches — andvalidateSettingsaccepts it solely as an optional string, so the two booleans' defaults and behavior stay byte-identical. Unlike those two leave-only flags the host DOES readcliPath, and on the 0.1.7 line it must read it through the single shared gateconfigString()/configStringList()exported bylib/environment.js(detectSymbol.for('cosmokit.volatile.write'), then.get()): cordisresolveConfig()validates every entry config against thisConfigeven when the row declares noconfig:at all, soconfig.cliPathis always a cosmokit wrapper there — truthy, andnorm()renders it"[object Object]", which is what made 0.3.24's 环境 tab show[object Object] ⚠️ 未找到 [config]withjson5Ok: false. Never read aconfig.*value raw; the same gate covers the siblingconfig.projectPath/config.projectRoots/config.screenshotDirreads inlib/index.jsand everythingresolveEnv()consumes for the preset's./toolsrow.test/environment.test.mjsandtest/index-rpc.test.mjspin the wrapper, plain-string, absent/empty, and look-alike-object (non-wrapper) cases. The CARD seat is version-dependent too: ≤ 0.1.5 declaressettings.plugin.item(keyhmos-sidebar), while 0.1.7-rc.2 REMOVED that slot and a bundle row's configuration seat is the keyedplugins.row.config, whose occupant key must equal`dsh-hmos-sidebar#dsh-hmos-sidebar`=`${package.json#name}#<row id in cordis.patch.yml>`— the official manager renders the row's configure control only while that exact key sits on its ledger, so a card left on one seat renders nowhere, silently. Register the rc.2 occupant as ONE options object ({ name, key }, the slots service readsoptions.name) from inside a non-gatingctx.inject(['slots'], …)callback that returns the registration disposer, render a one-liner alone forview === 'summary'(inline styles: the card style tag is created lazily by a card render, so the summary can precede it) and the existing card for'page', and never read the host-owned optionalformprop. The card must also survive a transport that arrives after mount: the row seat waits only forslots(the plugin-manager page declares it the moment it opens) whileconfigForms/settingsScopecan land arbitrarily later, so a mounted card must render a visible 等待设置传输 / 加载中 / 设置不可用 state instead ofnullwhenever the snapshot is notready, re-render off the arrival broadcast that the resolvingctx.inject([...])callback fires through the apply-owned waiter set (each card unsubscribes in its own effect cleanup;ctx.effectclears the set on unload/update), and keep the field rows and the save control out of every non-ready state — a card must never render nothing silently, never fabricate values, and never show a save control that cannot work.@deepseek-ai/schemasterymust stay a privatedependenciesentry whose FLOOR is ≥ 3.18.4 (^3.18.4):^3.18.1is satisfied by the 3.18.2 copy the profile root hoists, so pnpm never materializes a volatile-capable copy,SettingsForms.describe()drops this entry (no volatile field) and the settings surface disappears with no error at all — measured on the live 0.1.7-rc.2 profile: this package resolved 3.18.2 while the three sibling pills resolved their own 3.18.4, and the config page rendered only the manager's header.test/client-source.test.mjsguards the seat, the key derivation, the summary branch and the late-transport contract. - The same card is ALSO registered on the root-scope list seat
settings.section(idyotk-hmos-sidebar, order64, label thunk() => 'YOTK · 鸿蒙工作台'), which is what makes it a first-class page one click deep in 设置. That seat is additive and host-version dependent: keep it BESIDE the row seat (never instead of it), register it with the same non-gatingctx.inject(['slots'], …)shape whose callback returns the registration disposer that joins the plugin's disposal path, and render the SAMEHmosSettingsCard(one settings UI, one transport, one persistence path) — so a host that does not declare the seat simply never fires it, the seat can never become an activation gate, and it must work with no settings transport at all. The card's disclosure default follows the seat:defaultOpen: trueon the two single-card seats (the row seat'sview === 'page'branch and thesettings.sectionpage), collapsed on the ≤ 0.1.5settings.plugin.itemlist card, with the header button still folding it back up.test/client-source.test.mjsguards the seat's nav identity, its transport-free registration and its shared card component in both copies. - DSH tool-presentation identifiers are
native,ptc, andboth; never reintroduce the removedcodeidentifier.native-harmonyosmust declaremode: ptc; Liangshen must keeppromotedPresentation: ptc, validatenative | ptc, and calltools.presentAs('ptc')after promotion. @deepseek-ai/dsh-personarows useprefix/suffix/complete/includeRuntimeContext;prefixis required since DSH 0.1.5-rc.1 and the pre-0.1.5textkey now fails the whole preset mount with- $.prefix missing required value (at prefix). That release also split the persona prompt section intodeployment:persona-prefix/deployment:persona-suffix, soPERSONA_SECTION_NAMESinpresets/liangshen-native-harmonyos/tool-bootstrap.mjsmust list the new name (keeping the old names is fine) or the phase-1 assembly loses its persona and the promoted workspace/PTC lines stop applying.- Each bundled preset has TWO payloads that must stay row-for-row equivalent:
presets/<id>/agent.cordis.yml(directory, ≤ 0.1.5) andpresets/<id>.declarative.yml(declaration, ≥ 0.1.7-rc.1). Exactly three deltas are legitimate, all forced by rc.1:@deepseek-ai/dsh-workflow-worker-thread→@deepseek-ai/dsh-workflow-ptc(row idworkflow-ptc, no upstream alias), theskills/directory located withcreateRequire(baseUrl).resolve('dsh-hmos-sidebar/package.json')instead ofnew URL(..., baseUrl), and rc.1's product-row vocabulary (backgroundMode: one-shot+maxDepth: provider-managed, replacingenableRunInBackground). A declaration has NO directory of its own, so one of its rows may never name./file.mjs; that is why the two Liangshen modules are package subpath exports. After touching either payload, runnode ..\..\..\scripts\preset-declarative-check.mjsfrom the workspace root: it parses both with the loader's YAML dialect and fails on any other drift, including persona text. - A profile package upgrade does not update existing user preset copies. Publish the corrected presets first, then run
pnpm exec dsh-hmos-sidebar install-presets --all --forcefrom the target profile directory so the profile's installed package owns conflict handling and backups. On ≥ 0.1.7-rc.1 nothing is copied at all: the profile patch only points at the installed package, so upgrading the package refreshes the preset and re-running the installer reports “already current”.
Validation
Run from this package root:
npm test
node --check lib/index.js
node --check lib/client.js
node --check lib/dcli-tools.mjs
node --check lib/environment.js
node --check lib/dual-signing.js
node --check bin/dsh-hmos-sidebar.mjs
node --check presets/liangshen-native-harmonyos/tool-bootstrap.mjs
npm pack --dry-run
Then, from the workspace root, verify the two payloads still describe the same preset:
node scripts/preset-declarative-check.mjs
To exercise the declarative installer without a ≥ 0.1.7 host, point it at a throwaway profile and force the shape: node bin/dsh-hmos-sidebar.mjs install-presets --all --mode declarative --profile-dir <tmp-profile> --dry-run (a real profile only ever gets --dry-run unless the user asked for the install).
For Web changes, reconcile with dsh plugin --profile web add ., restart the existing dsh web process when Host or package location changed, then verify the real http://127.0.0.1:3080 panel.
Pitfalls
- Package documentation historically said 40 tools while implementation/tests may assert 41; treat executable definitions/tests as source of truth and keep docs synchronized.
- Main bundle mounting and preset tool mounting are separate lifecycle units; a working panel does not prove tools are visible to an agent.
- When either bundled preset changes, update BOTH of its payloads (directory and declaration) and any custom bootstrap together, retain the PTC source assertions in
test/package-contract.test.mjs, confirm both preset trees appear innpm pack --dry-run, and follow the repository-level version/tag workflow in../../AGENTS.md. - DSH 0.1.7-rc.1 removed the directory preset roster outright: a preset that exists only as
presets/<id>/is not loaded, and nothing reports an error — it simply never appears in the picker. Installing the declaration on a ≤ 0.1.5 host is the opposite failure: the@deepseek-ai/dsh-agent-presetrow cannot resolve and the whole Web boot fails withN entries did not activate, which is why the installer probes the profile instead of shipping both shapes as bundle patches. tools.presentAs('ptc')throws in rc.1 when the same scope already declares a static presentation (one composition selects one presentation). Liangshen therefore must NOT gain atool-presentationrow while its bootstrap switches presentation imperatively;native-harmonyoskeeps its staticmode: ptc.process.cwd()is a fallback project candidate, not an automatically trusted path-fence root.- Do not add POSIX fallbacks that imply support; npm
EBADPLATFORMand runtime guards are deliberate. - For local development, use the package root as the working directory; do not hard-code a machine-specific path in source or published documentation.
