mirror of https://github.com/garrytan/gstack.git
336 lines
12 KiB
TypeScript
336 lines
12 KiB
TypeScript
#!/usr/bin/env bun
|
|
/**
|
|
* setup-antigravity.ts
|
|
*
|
|
* Automated post-generation setup for the Google Antigravity host.
|
|
*
|
|
* What it does:
|
|
* 1. Resolves the repo-local generated skills directory:
|
|
* <repo>/.antigravity/skills/gstack
|
|
* 2. Ensures the global Antigravity skills parent directory exists:
|
|
* ~/.agents/skills/
|
|
* 3. Creates a platform-appropriate link at ~/.agents/skills/gstack:
|
|
* Windows → NTFS junction (no admin/Developer Mode required)
|
|
* macOS/Linux → directory symlink
|
|
* 4. Is fully idempotent: safe to run repeatedly; skips if already correct.
|
|
* 5. Detects and cleans up broken or stale links before re-creating.
|
|
*
|
|
* Why ~/.agents/skills/ (not ~/.antigravity/skills/):
|
|
* Antigravity v2 Desktop IDE and CLI discover repo-local and global AI skills
|
|
* under ~/.agents/skills/. The .antigravity/ directory is the VS Code app shell,
|
|
* not the skill discovery root. This script bridges the two.
|
|
*
|
|
* Usage:
|
|
* bun run scripts/setup-antigravity.ts
|
|
* bun run setup:antigravity (via package.json)
|
|
*
|
|
* The setup:antigravity script in package.json first runs gen:skill-docs --host
|
|
* antigravity so the source directory always exists before linking.
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
import antigravityConfig from '../hosts/antigravity';
|
|
|
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
|
|
const IS_WINDOWS = process.platform === 'win32';
|
|
|
|
/** Root of the gstack repository (parent of this script's directory). */
|
|
const REPO_ROOT = path.resolve(import.meta.dir, '..');
|
|
|
|
/** Source: the full generated Antigravity skills directory inside the repo.
|
|
* This is the PARENT that contains gstack/, gstack-ship/, gstack-review/, etc.
|
|
* We link this entire directory into ~/.agents/skills/gstack so Antigravity
|
|
* can discover all gstack-* skills in one place.
|
|
*/
|
|
const SOURCE_DIR = path.join(REPO_ROOT, '.antigravity', 'skills');
|
|
|
|
/** Target parent: the global Antigravity/agents skills directory. */
|
|
const TARGET_PARENT = path.join(os.homedir(), '.agents', 'skills');
|
|
|
|
/** Target link path: where Antigravity will discover gstack skills. */
|
|
const TARGET_LINK = path.join(TARGET_PARENT, 'gstack');
|
|
|
|
// ─── Logging helpers ─────────────────────────────────────────────────────────
|
|
|
|
function log(msg: string): void {
|
|
process.stdout.write(msg + '\n');
|
|
}
|
|
|
|
function warn(msg: string): void {
|
|
process.stderr.write('[warn] ' + msg + '\n');
|
|
}
|
|
|
|
function die(msg: string): never {
|
|
process.stderr.write('[error] ' + msg + '\n');
|
|
process.exit(1);
|
|
}
|
|
|
|
// ─── Utility: resolve a link's final real target ────────────────────────────
|
|
|
|
/**
|
|
* Return the real path a symlink/junction points at, or null if the link is
|
|
* broken (target doesn't exist on disk) or the path doesn't exist at all.
|
|
*/
|
|
function readLinkTarget(linkPath: string): string | null {
|
|
try {
|
|
const target = fs.readlinkSync(linkPath);
|
|
// Resolve relative targets against the link's directory
|
|
const resolved = path.isAbsolute(target)
|
|
? target
|
|
: path.resolve(path.dirname(linkPath), target);
|
|
return fs.existsSync(resolved) ? resolved : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return true if linkPath is a symlink or NTFS junction.
|
|
* On Windows, junctions report isSymbolicLink() === true via lstat.
|
|
*/
|
|
function isLink(linkPath: string): boolean {
|
|
try {
|
|
return fs.lstatSync(linkPath).isSymbolicLink();
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return true if linkPath exists as a real directory (not a link).
|
|
*/
|
|
function isRealDir(linkPath: string): boolean {
|
|
try {
|
|
const stat = fs.lstatSync(linkPath);
|
|
return stat.isDirectory() && !stat.isSymbolicLink();
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ─── Step 1: Verify source exists ────────────────────────────────────────────
|
|
|
|
function assertSourceExists(): void {
|
|
if (!fs.existsSync(SOURCE_DIR)) {
|
|
die(
|
|
`Source directory not found: ${SOURCE_DIR}\n` +
|
|
`Run 'bun run gen:skill-docs --host antigravity' first, or use:\n` +
|
|
` bun run setup:antigravity`
|
|
);
|
|
}
|
|
log(`✓ Source: ${SOURCE_DIR}`);
|
|
}
|
|
|
|
// ─── Step 2: Ensure target parent directory exists ───────────────────────────
|
|
|
|
function ensureTargetParent(): void {
|
|
if (!fs.existsSync(TARGET_PARENT)) {
|
|
fs.mkdirSync(TARGET_PARENT, { recursive: true });
|
|
log(`✓ Created: ${TARGET_PARENT}`);
|
|
} else {
|
|
log(`✓ Parent exists: ${TARGET_PARENT}`);
|
|
}
|
|
}
|
|
|
|
// ─── Step 3: Check existing link/directory state ─────────────────────────────
|
|
|
|
type LinkState =
|
|
| { kind: 'none' } // TARGET_LINK does not exist
|
|
| { kind: 'correct'; target: string } // already points at SOURCE_DIR
|
|
| { kind: 'stale'; target: string } // link exists but points elsewhere
|
|
| { kind: 'broken' } // link exists but target is missing
|
|
| { kind: 'real-dir' } // real directory (not a link) — back up
|
|
| { kind: 'real-file' }; // real file (not a link) — back up
|
|
|
|
function inspectTarget(): LinkState {
|
|
if (!fs.existsSync(TARGET_LINK) && !isLink(TARGET_LINK)) {
|
|
return { kind: 'none' };
|
|
}
|
|
|
|
if (isLink(TARGET_LINK)) {
|
|
const resolvedTarget = readLinkTarget(TARGET_LINK);
|
|
if (resolvedTarget === null) {
|
|
return { kind: 'broken' };
|
|
}
|
|
// Normalize both paths for comparison (handles Windows drive-letter case, trailing slashes)
|
|
const normalizedTarget = path.normalize(resolvedTarget);
|
|
const normalizedSource = path.normalize(SOURCE_DIR);
|
|
if (normalizedTarget === normalizedSource) {
|
|
return { kind: 'correct', target: resolvedTarget };
|
|
}
|
|
return { kind: 'stale', target: resolvedTarget };
|
|
}
|
|
|
|
if (isRealDir(TARGET_LINK)) {
|
|
return { kind: 'real-dir' };
|
|
}
|
|
|
|
// Exists but is neither link nor directory (shouldn't happen, but handle gracefully)
|
|
return { kind: 'real-file' };
|
|
}
|
|
|
|
// ─── Step 4: Remove existing stale/broken link or back up real directory ────
|
|
|
|
function removeOrBackup(state: LinkState): void {
|
|
if (state.kind === 'broken' || state.kind === 'stale') {
|
|
log(` Removing ${state.kind} link: ${TARGET_LINK}`);
|
|
if (state.kind === 'stale') {
|
|
log(` (was pointing at: ${(state as { kind: 'stale'; target: string }).target})`);
|
|
}
|
|
// Use unlinkSync for junctions/symlinks (rmSync throws EFAULT on Windows junctions)
|
|
fs.unlinkSync(TARGET_LINK);
|
|
return;
|
|
}
|
|
|
|
if (state.kind === 'real-dir' || state.kind === 'real-file') {
|
|
const backupPath = TARGET_LINK + '.backup-' + Date.now();
|
|
warn(
|
|
`${TARGET_LINK} is a ${state.kind === 'real-dir' ? 'real directory' : 'real file'} (not a link).\n` +
|
|
` Backing it up to: ${backupPath}\n` +
|
|
` If this was intentional, merge it back manually after setup.`
|
|
);
|
|
fs.renameSync(TARGET_LINK, backupPath);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// ─── Step 5: Create the link ─────────────────────────────────────────────────
|
|
|
|
function createLink(): void {
|
|
// Dynamically select link type based on the operating system
|
|
const linkType = IS_WINDOWS ? 'junction' : 'dir';
|
|
|
|
try {
|
|
fs.symlinkSync(SOURCE_DIR, TARGET_LINK, linkType);
|
|
log(`✓ Successfully bridged across ${process.platform} using a '${linkType}' link!`);
|
|
log(` ${TARGET_LINK} → ${SOURCE_DIR}`);
|
|
} catch (err: unknown) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
die(
|
|
`Failed to create ${linkType}.\n` +
|
|
` ${msg}\n\n` +
|
|
`Manual fallback:\n` +
|
|
(IS_WINDOWS
|
|
? ` mklink /J "${TARGET_LINK}" "${SOURCE_DIR}"`
|
|
: ` ln -s "${SOURCE_DIR}" "${TARGET_LINK}"`)
|
|
);
|
|
}
|
|
}
|
|
|
|
// ─── Step 6: Verify the link resolves correctly ──────────────────────────────
|
|
|
|
function verifyLink(): void {
|
|
const state = inspectTarget();
|
|
if (state.kind !== 'correct') {
|
|
die(
|
|
`Link verification failed after creation (state: ${state.kind}).\n` +
|
|
` Expected: ${TARGET_LINK} → ${SOURCE_DIR}\n` +
|
|
` Please report this as a bug in scripts/setup-antigravity.ts.`
|
|
);
|
|
}
|
|
log(`✓ Verified: ${TARGET_LINK} → ${SOURCE_DIR}`);
|
|
}
|
|
|
|
// ─── Step 7: Print discovery hint ───────────────────────────────────────────
|
|
|
|
function printDiscoveryHint(): void {
|
|
log('');
|
|
log('─'.repeat(60));
|
|
log(' Antigravity skill bridge ready.');
|
|
log(` Skills are discoverable at: ${TARGET_LINK}`);
|
|
log('');
|
|
if (IS_WINDOWS) {
|
|
log(' Windows note: an NTFS junction was created. No admin required.');
|
|
log(' To verify: dir /AL "' + path.dirname(TARGET_LINK) + '"');
|
|
} else {
|
|
log(' To verify: ls -la "' + path.dirname(TARGET_LINK) + '"');
|
|
}
|
|
log('');
|
|
log(' To refresh skills after a git pull:');
|
|
log(' bun run setup:antigravity');
|
|
log('─'.repeat(60));
|
|
}
|
|
|
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
|
|
async function main(): Promise<void> {
|
|
log('');
|
|
log('Setting up Antigravity skill bridge...');
|
|
log('');
|
|
|
|
// 1. Verify source
|
|
assertSourceExists();
|
|
|
|
// 1.5. Populate runtime assets
|
|
populateRuntimeAssets();
|
|
|
|
// 2. Ensure ~/.agents/skills/ exists
|
|
ensureTargetParent();
|
|
|
|
// 3. Inspect existing state
|
|
const state = inspectTarget();
|
|
|
|
if (state.kind === 'correct') {
|
|
log(`✓ Already linked correctly: ${TARGET_LINK}`);
|
|
log(` (${IS_WINDOWS ? 'junction' : 'symlink'} → ${SOURCE_DIR})`);
|
|
printDiscoveryHint();
|
|
return; // Idempotent: nothing to do
|
|
}
|
|
|
|
// 4. Remove stale/broken link or back up real directory
|
|
if (state.kind !== 'none') {
|
|
removeOrBackup(state);
|
|
}
|
|
|
|
// 5. Create the link
|
|
createLink();
|
|
|
|
// 6. Verify
|
|
verifyLink();
|
|
|
|
// 7. Done
|
|
printDiscoveryHint();
|
|
}
|
|
|
|
main().catch((err) => die(`Unhandled error: ${err}`));
|
|
|
|
// ─── Step 7: Populate runtime assets ──────────────────────────────────────────
|
|
|
|
function populateRuntimeAssets(): void {
|
|
const runtime = antigravityConfig.runtimeRoot;
|
|
if (!runtime) return;
|
|
|
|
function linkOrCopy(src: string, dst: string) {
|
|
if (!fs.existsSync(src)) return;
|
|
if (fs.existsSync(dst)) return;
|
|
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
|
|
if (isRealDir(src)) {
|
|
fs.symlinkSync(src, dst, IS_WINDOWS ? 'junction' : 'dir');
|
|
} else {
|
|
if (IS_WINDOWS) {
|
|
fs.copyFileSync(src, dst);
|
|
} else {
|
|
fs.symlinkSync(src, dst, 'file');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (runtime.globalSymlinks) {
|
|
for (const asset of runtime.globalSymlinks) {
|
|
linkOrCopy(path.join(REPO_ROOT, asset), path.join(SOURCE_DIR, asset));
|
|
}
|
|
}
|
|
|
|
if (runtime.globalFiles) {
|
|
for (const [dir, files] of Object.entries(runtime.globalFiles)) {
|
|
for (const file of files) {
|
|
linkOrCopy(path.join(REPO_ROOT, dir, file), path.join(SOURCE_DIR, dir, file));
|
|
}
|
|
}
|
|
}
|
|
}
|