Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Each released version is tagged in git (`v0.0.1`, `v0.1.0`, etc.) and includes t

## [Unreleased]

### Changed

- **`draftwise skills install` auto-detects which AI harnesses are on the machine instead of installing to all three by default.** Previously the default behavior was to write SKILL.md into every known harness's user-level skill dir (`~/.claude/skills/draftwise/`, `~/.cursor/skills/draftwise/`, `~/.gemini/skills/draftwise/`) regardless of whether the user actually had Claude Code, Cursor, or Gemini CLI installed. Now `detectInstalledProviders` (in `src/utils/skill-providers.js`) checks which of `~/.claude` / `~/.cursor` / `~/.gemini` exist at the chosen scope root and installs only to those. The detected set is logged so the user sees *why* a particular harness was picked. `--provider=all` is the explicit opt-in for the old behavior; `--provider=<name>` still targets one harness regardless of detection. When detection finds nothing the command errors with a hint pointing at both override flags. `skills help` now also prints "Detected harnesses (user scope): …" and the project-scope equivalent so the auto-detect set is visible without having to run install. Why: this is the same behavior impeccable's install uses (via the `vercel-labs/skills` package — auto-detect, with `--all` as the explicit override) and it's friendlier than littering provider dirs with files for harnesses the user doesn't have. CLAUDE.md's "Standalone skill" section and README's slash-command callout updated to match. `skills uninstall` keeps its existing "iterate every known dir and skip ones with nothing to remove" behavior — different goal (clean up stale Draftwise installs whether or not the harness is still on disk), so detection-on-uninstall would miss the cleanup case. — Ankur

## [0.2.1] — 2026-04-29 — Ankur

