Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
7
.changeset/790-augment-commands.md
Normal file
7
.changeset/790-augment-commands.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
type: Added
|
||||
pr: 801
|
||||
---
|
||||
**Augment (Auggie) installs now emit slash command definitions alongside skills.** A global `--augment` install writes `commands/gsd-<name>.md` files to `~/.augment/commands/` in addition to the existing `skills/gsd-<name>/SKILL.md` files, matching the integration depth of other fully-elevated runtimes and allowing Auggie users to invoke GSD as slash commands (`/gsd-phase`, `/gsd-ship`, etc.) without manual configuration (#790). Content rewrites (path normalisation and Augment-specific branding) are applied at install time. Uninstall removes the `gsd-*` command files while preserving user-owned commands. `mcpServers` registration is explicitly excluded — gsd ships no MCP server and does not register third-party servers.
|
||||
|
||||
<!-- docs-exempt: internal installer enhancement; the user-facing behaviour (slash command availability in Augment) is described in README.md which already covers the supported runtime list -->
|
||||
@@ -6320,6 +6320,46 @@ function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix) {
|
||||
walkAndRewrite(stagedDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply per-runtime content rewrites to flat .md files in a staged commands dir.
|
||||
* Used for runtimes that have a commandsKind in their layout and need content rewrites
|
||||
* (e.g. augment — replaces ~/.claude/ paths and applies branding conversions).
|
||||
*
|
||||
* IMPORTANT: `stageSkillsForProfile()` returns the original source directory unchanged
|
||||
* on a full/default profile (skills === '*'). This function MUST NOT mutate that source
|
||||
* directory. It always copies to a temp dir first, rewrites there, and returns the new
|
||||
* path so the caller installs from the temp copy, not the source.
|
||||
*
|
||||
* @param {string} stagedDir directory of staged flat .md command files (may be source dir)
|
||||
* @param {string} runtime
|
||||
* @param {string} pathPrefix
|
||||
* @returns {string} path to a temp dir with rewritten files (caller is responsible for cleanup)
|
||||
*/
|
||||
function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathPrefix) {
|
||||
if (!fs.existsSync(stagedDir)) return stagedDir;
|
||||
// Always copy to a temp dir — stageSkillsForProfile() returns the original source
|
||||
// dir on full/default profile (skills === '*'), so writing in-place would corrupt the
|
||||
// package source. A temp copy is unconditional to keep the code simple and safe.
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cmd-rewrites-'));
|
||||
try {
|
||||
for (const entry of fs.readdirSync(stagedDir, { withFileTypes: true })) {
|
||||
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
||||
let content = fs.readFileSync(path.join(stagedDir, entry.name), 'utf8');
|
||||
content = _applyRuntimeRewrites(content, runtime, pathPrefix);
|
||||
// For augment commands, apply the markdown conversion so tool references
|
||||
// and skill paths use Augment equivalents.
|
||||
if (runtime === 'augment') {
|
||||
content = convertClaudeToAugmentMarkdown(content);
|
||||
}
|
||||
fs.writeFileSync(path.join(tempDir, entry.name), content);
|
||||
}
|
||||
} catch (err) {
|
||||
try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
||||
throw err;
|
||||
}
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the per-runtime rewrite table to a single content string.
|
||||
* Extracted so it can be unit-tested independently of the filesystem walk.
|
||||
@@ -6718,8 +6758,14 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
|
||||
|
||||
for (const kind of layout.kinds) {
|
||||
const staged = kind.stage(resolvedProfile);
|
||||
// stagedForCopy: the directory to copy from (may differ from staged if rewrites
|
||||
// produce a temp copy — see applyRuntimeContentRewritesForCommandsInPlace).
|
||||
let stagedForCopy = staged;
|
||||
if (kind.kind === 'skills') {
|
||||
applyRuntimeContentRewritesInPlace(staged, runtime, pathPrefix);
|
||||
} else if (kind.kind === 'commands') {
|
||||
// Returns a temp dir with rewritten content so source files are never mutated.
|
||||
stagedForCopy = applyRuntimeContentRewritesForCommandsInPlace(staged, runtime, pathPrefix);
|
||||
}
|
||||
const dest = path.join(layout.configDir, kind.destSubpath);
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
@@ -6741,8 +6787,8 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
|
||||
|
||||
if (kind.prefix === '') {
|
||||
// Hermes: wipes entire dest dir — preserve anything not in staged.
|
||||
const stagedNames = fs.existsSync(staged)
|
||||
? new Set(fs.readdirSync(staged, { withFileTypes: true })
|
||||
const stagedNames = fs.existsSync(stagedForCopy)
|
||||
? new Set(fs.readdirSync(stagedForCopy, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory()).map(e => e.name))
|
||||
: new Set();
|
||||
for (const entry of fs.readdirSync(dest, { withFileTypes: true })) {
|
||||
@@ -6763,7 +6809,7 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
|
||||
}
|
||||
|
||||
_removeGsdEntries(dest, kind);
|
||||
_copyStaged(staged, dest, kind);
|
||||
_copyStaged(stagedForCopy, dest, kind);
|
||||
|
||||
// Restore user-owned dirs after the prune+copy
|
||||
for (const [dirName, snap] of toPreserve) {
|
||||
@@ -6773,7 +6819,7 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
|
||||
// For non-skills kinds (commands, agents): no user content to preserve;
|
||||
// just prune stale gsd-* entries and copy new ones.
|
||||
_removeGsdEntries(dest, kind);
|
||||
_copyStaged(staged, dest, kind);
|
||||
_copyStaged(stagedForCopy, dest, kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8955,6 +9001,21 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
||||
} else {
|
||||
failures.push('skills/gsd-*');
|
||||
}
|
||||
// Augment: also verify commands/ (emitted alongside skills/)
|
||||
if (isAugment) {
|
||||
const commandsDir = path.join(targetDir, 'commands');
|
||||
if (fs.existsSync(commandsDir)) {
|
||||
const cmdCount = fs.readdirSync(commandsDir)
|
||||
.filter(f => f.startsWith('gsd-') && f.endsWith('.md')).length;
|
||||
if (cmdCount > 0) {
|
||||
console.log(` ${green}✓${reset} Installed ${cmdCount} commands to commands/`);
|
||||
} else {
|
||||
failures.push('commands/gsd-*');
|
||||
}
|
||||
} else {
|
||||
failures.push('commands/gsd-*');
|
||||
}
|
||||
}
|
||||
|
||||
// Cursor only: also report the commands/ output (#785 — Cursor 1.6 slash commands)
|
||||
if (isCursor) {
|
||||
|
||||
@@ -289,7 +289,7 @@ Qwen Code supports 15 hook events. GSD registers the following events automatica
|
||||
npx @opengsd/gsd-core@latest --augment --global
|
||||
```
|
||||
|
||||
Skills land in `~/.augment/`. GSD installs skills and agents. No hook or statusline ownership.
|
||||
Skills land in `~/.augment/skills/` and slash command definitions land in `~/.augment/commands/`. GSD installs skills, agents, and commands (`/gsd-phase`, `/gsd-ship`, etc.). No hook or statusline ownership.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -323,7 +323,10 @@ function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope:
|
||||
break;
|
||||
|
||||
case 'augment':
|
||||
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToAugmentSkill', 'augment', configDir)];
|
||||
kinds = [
|
||||
commandsKind('commands', 'gsd-', configDir),
|
||||
skillsKind('skills', 'gsd-', 'convertClaudeCommandToAugmentSkill', 'augment', configDir),
|
||||
];
|
||||
break;
|
||||
|
||||
case 'trae':
|
||||
|
||||
177
tests/enh-790-augment-commands.test.cjs
Normal file
177
tests/enh-790-augment-commands.test.cjs
Normal file
@@ -0,0 +1,177 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Regression guard — enh(#790): Augment commands/ emitted alongside skills/.
|
||||
*
|
||||
* Verifies that a global Augment install writes:
|
||||
* - commands/gsd-<stem>.md (slash command definitions)
|
||||
* - skills/gsd-<stem>/SKILL.md (existing skill definitions)
|
||||
*
|
||||
* mcpServers in settings.json is explicitly excluded: gsd ships no MCP server
|
||||
* and registering third-party servers is out of scope for the installer.
|
||||
*
|
||||
* Ref: https://docs.augmentcode.com/cli/reference — ~/.augment/commands/<name>.md
|
||||
*/
|
||||
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const { installRuntimeArtifacts } = require('../bin/install.js');
|
||||
const { resolveRuntimeArtifactLayout } = require('../gsd-core/bin/lib/runtime-artifact-layout.cjs');
|
||||
const { loadSkillsManifest, resolveProfile } = require('../gsd-core/bin/lib/install-profiles.cjs');
|
||||
|
||||
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
|
||||
const MANIFEST = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const RESOLVED_CORE = resolveProfile({ modes: ['core'], manifest: MANIFEST });
|
||||
|
||||
// ─── Layout contract ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('enh-790 — augment layout has commands + skills kinds', () => {
|
||||
test('resolveRuntimeArtifactLayout augment returns 2 kinds', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('augment', '/tmp/fake-augment-dir');
|
||||
assert.strictEqual(layout.kinds.length, 2, 'augment must have exactly 2 artifact kinds');
|
||||
const kindNames = layout.kinds.map(k => k.kind).sort();
|
||||
assert.deepStrictEqual(kindNames, ['commands', 'skills']);
|
||||
});
|
||||
|
||||
test('augment commands kind targets commands/ with gsd- prefix', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('augment', '/tmp/fake-augment-dir');
|
||||
const commandsKind = layout.kinds.find(k => k.kind === 'commands');
|
||||
assert.ok(commandsKind, 'must have commands kind');
|
||||
assert.strictEqual(commandsKind.destSubpath, 'commands');
|
||||
assert.strictEqual(commandsKind.prefix, 'gsd-');
|
||||
});
|
||||
|
||||
test('augment skills kind targets skills/ with gsd- prefix', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('augment', '/tmp/fake-augment-dir');
|
||||
const skillsKind = layout.kinds.find(k => k.kind === 'skills');
|
||||
assert.ok(skillsKind, 'must have skills kind');
|
||||
assert.strictEqual(skillsKind.destSubpath, 'skills');
|
||||
assert.strictEqual(skillsKind.prefix, 'gsd-');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Install contract ────────────────────────────────────────────────────────
|
||||
|
||||
describe('enh-790 — installRuntimeArtifacts augment emits both commands and skills', () => {
|
||||
test('global augment install: commands/gsd-help.md and skills/gsd-help/SKILL.md exist', (t) => {
|
||||
const configDir = createTempDir('gsd-enh790-augment-');
|
||||
t.after(() => cleanup(configDir));
|
||||
|
||||
installRuntimeArtifacts('augment', configDir, 'global', RESOLVED_CORE);
|
||||
|
||||
// Commands dir
|
||||
const commandsDir = path.join(configDir, 'commands');
|
||||
assert.ok(fs.existsSync(commandsDir), 'commands/ dir must exist');
|
||||
const cmdFiles = fs.readdirSync(commandsDir).filter(f => f.startsWith('gsd-') && f.endsWith('.md'));
|
||||
assert.ok(cmdFiles.length > 0, 'at least one gsd-*.md command file must be installed');
|
||||
assert.ok(fs.existsSync(path.join(commandsDir, 'gsd-help.md')), 'commands/gsd-help.md must exist');
|
||||
|
||||
// Skills dir (pre-existing behavior preserved)
|
||||
const skillsDir = path.join(configDir, 'skills');
|
||||
assert.ok(fs.existsSync(skillsDir), 'skills/ dir must exist');
|
||||
assert.ok(fs.existsSync(path.join(skillsDir, 'gsd-help', 'SKILL.md')), 'skills/gsd-help/SKILL.md must exist');
|
||||
});
|
||||
|
||||
test('commands/gsd-help.md has Augment-compatible content (no raw ~/.claude/ refs)', (t) => {
|
||||
const configDir = createTempDir('gsd-enh790-content-');
|
||||
t.after(() => cleanup(configDir));
|
||||
|
||||
installRuntimeArtifacts('augment', configDir, 'global', RESOLVED_CORE);
|
||||
|
||||
const helpCmd = path.join(configDir, 'commands', 'gsd-help.md');
|
||||
assert.ok(fs.existsSync(helpCmd), 'gsd-help.md must exist');
|
||||
const content = fs.readFileSync(helpCmd, 'utf8');
|
||||
// Should not have raw ~/.claude/ references after path rewrite
|
||||
assert.ok(!content.includes('~/.claude/'), 'commands must not contain raw ~/.claude/ refs');
|
||||
});
|
||||
|
||||
test('command count matches skill count (profile parity)', (t) => {
|
||||
const configDir = createTempDir('gsd-enh790-parity-');
|
||||
t.after(() => cleanup(configDir));
|
||||
|
||||
installRuntimeArtifacts('augment', configDir, 'global', RESOLVED_CORE);
|
||||
|
||||
const commandsDir = path.join(configDir, 'commands');
|
||||
const skillsDir = path.join(configDir, 'skills');
|
||||
const cmdCount = fs.readdirSync(commandsDir).filter(f => f.startsWith('gsd-') && f.endsWith('.md')).length;
|
||||
const skillCount = fs.readdirSync(skillsDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length;
|
||||
assert.strictEqual(cmdCount, skillCount, 'command count must equal skill count for same profile');
|
||||
});
|
||||
|
||||
test('full profile install does NOT mutate source commands/gsd/ files', (t) => {
|
||||
// Regression guard: stageSkillsForProfile returns the real source dir on full profile
|
||||
// (skills === '*'). applyRuntimeContentRewritesForCommandsInPlace must copy to temp
|
||||
// before rewriting — it must NEVER write back to the source tree.
|
||||
const { resolveProfile } = require('../gsd-core/bin/lib/install-profiles.cjs');
|
||||
const RESOLVED_FULL = resolveProfile({ modes: ['full'], manifest: MANIFEST });
|
||||
assert.strictEqual(RESOLVED_FULL.skills, '*', 'full profile must have skills === "*"');
|
||||
|
||||
const configDir = createTempDir('gsd-enh790-full-');
|
||||
t.after(() => cleanup(configDir));
|
||||
|
||||
// Record source file content before install
|
||||
const srcHelpPath = path.join(__dirname, '..', 'commands', 'gsd', 'help.md');
|
||||
const srcContentBefore = fs.readFileSync(srcHelpPath, 'utf8');
|
||||
|
||||
installRuntimeArtifacts('augment', configDir, 'global', RESOLVED_FULL);
|
||||
|
||||
// Source file must be identical after install
|
||||
const srcContentAfter = fs.readFileSync(srcHelpPath, 'utf8');
|
||||
assert.strictEqual(srcContentBefore, srcContentAfter,
|
||||
'source commands/gsd/help.md must not be mutated by the install');
|
||||
|
||||
// Installed command file must have rewrites applied (Augment path substitution)
|
||||
const installedHelp = path.join(configDir, 'commands', 'gsd-help.md');
|
||||
assert.ok(fs.existsSync(installedHelp), 'installed gsd-help.md must exist');
|
||||
const installedContent = fs.readFileSync(installedHelp, 'utf8');
|
||||
assert.ok(!installedContent.includes('~/.claude/'), 'installed command must not have raw ~/.claude/ refs');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Uninstall contract ──────────────────────────────────────────────────────
|
||||
|
||||
describe('enh-790 — uninstallRuntimeArtifacts removes augment commands', () => {
|
||||
test('uninstall removes gsd-* commands but preserves user commands', (t) => {
|
||||
const configDir = createTempDir('gsd-enh790-uninstall-');
|
||||
t.after(() => cleanup(configDir));
|
||||
|
||||
const { uninstallRuntimeArtifacts } = require('../bin/install.js');
|
||||
|
||||
// Pre-create: a GSD command + a user-owned command
|
||||
const commandsDir = path.join(configDir, 'commands');
|
||||
fs.mkdirSync(commandsDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(commandsDir, 'gsd-help.md'), '# help\n');
|
||||
fs.writeFileSync(path.join(commandsDir, 'user-custom.md'), '# user\n');
|
||||
|
||||
uninstallRuntimeArtifacts('augment', configDir, 'global');
|
||||
|
||||
assert.ok(!fs.existsSync(path.join(commandsDir, 'gsd-help.md')), 'gsd-help.md must be removed');
|
||||
assert.ok(fs.existsSync(path.join(commandsDir, 'user-custom.md')), 'user-custom.md must be preserved');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── mcpServers exclusion ────────────────────────────────────────────────────
|
||||
|
||||
describe('enh-790 — mcpServers excluded (gsd ships no MCP server)', () => {
|
||||
test('augment install does not write settings.json mcpServers', (t) => {
|
||||
const configDir = createTempDir('gsd-enh790-mcp-excluded-');
|
||||
t.after(() => cleanup(configDir));
|
||||
|
||||
installRuntimeArtifacts('augment', configDir, 'global', RESOLVED_CORE);
|
||||
|
||||
// No settings.json with mcpServers should be written by the layout
|
||||
const settingsPath = path.join(configDir, 'settings.json');
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
assert.ok(!settings.mcpServers, 'settings.json must not contain mcpServers (gsd ships no MCP server)');
|
||||
}
|
||||
// If no settings.json at all, that is also correct
|
||||
});
|
||||
});
|
||||
@@ -145,15 +145,21 @@ describe('resolveRuntimeArtifactLayout — windsurf', () => {
|
||||
});
|
||||
|
||||
describe('resolveRuntimeArtifactLayout — augment', () => {
|
||||
test('returns correct layout for augment', () => {
|
||||
test('returns correct layout for augment (commands + skills)', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('augment', FAKE_DIR);
|
||||
assert.strictEqual(layout.runtime, 'augment');
|
||||
assert.strictEqual(layout.configDir, FAKE_DIR);
|
||||
assert.strictEqual(layout.kinds.length, 1);
|
||||
assert.strictEqual(layout.kinds[0].kind, 'skills');
|
||||
assert.strictEqual(layout.kinds[0].destSubpath, 'skills');
|
||||
assert.strictEqual(layout.kinds.length, 2);
|
||||
// commands kind first
|
||||
assert.strictEqual(layout.kinds[0].kind, 'commands');
|
||||
assert.strictEqual(layout.kinds[0].destSubpath, 'commands');
|
||||
assert.strictEqual(layout.kinds[0].prefix, 'gsd-');
|
||||
assert.strictEqual(typeof layout.kinds[0].stage, 'function');
|
||||
// skills kind second
|
||||
assert.strictEqual(layout.kinds[1].kind, 'skills');
|
||||
assert.strictEqual(layout.kinds[1].destSubpath, 'skills');
|
||||
assert.strictEqual(layout.kinds[1].prefix, 'gsd-');
|
||||
assert.strictEqual(typeof layout.kinds[1].stage, 'function');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user