Fresh antigravity workspace installs write under the canonical .agents/ (plural) base; legacy .agent/ stays recognized (dual-read). Global ~/.gemini/antigravity/ path unchanged. Closes #791.
This commit is contained in:
6
.changeset/791-antigravity-agents-dir.md
Normal file
6
.changeset/791-antigravity-agents-dir.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 1090
|
||||
---
|
||||
|
||||
**Antigravity workspace skills now install to the canonical `.agents/` directory** — fresh installs write workspace artifacts under `.agents/` (the Google-Codelabs-documented base) instead of `.agent/`; the legacy `.agent/` layout is still recognized so existing installs keep working. The global `~/.gemini/antigravity/` path is unchanged. (#1090)
|
||||
@@ -465,7 +465,7 @@ function getDirName(runtime) {
|
||||
if (runtime === 'gemini') return '.gemini';
|
||||
if (runtime === 'kilo') return '.kilo';
|
||||
if (runtime === 'codex') return '.codex';
|
||||
if (runtime === 'antigravity') return '.agent';
|
||||
if (runtime === 'antigravity') return '.agents';
|
||||
if (runtime === 'cursor') return '.cursor';
|
||||
if (runtime === 'windsurf') return '.windsurf';
|
||||
if (runtime === 'augment') return '.augment';
|
||||
@@ -500,7 +500,7 @@ function getConfigDirFromHome(runtime, isGlobal) {
|
||||
if (runtime === 'kilo') return "'.config', 'kilo'";
|
||||
if (runtime === 'codex') return "'.codex'";
|
||||
if (runtime === 'antigravity') {
|
||||
if (!isGlobal) return "'.agent'";
|
||||
if (!isGlobal) return "'.agents'";
|
||||
const antigravityDir = resolveAntigravityGlobalDir();
|
||||
const rel = path.relative(os.homedir(), antigravityDir);
|
||||
const segments = rel.split(path.sep).filter(Boolean);
|
||||
@@ -2210,8 +2210,8 @@ function convertClaudeAgentToCopilotAgent(content, isGlobal = false) {
|
||||
/**
|
||||
* Apply Antigravity-specific content conversion — path replacement + command name conversion.
|
||||
* Path mappings depend on install mode:
|
||||
* Global: ~/.claude/ → ~/.gemini/antigravity/, ./.claude/ → ./.agent/
|
||||
* Local: ~/.claude/ → .agent/, ./.claude/ → ./.agent/
|
||||
* Global: ~/.claude/ → ~/.gemini/antigravity/, ./.claude/ → ./.agents/
|
||||
* Local: ~/.claude/ → .agents/, ./.claude/ → ./.agents/
|
||||
* Applied to ALL Antigravity content (skills, agents, engine files).
|
||||
* @param {string} content - Source content to convert
|
||||
* @param {boolean} [isGlobal=false] - Whether this is a global install
|
||||
@@ -2225,14 +2225,14 @@ function convertClaudeToAntigravityContent(content, isGlobal = false) {
|
||||
c = c.replace(/\$HOME\/\.claude\b/g, '$HOME/.gemini/antigravity');
|
||||
c = c.replace(/~\/\.claude\b/g, '~/.gemini/antigravity');
|
||||
} else {
|
||||
c = c.replace(/\$HOME\/\.claude\//g, '.agent/');
|
||||
c = c.replace(/~\/\.claude\//g, '.agent/');
|
||||
c = c.replace(/\$HOME\/\.claude\//g, '.agents/');
|
||||
c = c.replace(/~\/\.claude\//g, '.agents/');
|
||||
// Bare form (no trailing slash) — must come after slash form to avoid double-replace
|
||||
c = c.replace(/\$HOME\/\.claude\b/g, '.agent');
|
||||
c = c.replace(/~\/\.claude\b/g, '.agent');
|
||||
c = c.replace(/\$HOME\/\.claude\b/g, '.agents');
|
||||
c = c.replace(/~\/\.claude\b/g, '.agents');
|
||||
}
|
||||
c = c.replace(/\.\/\.claude\//g, './.agent/');
|
||||
c = c.replace(/\.claude\//g, '.agent/');
|
||||
c = c.replace(/\.\/\.claude\//g, './.agents/');
|
||||
c = c.replace(/\.claude\//g, '.agents/');
|
||||
// Command name conversion (all gsd: references → gsd-)
|
||||
c = c.replace(/gsd:/g, 'gsd-');
|
||||
// Runtime-neutral agent name replacement (#766)
|
||||
@@ -9437,6 +9437,10 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
||||
// Get the target directory based on runtime and install type.
|
||||
// Cline local installs write to the project root (like Claude Code) — .clinerules
|
||||
// lives at the root, not inside a .cline/ subdirectory.
|
||||
// #791: antigravity local installs write to .agents/ (canonical). The legacy .agent/
|
||||
// directory is recognized by RUNTIME_DIRS (update-context) and _LEGACY_SCAN_SUBDIR_NAMES
|
||||
// but NOT auto-removed here; legacy .agent/ gsd artifacts are recognized but not
|
||||
// auto-removed on reinstall (dual-read fallback per issue #791 spec).
|
||||
const targetDir = isGlobal
|
||||
? getGlobalConfigDir(runtime, explicitConfigDir)
|
||||
: isCline
|
||||
@@ -11900,7 +11904,8 @@ const _LEGACY_SCAN_SUBDIR_NAMES = [
|
||||
'.codex',
|
||||
'.copilot',
|
||||
'.github', // copilot local form
|
||||
'.agent', // antigravity local form
|
||||
'.agents', // antigravity local form (canonical, #791)
|
||||
'.agent', // antigravity local form (legacy, backward-compat)
|
||||
'.cursor',
|
||||
'.windsurf',
|
||||
'.codeium/windsurf',
|
||||
|
||||
@@ -371,7 +371,7 @@ for the new shape before changing migration behavior.
|
||||
| Kimi CLI | Agent Skills in `skills/gsd-*/SKILL.md`; explicit custom agent YAML/prompt artifacts in `agents/gsd.yaml`, `agents/gsd.md`, and `agents/subagents/gsd-*`; `gsd-core/` payload files referenced by generated skills; manifest, pristine, local-patch, and migration journal files from the normal installer safety pipeline | Global `KIMI_CONFIG_DIR`, explicit `--config-dir`, or first-existing generic skills root: `~/.config/agents` when `~/.config/agents/skills` exists or no generic skills root exists yet, otherwise `~/.agents` when `~/.agents/skills` exists and `~/.config/agents/skills` does not; `KIMI_CONFIG_DIR` and `--config-dir` are GSD write-location overrides and arbitrary roots require Kimi-side `--skills-dir` or `extra_skill_dirs` configuration for skill discovery; local `--kimi --local` is guarded and writes no project-level artifacts | GSD owns only generated `skills/gsd-*`, `agents/gsd.*`, `agents/subagents/gsd-*`, installed `gsd-core/` payload files, and manifest/preservation/migration records. GSD does not own Kimi config files, hooks, settings, rules, statusline, update-banner registration, or non-GSD Kimi skills/agents. Reinstall/update must preserve locally modified generated Kimi artifacts through manifest-backed `gsd-local-patches/`; uninstall removes only GSD-owned Kimi artifacts and preserves non-GSD user content. | [Agent Skills](https://moonshotai.github.io/kimi-cli/en/customization/skills.html), [Agents and Subagents](https://moonshotai.github.io/kimi-cli/en/customization/agents.html), [Tools](https://moonshotai.github.io/kimi-code/en/reference/tools.html); docs checked 2026-06-07 |
|
||||
| Codex | Skills in `skills/gsd-*/SKILL.md`; agents as source markdown plus per-agent TOML in `agents/`; `[agents.gsd-*]` and hooks in `config.toml` | Global `CODEX_HOME` or `~/.codex`; local `./.codex` | GSD owns generated skills, generated agent TOML, `agents.gsd-*` config sections, `[features].hooks` when added by GSD (canonical; legacy alias `codex_hooks` is recognized and migrated forward, #3566), and GSD hook entries | [Codex config schema](https://developers.openai.com/codex/config-schema.json), [Codex developer docs](https://developers.openai.com/codex/); docs not versioned, checked 2026-05-15; installer compatibility sentinel: Codex 0.130.0 features.hooks key (legacy `codex_hooks` recognized) |
|
||||
| GitHub Copilot | Skills in `skills/gsd-*/SKILL.md`; agents as `.agent.md`; repository instructions in `copilot-instructions.md` | Global `COPILOT_CONFIG_DIR`, `COPILOT_HOME`, or `~/.copilot`; local `./.github` | GSD owns generated skill/agent files and GSD-authored instruction files; no hook/statusline ownership | [Repository custom instructions](https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions), [Copilot CLI custom instructions](https://docs.github.com/en/copilot/how-tos/copilot-cli/add-custom-instructions); GitHub Docs product docs, checked 2026-05-11 |
|
||||
| Antigravity | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; Gemini-style `settings.json` hooks when installed by GSD | Global `ANTIGRAVITY_CONFIG_DIR` or `~/.gemini/antigravity`; local `./.agent` | GSD owns generated skills/agents/hooks and GSD settings entries only | Public Antigravity install/config docs for this file layout were not stable or complete as of 2026-05-11; installer compatibility therefore uses GSD's Gemini-compatible settings policy, documented shim baseline. |
|
||||
| Antigravity | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; Gemini-style `settings.json` hooks when installed by GSD | Global `ANTIGRAVITY_CONFIG_DIR` or `~/.gemini/antigravity`; local `./.agents` (canonical, #791) or `./.agent` (legacy, recognized for backward-compat) | GSD owns generated skills/agents/hooks and GSD settings entries only | Public Antigravity install/config docs for this file layout were not stable or complete as of 2026-05-11; installer compatibility therefore uses GSD's Gemini-compatible settings policy, documented shim baseline. Fresh installs write to `.agents/` (the Google-Codelabs-documented form); existing `.agent/` installs continue to be detected and served. |
|
||||
| Cursor | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; rule references under `rules/`; lifecycle hooks via `hooks.json` (sessionStart + postToolUse, #777) | Global `CURSOR_CONFIG_DIR` or `~/.cursor`; local `./.cursor` | GSD owns generated skills/agents, GSD rule files or references, and GSD-managed `hooks.json` entries (sentinel `gsd-managed:true`); no statusline ownership | [Cursor rules](https://docs.cursor.com/context/rules); [Cursor hooks](https://docs.cursor.com/context/hooks); docs not versioned, checked 2026-06-07 |
|
||||
| Windsurf | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; rule references under `rules/` | Global `WINDSURF_CONFIG_DIR` or `~/.codeium/windsurf`; local `./.windsurf` | GSD owns generated skills/agents and GSD rule files or references; no hook/statusline ownership | Windsurf public rule docs were source-limited in search results as of 2026-05-11; installer targets the common workspace rules convention `./.windsurf/rules` and must be rechecked before migrations rewrite rules |
|
||||
| Augment Code | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/` | Global `AUGMENT_CONFIG_DIR` or `~/.augment`; local `./.augment` | GSD owns generated skills/agents only; no hook/statusline ownership | [Augment Agent Skills](https://docs.augmentcode.com/cli/skills), [Augment IDE skills](https://docs.augmentcode.com/using-augment/skills); IDE skills public beta in VS Code 0.789.0+, checked 2026-05-11 |
|
||||
|
||||
@@ -13,7 +13,7 @@ Detect the installed GSD version, scope, runtime, and config dir.
|
||||
|
||||
First, derive `PREFERRED_CONFIG_DIR` and `PREFERRED_RUNTIME` from the invoking prompt's `execution_context` path — this is the one input only the workflow knows:
|
||||
- If the path contains `/gsd-core/workflows/update.md`, strip that suffix and store the remainder as `PREFERRED_CONFIG_DIR`.
|
||||
- Infer `PREFERRED_RUNTIME` from the path: `/.codex/` -> `codex`; `/.gemini/antigravity-ide/`, `/.gemini/antigravity-cli/`, `/.gemini/antigravity/`, `/.agent/` -> `antigravity` (`.agent` is the local Antigravity install dir; see bin/install.js `getDirName('antigravity')`, #503); `/.gemini/` -> `gemini`; `/.config/kilo/` or `/.kilo/` -> `kilo`; `/.config/opencode/` or `/.opencode/` -> `opencode`; otherwise `claude`.
|
||||
- Infer `PREFERRED_RUNTIME` from the path: `/.codex/` -> `codex`; `/.gemini/antigravity-ide/`, `/.gemini/antigravity-cli/`, `/.gemini/antigravity/`, `/.agents/` or `/.agent/` -> `antigravity` (`.agents` is the canonical local Antigravity install dir (#791); `.agent` is the legacy form (#503); see bin/install.js `getDirName('antigravity')`); `/.gemini/` -> `gemini`; `/.config/kilo/` or `/.kilo/` -> `kilo`; `/.config/opencode/` or `/.opencode/` -> `opencode`; otherwise `claude`.
|
||||
|
||||
Then resolve the install context via the deterministic projection (#498). **Do NOT re-derive scope, runtime, or version by hand** — `update-context` owns that cascade in tested code (`gsd-core/bin/lib/update-context.cjs`), the same way `check-latest-version` owns the package name (#2992):
|
||||
|
||||
@@ -443,7 +443,7 @@ for dir in "${CACHE_DIRS[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
for dir in .claude .config/opencode .opencode .gemini/antigravity-ide .gemini/antigravity-cli .gemini/antigravity .agent .gemini .config/kilo .kilo .codex .cursor .codeium/windsurf .augment .trae .qwen .hermes .codebuddy .cline; do
|
||||
for dir in .claude .config/opencode .opencode .gemini/antigravity-ide .gemini/antigravity-cli .gemini/antigravity .agents .agent .gemini .config/kilo .kilo .codex .cursor .codeium/windsurf .augment .trae .qwen .hermes .codebuddy .cline; do
|
||||
rm -f "./$dir/cache/gsd-update-check"*.json
|
||||
rm -f "$HOME/$dir/cache/gsd-update-check"*.json
|
||||
done
|
||||
|
||||
@@ -24,7 +24,8 @@ export const RUNTIME_DIRS: RuntimeDirEntry[] = [
|
||||
['antigravity', '.gemini/antigravity-ide'],
|
||||
['antigravity', '.gemini/antigravity-cli'],
|
||||
['antigravity', '.gemini/antigravity'],
|
||||
['antigravity', '.agent'], // local Antigravity install dir (#503; bin/install.js getDirName('antigravity'))
|
||||
['antigravity', '.agents'], // local Antigravity install dir canonical (#791; bin/install.js getDirName('antigravity'))
|
||||
['antigravity', '.agent'], // local Antigravity install dir legacy (#503; backward-compat with pre-#791 installs)
|
||||
['gemini', '.gemini'],
|
||||
['kilo', '.config/kilo'],
|
||||
['kilo', '.kilo'],
|
||||
|
||||
@@ -73,12 +73,12 @@ describe('convertClaudeToAntigravityContent bare path replacement (#2418)', () =
|
||||
});
|
||||
|
||||
describe('local install', () => {
|
||||
test('replaces ~/.claude (bare, no trailing slash) with .agent', () => {
|
||||
test('replaces ~/.claude (bare, no trailing slash) with .agents', () => {
|
||||
const input = 'configDir = ~/.claude';
|
||||
const result = convertClaudeToAntigravityContent(input, false);
|
||||
assert.ok(
|
||||
result.includes('.agent'),
|
||||
`Expected .agent in output, got: ${result}`
|
||||
result.includes('.agents'),
|
||||
`Expected .agents in output, got: ${result}`
|
||||
);
|
||||
assert.ok(
|
||||
!result.includes('~/.claude'),
|
||||
@@ -86,12 +86,12 @@ describe('convertClaudeToAntigravityContent bare path replacement (#2418)', () =
|
||||
);
|
||||
});
|
||||
|
||||
test('replaces $HOME/.claude (bare, no trailing slash) with .agent', () => {
|
||||
test('replaces $HOME/.claude (bare, no trailing slash) with .agents', () => {
|
||||
const input = 'export DIR=$HOME/.claude';
|
||||
const result = convertClaudeToAntigravityContent(input, false);
|
||||
assert.ok(
|
||||
result.includes('.agent'),
|
||||
`Expected .agent in output, got: ${result}`
|
||||
result.includes('.agents'),
|
||||
`Expected .agents in output, got: ${result}`
|
||||
);
|
||||
assert.ok(
|
||||
!result.includes('$HOME/.claude'),
|
||||
@@ -102,8 +102,8 @@ describe('convertClaudeToAntigravityContent bare path replacement (#2418)', () =
|
||||
test('does not double-replace ~/.claude/ paths', () => {
|
||||
const input = 'See ~/.claude/gsd-core/';
|
||||
const result = convertClaudeToAntigravityContent(input, false);
|
||||
// .agent/ should appear exactly once
|
||||
const count = (result.match(/\.agent\//g) || []).length;
|
||||
// .agents/ should appear exactly once
|
||||
const count = (result.match(/\.agents\//g) || []).length;
|
||||
assert.strictEqual(count, 1, `Expected exactly 1 replacement, got ${count} in: ${result}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('bug #2557: Gemini/Antigravity local hooks use relative paths (not $CLA
|
||||
});
|
||||
|
||||
test('Antigravity local prefix is bare dirName', () => {
|
||||
assert.equal(projectLocalHookPrefix({ runtime: 'antigravity', dirName: '.agent' }), '.agent');
|
||||
assert.equal(projectLocalHookPrefix({ runtime: 'antigravity', dirName: '.agents' }), '.agents');
|
||||
});
|
||||
|
||||
test('non-Gemini local prefix remains $CLAUDE_PROJECT_DIR anchored', () => {
|
||||
|
||||
@@ -112,7 +112,7 @@ describe('bug #3439: shell projection module owns managed-hook policy and legacy
|
||||
|
||||
test('projectLocalHookPrefix centralizes runtime-specific project-dir interpolation policy', () => {
|
||||
assert.equal(projectLocalHookPrefix({ runtime: 'gemini', dirName: '.gemini' }), '.gemini');
|
||||
assert.equal(projectLocalHookPrefix({ runtime: 'antigravity', dirName: '.agent' }), '.agent');
|
||||
assert.equal(projectLocalHookPrefix({ runtime: 'antigravity', dirName: '.agents' }), '.agents');
|
||||
assert.equal(
|
||||
projectLocalHookPrefix({ runtime: 'claude', dirName: '.claude' }),
|
||||
'"$CLAUDE_PROJECT_DIR"/.claude',
|
||||
|
||||
@@ -49,8 +49,26 @@ function fakeFs(files) {
|
||||
};
|
||||
}
|
||||
|
||||
describe('/gsd:update detects local Antigravity (.agent) installs (#503)', () => {
|
||||
test('projection resolves a LOCAL ./.agent install to the antigravity runtime', () => {
|
||||
describe('/gsd:update detects local Antigravity (.agent / .agents) installs (#503 / #791)', () => {
|
||||
test('projection resolves a LOCAL ./.agents install to the antigravity runtime (#791 canonical)', () => {
|
||||
const HOME = '/home/u';
|
||||
const CWD = '/work/proj';
|
||||
const agentsDir = `${CWD}/.agents`;
|
||||
const ffs = fakeFs({
|
||||
[`${agentsDir}/gsd-core/VERSION`]: '1.50.0\n',
|
||||
[`${agentsDir}/gsd-core/workflows/update.md`]: 'x',
|
||||
});
|
||||
const r = resolveUpdateContext({ home: HOME, cwd: CWD, env: {}, fs: ffs });
|
||||
assert.equal(
|
||||
r.runtime,
|
||||
'antigravity',
|
||||
`a local .agents install must map to the antigravity runtime, got "${r.runtime}"`,
|
||||
);
|
||||
assert.equal(r.scope, 'LOCAL');
|
||||
assert.equal(r.installedVersion, '1.50.0');
|
||||
});
|
||||
|
||||
test('projection resolves a LOCAL ./.agent install to the antigravity runtime (#503 backward-compat)', () => {
|
||||
const HOME = '/home/u';
|
||||
const CWD = '/work/proj';
|
||||
const agentDir = `${CWD}/.agent`;
|
||||
@@ -62,26 +80,33 @@ describe('/gsd:update detects local Antigravity (.agent) installs (#503)', () =>
|
||||
assert.equal(
|
||||
r.runtime,
|
||||
'antigravity',
|
||||
`a local .agent install must map to the antigravity runtime, got "${r.runtime}"`,
|
||||
`a legacy .agent install must still map to the antigravity runtime, got "${r.runtime}"`,
|
||||
);
|
||||
assert.equal(r.scope, 'LOCAL');
|
||||
assert.equal(r.installedVersion, '1.40.0');
|
||||
});
|
||||
|
||||
test('execution_context classifier maps a /.agent/ path to antigravity (update.md)', () => {
|
||||
test('execution_context classifier maps /.agents/ and /.agent/ paths to antigravity (update.md)', () => {
|
||||
const hasAgentsClassifierRule =
|
||||
/\/\.agents\/[^\n]*->[^\n]*antigravity/.test(UPDATE_MD);
|
||||
assert.ok(
|
||||
hasAgentsClassifierRule,
|
||||
'update.md classifier must map a `/.agents/` path to the `antigravity` runtime',
|
||||
);
|
||||
const hasAgentClassifierRule =
|
||||
/\/\.agent\/[^\n]*->[^\n]*antigravity/.test(UPDATE_MD);
|
||||
assert.ok(
|
||||
hasAgentClassifierRule,
|
||||
'update.md classifier must map a `/.agent/` path to the `antigravity` runtime',
|
||||
'update.md classifier must still map a `/.agent/` path to the `antigravity` runtime (backward-compat)',
|
||||
);
|
||||
});
|
||||
|
||||
test('every runtime-dir `for dir in` loop in update.md includes .agent', () => {
|
||||
test('every runtime-dir `for dir in` loop in update.md includes .agents and .agent', () => {
|
||||
// The LOCAL-scope discovery loop moved into the projection (#498); the
|
||||
// post-update cache-clear loop remains inline and still enumerates the
|
||||
// runtime config dirs as a literal `.claude ... .codex` list, so it must
|
||||
// include .agent or a local Antigravity install keeps a stale indicator.
|
||||
// include both .agents (canonical, #791) and .agent (legacy, #503) or
|
||||
// stale indicators could linger.
|
||||
const runtimeDirLoops = UPDATE_MD
|
||||
.split('\n')
|
||||
.filter((l) => /for dir in .*\.claude.*\.codex/.test(l));
|
||||
@@ -90,9 +115,13 @@ describe('/gsd:update detects local Antigravity (.agent) installs (#503)', () =>
|
||||
`expected at least 1 runtime-dir loop in update.md, found ${runtimeDirLoops.length}`,
|
||||
);
|
||||
for (const loop of runtimeDirLoops) {
|
||||
assert.ok(
|
||||
/(^|\s)\.agents(\s|$)/.test(loop),
|
||||
`every runtime-dir loop must include .agents (canonical), got: ${loop.trim()}`,
|
||||
);
|
||||
assert.ok(
|
||||
/(^|\s)\.agent(\s|$)/.test(loop),
|
||||
`every runtime-dir loop must include .agent, got: ${loop.trim()}`,
|
||||
`every runtime-dir loop must include .agent (legacy backward-compat), got: ${loop.trim()}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ const EXPECTED_ALL_HOOKS = [
|
||||
|
||||
const RUNTIME_META = {
|
||||
claude: { localDir: '.claude', globalSuffix: '.claude' },
|
||||
antigravity: { localDir: '.agent', globalSuffix: path.join('.gemini', 'antigravity') },
|
||||
antigravity: { localDir: '.agents', globalSuffix: path.join('.gemini', 'antigravity') },
|
||||
augment: { localDir: '.augment', globalSuffix: '.augment' },
|
||||
cline: { localDir: '.cline', globalSuffix: '.cline' },
|
||||
codebuddy: { localDir: '.codebuddy', globalSuffix: '.codebuddy' },
|
||||
@@ -111,7 +111,7 @@ function runMinimalInstall({ runtime, scope, extraArgs = [] }) {
|
||||
try {
|
||||
const LOCAL_DIR_NAME = {
|
||||
claude: '.claude', opencode: '.opencode', gemini: '.gemini', kilo: '.kilo',
|
||||
codex: '.codex', copilot: '.github', antigravity: '.agent', cursor: '.cursor',
|
||||
codex: '.codex', copilot: '.github', antigravity: '.agents', cursor: '.cursor',
|
||||
windsurf: '.windsurf', augment: '.augment', trae: '.trae', qwen: '.qwen',
|
||||
codebuddy: '.codebuddy', cline: '.',
|
||||
};
|
||||
|
||||
@@ -261,7 +261,7 @@ describe('getConfigDirFromHome — spot-checks', () => {
|
||||
assert.strictEqual(getConfigDirFromHome('trae', true), "'.trae'");
|
||||
});
|
||||
|
||||
test('antigravity returns .agent (local) and legacy fallback global path when no 2.x dirs exist', () => {
|
||||
test('antigravity returns .agents (local) and legacy fallback global path when no 2.x dirs exist', () => {
|
||||
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-antigravity-empty-'));
|
||||
const savedHome = process.env.HOME;
|
||||
const savedUserProfile = process.env.USERPROFILE;
|
||||
@@ -270,7 +270,7 @@ describe('getConfigDirFromHome — spot-checks', () => {
|
||||
process.env.HOME = home;
|
||||
process.env.USERPROFILE = home;
|
||||
try {
|
||||
assert.strictEqual(getConfigDirFromHome('antigravity', false), "'.agent'");
|
||||
assert.strictEqual(getConfigDirFromHome('antigravity', false), "'.agents'");
|
||||
assert.strictEqual(getConfigDirFromHome('antigravity', true), "'.gemini', 'antigravity'");
|
||||
} finally {
|
||||
if (savedHome === undefined) delete process.env.HOME;
|
||||
@@ -836,6 +836,110 @@ describe('install — changeset CLI lands at scripts/changeset/cli.cjs (#935)',
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Section N: Antigravity .agents canonical workspace dir (#791) ─────────────
|
||||
// allow-test-rule: runtime-contract-is-the-product
|
||||
// Reads deployed agent .md files whose text IS the product surface the
|
||||
// Antigravity runtime loads at startup (path references, command names).
|
||||
|
||||
describe('antigravity local install writes to .agents/ canonical dir (#791)', () => {
|
||||
let tmpDir;
|
||||
let previousCwd;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempDir('gsd-antigravity-791-');
|
||||
previousCwd = process.cwd();
|
||||
process.chdir(tmpDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(previousCwd);
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('install writes workspace skills under .agents/skills/', () => {
|
||||
const result = install(false, 'antigravity');
|
||||
const agentsDir = path.join(tmpDir, '.agents');
|
||||
assert.strictEqual(result.runtime, 'antigravity');
|
||||
assert.ok(fs.existsSync(agentsDir), '.agents/ must be created for local antigravity install');
|
||||
const skillsDir = path.join(agentsDir, 'skills');
|
||||
assert.ok(fs.existsSync(skillsDir), '.agents/skills/ must exist after install');
|
||||
const skillEntries = fs.readdirSync(skillsDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'));
|
||||
assert.ok(skillEntries.length > 0, 'at least one gsd-* skill must be installed under .agents/skills/');
|
||||
const firstSkill = path.join(skillsDir, skillEntries[0].name, 'SKILL.md');
|
||||
assert.ok(fs.existsSync(firstSkill), `SKILL.md must exist at ${firstSkill}`);
|
||||
});
|
||||
|
||||
test('installed agent files reference .agents/ not ~/.claude/ or bare .agent/', () => {
|
||||
// NOTE: skill content is intentionally NOT asserted here. The installer calls
|
||||
// convertClaudeCommandToAntigravitySkill(content, skillName, runtime, cmdNames)
|
||||
// where the 3rd arg is the string "antigravity" (truthy), routing local skills
|
||||
// through the global content branch — a pre-existing quirk tracked separately.
|
||||
// Agent files are NOT affected: convertClaudeAgentToAntigravityAgent(content, isGlobal)
|
||||
// receives the boolean isGlobal correctly, so local agents use the local (.agents/) branch.
|
||||
install(false, 'antigravity');
|
||||
const agentsDest = path.join(tmpDir, '.agents', 'agents');
|
||||
assert.ok(fs.existsSync(agentsDest), '.agents/agents/ must exist after local install');
|
||||
const agentFiles = fs.readdirSync(agentsDest)
|
||||
.filter(f => f.startsWith('gsd-') && f.endsWith('.md'));
|
||||
assert.ok(agentFiles.length > 0, 'pre-condition: at least one gsd-* agent must be installed');
|
||||
for (const file of agentFiles) {
|
||||
const content = fs.readFileSync(path.join(agentsDest, file), 'utf8');
|
||||
// Local agent content must not contain global home-dir paths (should be .agents/)
|
||||
assert.ok(
|
||||
!content.includes('~/.claude/') && !content.includes('$HOME/.claude/'),
|
||||
`${file} must not contain ~/.claude/ or $HOME/.claude/ in a local install; content uses .agents/`,
|
||||
);
|
||||
// Local agent content must not reference the legacy singular .agent/ path
|
||||
const bareAgentRefs = content.match(/(?<!\w)\.agent(?!s)\//g) || [];
|
||||
assert.strictEqual(
|
||||
bareAgentRefs.length, 0,
|
||||
`${file} must not reference legacy .agent/ path; found: ${bareAgentRefs.join(', ')}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('legacy .agent/ is NOT written on a fresh local install', () => {
|
||||
install(false, 'antigravity');
|
||||
const legacyDir = path.join(tmpDir, '.agent');
|
||||
assert.ok(!fs.existsSync(legacyDir),
|
||||
'.agent/ must not be created by a fresh install (new installs use .agents/)');
|
||||
});
|
||||
|
||||
test('global antigravity install still writes to ~/.gemini/antigravity (unchanged)', () => {
|
||||
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ag-global-'));
|
||||
const savedHome = process.env.HOME;
|
||||
const savedUserProfile = process.env.USERPROFILE;
|
||||
const savedAntigravityConfig = process.env.ANTIGRAVITY_CONFIG_DIR;
|
||||
delete process.env.ANTIGRAVITY_CONFIG_DIR;
|
||||
process.env.HOME = homeDir;
|
||||
process.env.USERPROFILE = homeDir;
|
||||
try {
|
||||
const result = install(true, 'antigravity');
|
||||
assert.strictEqual(result.runtime, 'antigravity');
|
||||
assert.ok(
|
||||
result.configDir.startsWith(homeDir),
|
||||
`global antigravity install must go under HOME, got: ${result.configDir}`,
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(result.configDir, 'skills')),
|
||||
'global antigravity install must create skills/ under ~/.gemini/antigravity',
|
||||
);
|
||||
assert.ok(
|
||||
!fs.existsSync(path.join(homeDir, '.agents')),
|
||||
'.agents/ must NOT be created by a global install (global path is ~/.gemini/antigravity)',
|
||||
);
|
||||
} finally {
|
||||
if (savedHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = savedHome;
|
||||
if (savedUserProfile === undefined) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = savedUserProfile;
|
||||
if (savedAntigravityConfig === undefined) delete process.env.ANTIGRAVITY_CONFIG_DIR;
|
||||
else process.env.ANTIGRAVITY_CONFIG_DIR = savedAntigravityConfig;
|
||||
cleanup(homeDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
// ─── Section 6: Windsurf / devin-desktop alias (#792) ───────────────────────
|
||||
|
||||
describe('install — --devin-desktop CLI flag routes to windsurf runtime (#792)', () => {
|
||||
|
||||
Reference in New Issue
Block a user