Core Development

Packaging & Installing Plugins

Learn to bundle, test locally with npm link, publish to npm, and install plugins directly via cordis.yml.

During local development, developers typically mount plugins directly using the --patch parameter. However, in cross-team collaboration, enterprise artifact registry publication, or automated CI/CD deployment, plugins must be packaged as standardized Bundles and installed into runnable Profiles using the dsh plugin command.

This tutorial breaks down the Bundle vs. Profile architecture, the four-tier layer composition hierarchy, and walks through packaging an “Enterprise CI/CD Automation Pipeline Bundle (dsh-cicd-runner)”.


Conceptual Foundation: The Dual Contract of Bundles and Profiles

The Harness distribution system separates capabilities from execution environments:

┌─────────────────────────────────────────────────────────────┐
│                 Bundle vs. Profile Architecture             │
├─────────────────────┬───────────────────────────────────────┤
│ 1. Bundle (Package) │ A published npm package declaring     │
│                     │ dsh.bundle in package.json (Patch layer)│
├─────────────────────┼───────────────────────────────────────┤
│ 2. Profile (Runtime)│ An executable environment declaring   │
│                     │ dsh.profile in package.json (Bundles) │
└─────────────────────┴───────────────────────────────────────┘
  • Bundle: Authored and published by extension developers. Contains compiled code and a cordis.patch.yml layer specifying contributed plugin nodes;
  • Profile: Stored under $DSH_HOME/profiles/<name>. Represents a concrete execution target recording the active bundle dependency list and their loading sequence.

Step 1: Author the Bundle Package

Establish the standard directory tree for dsh-cicd-runner:

dsh-cicd-runner/
├── package.json       # Declares dsh.bundle metadata
├── cordis.patch.yml   # Patch layer applied when the bundle is included
└── index.js           # Compiled plugin entry point

1) Write package.json Manifest

json
{
  "name": "dsh-cicd-runner",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "files": ["index.js", "cordis.patch.yml"],
  "dsh": {
    "bundle": {
      "patch": "./cordis.patch.yml"
    }
  }
}

2) Implement Plugin Logic index.js

javascript
export const name = 'cicd-runner';

export function apply(ctx) {
    console.log('[CI/CD]  Continuous integration plugin loaded successfully into Profile!');
}

3) Write Configuration Patch cordis.patch.yml

Production Packaging Rule: Bundle patch files must reference the plugin by its npm package name rather than a relative path, allowing standard Node.js module resolution to locate files across any host environment:

yaml
- insert:
    - id: cicd-service
      name: dsh-cicd-runner
      config:
        autoTriggerOnPush: true

Step 2: Installing into a Profile and Live Verification

Use dsh plugin --profile <name> to install bundles:

bash
# Install local bundle into a profile named prod-agent
dsh plugin --profile prod-agent add ./dsh-cicd-runner

First use initializes the profile directory, appending the new package to dsh.profile.bundles:

json
{
  "name": "dsh-profile-prod-agent",
  "private": true,
  "dependencies": {
    "dsh-cicd-runner": "link:./dsh-cicd-runner"
  },
  "dsh": {
    "profile": {
      "bundles": [
        "@deepseek-ai/dsh-base",
        "dsh-cicd-runner"
      ]
    }
  }
}

Inspection and Execution:

bash
# 1. Preview the merged YAML configuration tree
dsh --profile prod-agent --dump-config

# 2. Boot the profile
dsh --profile prod-agent

To remove the bundle:

bash
dsh plugin --profile prod-agent remove dsh-cicd-runner

Four-Tier Layer Composition Order

Configuration layers are composed bottom-up in strict priority:

┌─────────────────────────────────────────────────────────────┐
│  Tier 4: CLI Overrides (--patch <path>)                     │ ◀ Highest Priority
├─────────────────────────────────────────────────────────────┤
│  Tier 3: Global Home Patch ($DSH_HOME/cordis.patch.yml)     │
├─────────────────────────────────────────────────────────────┤
│  Tier 2: Profile Patch (profile/cordis.patch.yml)           │
├─────────────────────────────────────────────────────────────┤
│  Tier 1: Bundle Layers (ordered by dsh.profile.bundles)     │ ◀ Base Floor
└─────────────────────────────────────────────────────────────┘

Whole-Row Replacement Invariant: Higher tiers perform whole-row replacements for matching plugin IDs rather than recursive object merges. If adjusting a single property, specify all required fields for that row in your patch.


Frequently Asked Questions (FAQ)

Q1: Can a project be both a Bundle and a Profile?

Answer: No. A Bundle is an installable package declaring dsh.bundle; a Profile is an executable environment declaring dsh.profile. Their architectural roles are mutually exclusive.

Q2: Why must cordis.patch.yml reference the npm package name?

Answer: When installed via npm or dsh plugin add, files reside in node_modules. Referencing the package name ensures that Node module resolution locates compiled code deterministically across any environment.

Q3: Why does Patch execute "whole-row replacement" instead of deep recursive merging?

Answer: Whole-row replacement guarantees predictable configuration states, preventing hidden ghost properties from lingering after overrides. Use dsh --dump-config to inspect the final composite tree.

Q4: How do I install custom Bundles in air-gapped environments without internet access?

Answer: Package the bundle into a tarball using npm pack, then run dsh plugin --profile prod add ./dsh-cicd-runner-1.0.0.tgz. The package manager links directly from the local archive.