The polish + cleanup release. Half ergonomic improvements (init now auto-detects new project vs existing codebase from the filesystem instead of asking; plain-language UX throughout init), half code-quality work (five small dedup + tidy PRs that shrink the public API surface, fix two user-visible typos, and consolidate the drafting-command boilerplate behind shared helpers). One genuinely new surface: `draftwise skills <install|uninstall|help>` drops standalone slash-command skills into Claude Code, Cursor, and Gemini CLI's user-level skill dirs — same SKILL.md, per-provider frontmatter trim — independent of the Claude Code marketplace plugin.
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ src/core/scanner.js → codebase scanning (frameworks, routes, components,
src/ai/provider.js → routes complete() calls to the right provider adapter
src/ai/providers/ → claude.js wired; openai.js + gemini.js stubbed
src/ai/prompts/ → one prompt module per command. Each exports brownfield + greenfield SYSTEM constants, a selectSystem(projectState) helper, a buildPrompt() that branches on projectState, and an agent-mode instruction
src/utils/ → config.js (yaml loader; returns projectState/stack/scanMaxFiles), specs.js (list .draftwise/specs/), slug.js, overview.js (read .draftwise/overview.md for greenfield context), scan-cache.js (fingerprinted scan cache, drop-in for scan()), flow-filter.js (narrow scan to flow-relevant items), scan-warnings.js (truncation + missing-framework messages), fs.js (shared pathExists), scan-projection.js (shared compactScan that trims a raw scan into a prompt-sized projection), tty.js (isInteractive helper), agent-handoff.js (shared orienting prefix logged before every agent-mode handoff), project-state.js (filesystem auto-detect for `init` — bail-fast walk for source files using scanner's IGNORE_DIRS + CODE_EXTENSIONS), skill-providers.js (provider dir mapping + Claude-only frontmatter trim shared across `skills install` / `uninstall` / `help`)
src/utils/ → config.js (yaml loader; returns projectState/stack/scanMaxFiles), specs.js (list .draftwise/specs/), slug.js, overview.js (read .draftwise/overview.md for greenfield context), scan-cache.js (fingerprinted scan cache, drop-in for scan()), flow-filter.js (narrow scan to flow-relevant items), scan-warnings.js (truncation + missing-framework messages), fs.js (shared pathExists), scan-projection.js (shared compactScan that trims a raw scan into a prompt-sized projection), tty.js (isInteractive helper), agent-handoff.js (shared orienting prefix logged before every agent-mode handoff), project-state.js (filesystem auto-detect for `init` — bail-fast walk for source files using scanner's IGNORE_DIRS + CODE_EXTENSIONS), skill-providers.js (provider dir mapping + Claude-only frontmatter trim + `detectInstalledProviders` filesystem check shared across `skills install` / `uninstall` / `help`)
test/ → vitest, mirrors src structure
.claude-plugin/ → plugin marketplace declaration (see "Claude Code plugin" below)
plugin/ → plugin source tree shipped via the marketplace
Expand All @@ -51,7 +51,7 @@ plugin/ → plugin source tree shipped via the marketplace
**Claude Code skill — two install paths, three harnesses on the standalone path.** `.claude-plugin/marketplace.json` at repo root declares a single `draftwise` plugin with `source: ./plugin`. Inside `plugin/` is `.claude-plugin/plugin.json` (the install manifest) and `skills/draftwise/SKILL.md` plus `skills/draftwise/reference/<verb>.md` per CLI verb. The same SKILL.md ships through two install paths with different slash-command shapes:

- **Marketplace plugin** (`/plugin marketplace add 4nkur/draftwise` then `/plugin install draftwise`): Claude Code namespaces all plugin skills as `<plugin>:<skill>`, so the chat form is `/draftwise:draftwise <verb>`. The namespace prefix is mandatory for plugin-installed skills regardless of `/plugin install` scope (user / project / project-this-user) — see [anthropics/claude-code#15882](https://github.com/anthropics/claude-code/issues/15882) (closed: not planned). Claude Code only.
- **Standalone skill** (`draftwise skills install`): writes the same SKILL.md into each known harness's user-level skill dir (`~/.claude/skills/draftwise/`, `~/.cursor/skills/draftwise/`, `~/.gemini/skills/draftwise/`). No plugin manifest sits alongside it, so each harness reads it as a regular user skill — bare `/draftwise <verb>`, matching the CLI binary. Same pattern impeccable uses for its bare verbs across providers. `--provider=<name>` narrows; `--scope=project` writes under `<cwd>` instead of `~`. Per-provider frontmatter trim in `src/utils/skill-providers.js` strips Claude-only fields (`user-invocable`, `argument-hint`, `allowed-tools`) for non-Claude harnesses; body is identical.
- **Standalone skill** (`draftwise skills install`): writes the same SKILL.md into each known harness's user-level skill dir (`~/.claude/skills/draftwise/`, `~/.cursor/skills/draftwise/`, `~/.gemini/skills/draftwise/`). No plugin manifest sits alongside it, so each harness reads it as a regular user skill — bare `/draftwise <verb>`, matching the CLI binary. Same pattern impeccable uses for its bare verbs across providers. **Default is auto-detect:** with no `--provider` flag, `detectInstalledProviders` checks which of `~/.claude` / `~/.cursor` / `~/.gemini` exist at the chosen scope root and installs only to those — same shape as impeccable's install behavior (which delegates to the `vercel-labs/skills` package). `--provider=all` forces install everywhere regardless of detection (the old default); `--provider=<name>` targets one harness regardless of detection; `--scope=project` writes under `<cwd>` instead of `~`. When auto-detect finds nothing, the command errors with a hint pointing at `--provider=all` / `--provider=<name>`. `skills uninstall` keeps its "remove from every known dir, skip empty" behavior — different goal (clean up stale Draftwise installs whether or not the harness is still present), so detection-on-uninstall would miss the cleanup case. Per-provider frontmatter trim in `src/utils/skill-providers.js` strips Claude-only fields (`user-invocable`, `argument-hint`, `allowed-tools`) for non-Claude harnesses; body is identical.

Pattern follows impeccable's distribution model: one skill routes to per-verb references that drive the conversation in chat and shell out to the npm-installed `draftwise` CLI. The two install paths are independent and may coexist (you'll see both `/draftwise:draftwise <verb>` and `/draftwise <verb>` listed in Claude Code). `package.json` `files` ships `plugin/skills/` (so the standalone install can copy from `node_modules/draftwise/plugin/skills/draftwise/`) but excludes `plugin/.claude-plugin/`. References include pre-flight checks (e.g. `new` warns if `overview.md` is stale, `tech` nudges to skim the product spec first) and tone shaping for how to ask the user about ambiguous flag values; they explicitly inherit `src/ai/prompts/principles.js`'s collaboration standards so the chat-driven conversation matches what the CLI's api-mode synthesis enforces. The `skills` subcommand group lives at `src/commands/skills/{install,uninstall,help}.js`; routing in `src/index.js` follows the `git remote <sub>` / `gh pr <sub>` pattern via `SUBCOMMAND_GROUPS`.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ draftwise init
```
draftwise skills install
```
Drops a standalone skill into each known harness's skill dir at the user level: `~/.claude/skills/draftwise/`, `~/.cursor/skills/draftwise/`, `~/.gemini/skills/draftwise/`. Slash form is `/draftwise init`, `/draftwise new "<idea>"`, etc. Narrow with `--provider=claude|cursor|gemini` or write at project scope with `--scope=project`. `draftwise skills help` shows what's installed where; `draftwise skills uninstall` removes it.
Auto-detects which harnesses you have on this machine (by checking for `~/.claude/`, `~/.cursor/`, `~/.gemini/`) and installs the standalone skill only to those — slash form `/draftwise init`, `/draftwise new "<idea>"`, etc. Pass `--provider=all` to install everywhere regardless of detection, `--provider=claude|cursor|gemini` to target one explicitly, or `--scope=project` to write at `<cwd>/.<provider>/skills/draftwise/` instead of the user-level dir. `draftwise skills help` shows what's installed where (and which harnesses auto-detect would target); `draftwise skills uninstall` removes it.
- **`/draftwise:draftwise <verb>` via the Claude Code plugin marketplace.** If you prefer Claude Code's `/plugin` workflow:
```
/plugin marketplace add 4nkur/draftwise
Expand Down
27 changes: 26 additions & 1 deletion src/commands/skills/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { pathExists } from '../../utils/fs.js';
import {
PROVIDER_NAMES,
PROVIDERS,
detectInstalledProviders,
resolveProviderTarget,
} from '../../utils/skill-providers.js';

Expand All @@ -13,7 +14,8 @@ Usage:

Walks ~/.<provider>/skills/draftwise/ and <cwd>/.<provider>/skills/draftwise/
for each known harness (Claude Code, Cursor, Gemini CLI), reports what's
installed, and points at the matching install / uninstall command.
installed, and prints the auto-detected harness set that
\`draftwise skills install\` (with no --provider flag) would target.
`;

function pad(s, n) {
Expand All @@ -39,6 +41,29 @@ export default async function skillsHelp(_args = [], deps = {}) {
}
}
log('');

const detectedUser = await detectInstalledProviders({
scope: 'user',
cwd,
home,
});
const detectedProject = await detectInstalledProviders({
scope: 'project',
cwd,
home,
});
const labelList = (names) =>
names.length === 0 ? 'none' : names.map((n) => PROVIDERS[n].label).join(', ');
log(
`Detected harnesses (user scope, --scope=user): ${labelList(detectedUser)}`,
);
log(
`Detected harnesses (project scope, --scope=project): ${labelList(detectedProject)}`,
);
log(
'`draftwise skills install` (no flag) targets the detected set; pass --provider=all to override.',
);
log('');
log('Install: draftwise skills install [--provider=<name>] [--scope=<user|project>] [--force]');
log('Uninstall: draftwise skills uninstall [--provider=<name>] [--scope=<user|project>]');
log('');
Expand Down
45 changes: 36 additions & 9 deletions src/commands/skills/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,24 @@ import { pathExists } from '../../utils/fs.js';
import {
PROVIDER_NAMES,
PROVIDERS,
detectInstalledProviders,
resolveProviderTarget,
transformSkillForProvider,
} from '../../utils/skill-providers.js';

export const HELP = `draftwise skills install — install Draftwise as a standalone slash-command skill

Usage:
draftwise skills install # install for all known harnesses (Claude, Cursor, Gemini)
draftwise skills install --provider=claude # install for one harness
draftwise skills install # auto-detect harnesses on disk (~/.claude, ~/.cursor, ~/.gemini) and install to those
draftwise skills install --provider=all # install to every known harness regardless of detection
draftwise skills install --provider=claude # install to one harness regardless of detection
draftwise skills install --scope=project # install at <cwd>/.<provider>/skills/draftwise/
draftwise skills install --force # overwrite existing installs

Flags:
--provider <claude|cursor|gemini|all> Which harness(es) to target. Default: all.
--provider <claude|cursor|gemini|all> Which harness(es) to target. Default: auto-detect
(install only to harnesses whose provider dir
exists at the chosen scope).
--scope <user|project> user → ~/.<provider>/skills/draftwise/
project → <cwd>/.<provider>/skills/draftwise/
Default: user.
Expand Down Expand Up @@ -79,14 +83,25 @@ async function copyTreeWithTransform(src, dest, provider) {
}
}

function resolveProviderList(flag) {
if (!flag || flag === 'all') return PROVIDER_NAMES;
if (!PROVIDER_NAMES.includes(flag)) {
async function resolveProviderList(flag, { scope, cwd, home }) {
if (flag === 'all') return PROVIDER_NAMES;
if (flag) {
if (!PROVIDER_NAMES.includes(flag)) {
throw new Error(
`Invalid --provider value "${flag}". Use one of: ${PROVIDER_NAMES.join(', ')}, all.`,
);
}
return [flag];
}
const detected = await detectInstalledProviders({ scope, cwd, home });
if (detected.length === 0) {
const root = scope === 'project' ? cwd : home;
const dirs = PROVIDER_NAMES.map((p) => PROVIDERS[p].providerDir).join(', ');
throw new Error(
`Invalid --provider value "${flag}". Use one of: ${PROVIDER_NAMES.join(', ')}, all.`,
`No AI harnesses detected at ${root} (looked for: ${dirs}). Pass --provider=all to install for every known harness, or --provider=<claude|cursor|gemini> to target one explicitly.`,
);
}
return [flag];
return detected;
}

export default async function skillsInstall(args = [], deps = {}) {
Expand Down Expand Up @@ -117,14 +132,26 @@ export default async function skillsInstall(args = [], deps = {}) {
);
}
const force = Boolean(parsed.values.force);
const providers = resolveProviderList(parsed.values.provider);
const providers = await resolveProviderList(parsed.values.provider, {
scope,
cwd,
home,
});

if (!(await pathExists(sourceDir))) {
throw new Error(
`Skill source not found at ${sourceDir}. Try reinstalling draftwise: npm i -g draftwise.`,
);
}

// Tell the user *why* this provider list was chosen when they didn't pass
// a flag. Auto-detect picking one harness when three exist on the machine
// is otherwise silent and confusing.
if (!parsed.values.provider) {
const labels = providers.map((p) => PROVIDERS[p].label).join(', ');
log(`Detected harness(es): ${labels}. (Use --provider=all to install everywhere, or --provider=<name> to target one.)`);
}

// Fail fast on conflicts before we write anything: if any target dir exists
// and --force wasn't passed, error with a single message that lists every
// conflict — friendlier than half-installing then erroring.
Expand Down
17 changes: 17 additions & 0 deletions src/utils/skill-providers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { join } from 'node:path';
import { homedir } from 'node:os';
import { parse as yamlParse, stringify as yamlStringify } from 'yaml';
import { pathExists } from './fs.js';

// One row per AI harness Draftwise's standalone skill targets. Each provider
// reads SKILL.md from its own `<scope>/<provider-dir>/skills/draftwise/`
Expand Down Expand Up @@ -33,6 +34,22 @@ export function resolveProviderTarget({ provider, scope, cwd, home = homedir() }
return join(root, meta.providerDir, 'skills', 'draftwise');
}

// Returns the subset of PROVIDER_NAMES whose provider dir exists at the given
// scope root — `~/.claude`, `~/.cursor`, `~/.gemini` for user scope, or the
// `<cwd>/.<provider-dir>` equivalents for project scope. The presence of the
// dir is treated as a proxy for "this harness is installed on this machine."
// Used by `skills install` to install only where the user actually has the
// harness, instead of writing to all three by default.
export async function detectInstalledProviders({ scope, cwd, home = homedir() }) {
const root = scope === 'project' ? cwd : home;
const found = [];
for (const provider of PROVIDER_NAMES) {
const dir = join(root, PROVIDERS[provider].providerDir);
if (await pathExists(dir)) found.push(provider);
}
return found;
}

// Splits a SKILL.md (or any markdown with YAML frontmatter) into its
// frontmatter object and body string. Returns null frontmatter if the file
// has no frontmatter block — caller decides what to do.
Expand Down
17 changes: 17 additions & 0 deletions test/commands/skills/help.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,21 @@ describe('draftwise skills help (state report)', () => {
expect(out).toMatch(/Claude Code\s+user\s+not installed/);
expect(out).toMatch(/Gemini CLI\s+project\s+not installed/);
});

it('shows the detected-harness set that auto-detect install would target', async () => {
await mkdir(join(home, '.claude'), { recursive: true });
await mkdir(join(cwd, '.gemini'), { recursive: true });

await skillsHelp([], deps());
const out = logs.join('\n');
expect(out).toMatch(/Detected harnesses \(user scope[^)]*\):\s+Claude Code/);
expect(out).toMatch(/Detected harnesses \(project scope[^)]*\):\s+Gemini CLI/);
});

it('reports "none" when no harness dirs exist at a scope', async () => {
await skillsHelp([], deps());
const out = logs.join('\n');
expect(out).toMatch(/Detected harnesses \(user scope[^)]*\):\s+none/);
expect(out).toMatch(/Detected harnesses \(project scope[^)]*\):\s+none/);
});
});
Loading