Skip to main content

CREATE AND PUBLISH

This guide shows you how to create a TypeScript extension, publish it to the swamp registry, and install it in another repo.

If you are publishing for the first time, Publish Your First Extension walks the same pipeline step by step, and About the Extension Publishing Lifecycle explains why each step gates the next.

Prerequisites

  • A swamp repository (swamp repo init)
  • A swamp account (swamp auth login)

Write the extension model

Create the extension file at extensions/models/disk_usage.ts:

import { z } from "npm:zod@4";

export const model = {
  type: "@mycollective/disk-usage",
  version: "2026.04.03.1",

  globalArguments: z.object({
    path: z.string().describe("Filesystem path to check"),
  }),

  resources: {
    result: {
      description: "Disk usage statistics",
      schema: z.object({
        path: z.string(),
        totalBytes: z.number(),
        usedBytes: z.number(),
        availableBytes: z.number(),
        usedPercent: z.number(),
      }),
      lifetime: "infinite" as const,
      garbageCollection: 10,
    },
  },

  methods: {
    check: {
      description: "Check disk usage at the configured path",
      arguments: z.object({}),
      execute: async (
        _args: Record<string, unknown>,
        context: Record<string, unknown>,
      ) => {
        const globalArgs = context.globalArgs as { path: string };

        const cmd = new Deno.Command("df", {
          args: ["-k", globalArgs.path],
          stdout: "piped",
          stderr: "piped",
        });
        const output = await cmd.output();
        const stdout = new TextDecoder().decode(output.stdout);
        const lines = stdout.trim().split("\n");
        const parts = lines[1].split(/\s+/);

        // `df -k` reports 1024-byte blocks on both Linux and macOS.
        const totalBytes = parseInt(parts[1]) * 1024;
        const usedBytes = parseInt(parts[2]) * 1024;
        const availableBytes = parseInt(parts[3]) * 1024;
        const usedPercent = Math.round((usedBytes / totalBytes) * 100);

        const writeResource = context.writeResource as (
          spec: string,
          name: string,
          data: Record<string, unknown>,
        ) => Promise<{ name: string }>;
        const resultHandle = await writeResource("result", "result", {
          path: globalArgs.path,
          totalBytes,
          usedBytes,
          availableBytes,
          usedPercent,
        });

        return { dataHandles: [resultHandle] };
      },
    },
  },
};

Replace @mycollective with your collective name. Run swamp auth whoami to check your username and collectives.

Pin your imports

The example uses npm:zod@4 — note the @4 version pin. Pin every non-local import for reproducibility:

  • npm:lodash-es@4.17.21
  • jsr:@std/assert@1.0.0
  • https://deno.land/std@0.224.0/async/delay.ts (version in the URL)

Any Deno-compatible specifier works — npm:, jsr:, and https: are all inlined into the bundle at push time. Unpinned specifiers resolve to the registry's current "latest" when the extension is bundled, so the published bundle silently changes whenever the upstream package publishes a new version.

Test the model locally

Create a model definition from the new type:

$ swamp model create @mycollective/disk-usage home-disk --global-arg path=/home

The output confirms the model was created and lists its global arguments and methods.

Run the method to confirm it executes:

$ swamp model method run home-disk check

The report at the end shows succeeded and the number of resources produced.

Create the manifest

Create extensions/models/manifest.yaml:

manifestVersion: 1

name: "@mycollective/disk-usage"
version: "2026.04.03.1"
description: "Check disk usage statistics for a filesystem path"
repository: "https://github.com/mycollective/disk-usage"

models:
  - disk_usage.ts

labels:
  - monitoring
  - filesystem

The name must start with your collective prefix (@username/ or @org/). Run swamp auth whoami to check which collectives you belong to. The repository URL is optional, but swamp extension push warns when it is absent and the quality score rewards a verified public repository.

Bump the version

Before each publish, get the next version from the registry:

$ swamp extension version --manifest extensions/models/manifest.yaml

You will see output like:

info    extension·version Current published: (none — not yet published)
info    extension·version Next version (today): "2026.04.13.1"

Update manifest.yaml with the next version. If the model source file also contains a version field, update it to match.

Format, lint, and validate

Format the extension source:

$ swamp extension fmt extensions/models/manifest.yaml

To check without modifying files, add --check:

$ swamp extension fmt extensions/models/manifest.yaml --check

You will see output like:

info    extension·fmt All quality checks passed.

Score the extension against the quality rubric. This does not gate the push — the score is informational — but it surfaces missing factors while you still have the source open:

$ swamp extension quality extensions/models/manifest.yaml

The output lists each factor and whether it was earned. See Improve Your Score for per-factor fixes and the Scorecard Rubric for the full list.

Validate with a dry run before publishing:

$ swamp extension push extensions/models/manifest.yaml --dry-run

The dry run validates the extension without uploading. If the push reports safety warnings (e.g., Deno.Command() usage), you will be prompted to confirm.

The dry run also prints any review warnings. You will see output like:

Extension review warnings:
  ["medium"] "Testing Completeness" — "extensions/models/hello.ts":
    "No sibling `_test.ts` found — cover both success and failure paths
    with unit tests before publishing."
  ["medium"] "Adversarial review" — "/tmp/.../review-report.json":
    "No adversarial review recorded for the current code — perform the
    review and write the report here ..."

Address review warnings

Review warnings do not block the push — you will be prompted to confirm if any are present. To resolve them before publishing:

Add missing tests. If the warning identifies a model file without a sibling test, create a _test.ts file next to it that covers both success and failure paths.

Write the adversarial-review report. Run the dry run with --json to get the report skeleton:

$ swamp extension push extensions/models/manifest.yaml --dry-run --json

The JSON output includes a reviewRuleWarnings entry with a skeleton field containing a pre-filled JSON template. Copy the skeleton to the file path shown in the warning, then fill in each dimension's verdict ("pass", "issue", or "not-applicable") and note. Re-run the dry run to confirm the warnings clear.

If you use Claude Code, the swamp-extension skill performs the review and writes the report automatically.

Publish to the registry

$ swamp extension push extensions/models/manifest.yaml

You will see output like:

info    extension·push Pushed "@mycollective/disk-usage"@"2026.04.13.1"
info    extension·push Extension ID: "bbc4f071-..."
info    extension·push Archive size: "1.4KB"
info    extension·push "Models: 1, Workflows: 0, Vaults: 0, Bundles: 1"

If the version already exists, the CLI offers to bump the MICRO component.

Add --release-notes "description of changes" to attach release notes, or add a releaseNotes field to the manifest.

Tip

If you use Claude Code, the /swamp-extension-publish skill walks you through the full publishing prerequisites — repo initialization, authentication, manifest validation, collective ownership, version bumping, formatting, and dry-run — before allowing the push.

Install the extension in another repo

In a different swamp repo, pull the extension:

$ swamp extension pull @mycollective/disk-usage

The model type is now available. Create definitions from it with swamp model create @mycollective/disk-usage <name>.

To restore pulled extensions after cloning (e.g., in CI), run swamp extension install. See Extension Management for the full reference on pull, install, list, update, outdated, and rm.