@zapier/connectors-dev
v0.16.1
Published
Standard dev tooling and validator for connectors: validate (with --fix), stage, and scaffold-connector.
Readme
@zapier/connectors-dev
Standard dev tooling and validator for connectors built with @zapier/connectors-sdk. Provides a unified connectors-dev CLI and a programmatic API:
| Command | What it does |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| validate | Validate connectors against agentskills.io rules (via skills-ref) plus the connector contract (package.json, index.ts, cli.ts, scripts/*.ts, evals/evals.json). With --fix, apply every fixable rule instead of reporting: shared assets, index.ts wiring, and package.json fundamentals. Vendor-specific policy (e.g. NOTICE / legal fragments / LICENSE) lives in an extends config package such as @zapier/connectors-dev-config. |
| scaffold-connector | Bootstrap a new connector shell: package.json, connections.ts, SKILL.md, index.ts, and placeholder directories. |
| stage | Copy a connector into a clean tmp directory (git-tracked files only) for standalone install testing — mirrors what an agentskills.io client receives. |
| render-doc | Print the canonical body of one doc (SKILL.md or README.md) a connector should match (the body scaffold-connector would generate now), for diffing against the live doc when migrating to updated templates. The file to render is a required argument: render-doc <dir> SKILL.md or render-doc <dir> README.md. |
CLI
# Validate a single connector
connectors-dev validate ./apps/my-connector
# Validate every connector under apps/
# (a path that isn't itself a connector is treated as a folder of connectors)
connectors-dev validate apps
# Apply every fixable rule (shared assets, index.ts, package.json, + any config rules)
connectors-dev validate apps/my-connector --fix
# Fix every connector under apps/
connectors-dev validate apps --fix
# Bootstrap a new connector
connectors-dev scaffold-connector ./apps/my-connector --slug my-connector
connectors-dev scaffold-connector ./apps/my-connector --slug my-connector --license Elastic-2.0
# Stage a connector for standalone testing
connectors-dev stage apps/notion
# → prints path to tmp dir; capture it: path=$(connectors-dev stage apps/notion)
# Stage and also produce a ZIP
connectors-dev stage apps/notion --zip
# Vendor the local SDK source into the staging dir (test SDK changes before publishing)
connectors-dev stage apps/notion --local-sdk packages/connectors-sdk
# Print the canonical doc a connector should match (for migrating to updated templates)
connectors-dev render-doc apps/notion SKILL.md
connectors-dev render-doc apps/notion README.mdRun connectors-dev with no arguments for the full option reference.
Programmatic API
import {
scaffoldConnector,
validate,
formatIssues,
evalsJsonSchema,
describeEvalsIssue,
} from "@zapier/connectors-dev";
// Validate connectors and format any issues. The rule set and package-name
// pattern come from connectors-dev.config.ts (loaded from the cwd).
const results = await validate(["./apps/my-connector"]);
for (const [connectorPath, issues] of results) {
console.error(formatIssues(connectorPath, issues));
}
// Scaffold a new connector. The rule set and package-name pattern come from
// connectors-dev.config.ts too (loaded from `cwd`, defaulting to process.cwd()).
const { connectorDir, filesWritten } = await scaffoldConnector({
slug: "my-connector",
license: "Elastic-2.0",
out: "./apps/my-connector",
});
// Parse/validate evals.json against the same schema the validator uses
const parsed = evalsJsonSchema.safeParse(rawEvals);
if (!parsed.success) {
for (const issue of parsed.error.issues)
console.error(describeEvalsIssue(issue));
}Validation
connectors-dev validate <path>... composes two layers:
- agentskills.io
skills-ref—SKILL.mdfrontmatter and skill-folder rules. - Connector contract —
package.json, bundleindex.ts/cli.ts,scripts/*.ts(defineTool+handleIfScriptMain),evals/evals.json, andSKILL.mdextensions (metadata.api-docs,compatibility,metadata.source).
Vendor-specific policy (e.g. a scoped package name, LICENSE / NOTICE, legal fragments) is not built in — it arrives through an extends config package such as @zapier/connectors-dev-config. A connector copied out of the monorepo runs with no config and is therefore only held to the brand-neutral contract above.
Each path is auto-detected: a directory with a package.json is a single
connector, otherwise its immediate subdirectories with a package.json are
validated (a folder of connectors).
Check and --fix report identically — each issue is one <icon> path [rule] message
line, where the icon is its status: ✅ fixed, 🤖 fixable (rerun with --fix),
⛔️ non-fixable (fix by hand). A trailing summary tallies Fixed / Fixable /
Non-fixable. Exit code is 0 only when nothing blocks: a check run passes when
there are no issues, a --fix run passes when nothing non-fixable remains
(✅-fixed issues don't fail it). Blocking runs print to stderr and exit 1.
Options
| Flag | Purpose |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| --fix | Apply every fixable rule instead of reporting (shared assets, index.ts, package.json, plus any config-supplied rules) |
| --ignore-rule <rule> | Ignore a rule for this run only (repeatable: --ignore-rule foo --ignore-rule bar). Unioned with the config's ignoreRules — never modifies the config file itself. |
The rule set and package.json name policy come from
connectors-dev.config.ts (or the built-in defaults) — there
is no per-run name-pattern flag.
What it checks
| Layer | Rule tag | Method |
| ---------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| SKILL.md | agentskills | Upstream skills-ref validate() |
| SKILL.md metadata.api-docs | api-docs | HTTPS URL presence — agents need it to look up endpoints and auth schemes |
| SKILL.md compatibility | compatibility | Required field with pinned value — harnesses read it to decide if they can run the connector |
| SKILL.md metadata.source | source | Present + valid https:// URL (the exact canonical URL is a vendor policy, enforced by an extends config) |
| scripts/, package.json, evals/evals.json | scripts-dir, package.json, evals.json | JSON + filesystem (scripts-dir fixably creates a missing scripts/) |
| index.ts, scripts/*.ts | index.ts, scripts | TypeScript compiler API (AST) |
| scripts/*.ts runtime shape | scripts | Dynamic import + getToolDefinitionShapeIssues |
| cli.js, cli.ts, tsup.config.ts | cli.js, cli.ts, tsup.config.ts | Byte-identical sync to the canonical assets/ copy |
Rules & fixing
validate and validate --fix are the same pass over the same rule set — the
only difference is the fix flag on the context. A ConnectorRule is a single
function that inspects one connector, reports findings via ctx.report, and
repairs what it can when ctx.fix is set:
interface RuleContext {
connectorDir: string;
packageNamePattern: string;
fix: boolean;
report(finding: {
message: string;
file?: string;
line?: number;
fixStatus?: "fixable" | "fixed";
}): void;
}
type ConnectorRule = (ctx: RuleContext) => void | Promise<void>;
interface RuleEntry {
name: string;
rule: ConnectorRule;
}A rule never names itself: the name lives on the registry RuleEntry, and the
runner stamps it onto every reported finding. Findings carry an optional
fixStatus — "fixable" in a check run, "fixed" after a fix run — so the CLI
can report how many issues a --fix repaired and which still need hand-editing.
runRule(entry, base) runs a single rule against one connector and returns the
stamped findings; the base context's fix flag is the only thing that makes it
a check run versus a fix run. It's the unit-test entry point for a rule. Rules get
no special treatment — a builtin rule is the same shape a downstream
config/policy package would provide, so a rule can move out of this package
unchanged.
Configuration
validate, validate --fix, and scaffold-connector read an optional
connectors-dev.config.{ts,js,mjs} from the working directory (loaded with
c12, so extends layers and a sibling .env
work). A connector copied out of the monorepo has no config and falls back to
the built-in defaults, which is correct for a standalone install.
import { defineConnectorsDevConfig } from "@zapier/connectors-dev";
export default defineConnectorsDevConfig({
// Pull in a shared policy package (resolved + merged first).
extends: "@zapier/connectors-dev-config",
// package.json name pattern; {name} = connector slug. Default "{name}-connector".
packageNamePattern: "@zapier/{name}-connector",
// Drop builtin rules by name…
ignoreRules: ["legal"],
// …and append your own (same RuleEntry shape as a builtin).
rules: [{ name: "house-style", rule: (ctx) => {} }],
});| Field | Default | Purpose |
| -------------------- | -------------------- | ----------------------------------------------------------------------------------- |
| packageNamePattern | "{name}-connector" | package.json name pattern; {name} is the connector slug (directory basename). |
| extends | (none) | Local path(s) or package(s) to inherit and deep-merge (a later layer wins). |
| ignoreRules | [] | Builtin rule names to drop from the run. |
| rules | [] | Extra RuleEntry objects appended after the builtins. |
A config-supplied rule runs through the exact same path as a builtin — there is
no builtin-vs-external branch — so a rule can migrate to a downstream policy
package (dropped here via ignoreRules, added there via rules) with no engine
change. packageNamePattern is config-only: there is no --naming flag, so
validate and scaffold-connector always agree on the package name a connector
should carry.
Assets
The package ships canonical connector asset files under assets/:
| File | Purpose |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| cli.js | Plain-JS connector CLI proxy — runs dist/cli.js when present (npm install route) or falls back to cli.ts (Node 22.18+). Ports the old preflight readiness checks: bails with actionable guidance when node_modules is missing or the .ts source can't run on this Node. |
| cli.ts | TypeScript connector CLI entry point (no shebang; executed via cli.js). |
| tsup.config.ts | tsup build config shared by all connectors. |
Each asset is enforced by its own byte-identical rule (createAssetRule): content equality is the whole contract, so no separate shebang / AST / exec-bit check is needed. These files are the source of truth — edit here; run connectors-dev validate <dir> --fix (or the monorepo's pnpm run validate:fix) to propagate changes to every connector.
scaffold-connector options
Invoked as scaffold-connector <dir> --slug <slug> [options] — the output directory is the one positional, the slug is a required named option.
| Option | Default | Notes |
| ---------------------- | ----------------- | ------------------------------------------------------------------ |
| --slug <slug> | (required) | Connector slug (lowercase, digits, hyphens; starts with a letter) |
| --title <name> | titleCase(slug) | Human-readable app display name |
| --license <spdx> | (none) | SPDX license identifier written into package.json and SKILL.md |
| --description <text> | auto | One-sentence description for package.json and SKILL.md |
| --api-docs <url> | (placeholder) | Vendor API docs URL for SKILL.md frontmatter |
| --force | false | Overwrite an existing directory |
The package name follows the packageNamePattern from
connectors-dev.config.ts (default {name}-connector), so a scaffolded
connector passes validate without a separate scope flag.
