fix(#813): apply per-runtime skill path rewrites in applySurface (#817)

* fix(#813): apply per-runtime skill path rewrites in applySurface

applySurface() re-staged skill artifacts but, unlike installRuntimeArtifacts(),
never applied the per-runtime path rewrites. So /gsd:surface
(profile/enable/disable/reset) overwrote installed SKILL.md bodies with the
converter's default ~/.claude paths instead of the install target (pathPrefix),
silently regressing skill path references for every skillsKind runtime until
the next reinstall.

applySurface now mirrors installRuntimeArtifacts: for kind.kind === 'skills' it
derives pathPrefix the same way and applies applyRuntimeContentRewritesInPlace
on the staged dir before syncing.

- bin/install.js: export applyRuntimeContentRewritesInPlace
- runtime-artifact-layout.cts: carry resolved scope on Layout; export
  getInstallExports; type computePathPrefix/applyRuntimeContentRewritesInPlace
  on InstallExports
- surface.cts: lazily derive pathPrefix (only when a skills kind exists) and
  apply the rewrite via the shared getInstallExports accessor — single source of
  truth with install, only skills kinds rewritten (matches install)
- tests: regression test parameterized over cursor + codex asserting
  post-applySurface bodies carry the install pathPrefix, not ~/.claude
- CONTEXT.md: glossary updated for the applySurface rewrite parity + scope seam

Closes #813

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#813): add changeset fragment for PR #817

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#813): normalize configDir prefix to forward slashes for Windows CI

The #813 regression assertion compared skill bodies against a raw
${configDir}/ prefix, but production derives pathPrefix via
path.resolve(configDir).replace(/\\/g, '/'). On Windows, mkdtempSync
returns backslash paths while the rewritten body uses forward slashes,
so the assertion would fail Windows-only (not covered by local gsd-test).
Normalize the expected prefix the same way production does.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-07 16:26:14 -04:00
committed by GitHub
parent ea0d8f09b1
commit 907cd01aa3
6 changed files with 119 additions and 4 deletions

View File

@@ -33,6 +33,8 @@ const _require: NodeRequire = require;
interface InstallExports {
readGsdCommandNames: () => string[];
computePathPrefix: (opts: { isGlobal: boolean; isOpencode: boolean; isWindowsHost: boolean; resolvedTarget: string; homeDir: string }) => string;
applyRuntimeContentRewritesInPlace: (stagedDir: string, runtime: string, pathPrefix: string) => void;
[converterName: string]: unknown;
}
@@ -84,6 +86,7 @@ interface ArtifactKind {
interface Layout {
runtime: string;
configDir: string;
scope?: 'local' | 'global';
kinds: ArtifactKind[];
}
@@ -371,7 +374,7 @@ function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope:
throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`);
}
return { runtime, configDir, kinds };
return { runtime, configDir, scope, kinds };
}
export = { resolveRuntimeArtifactLayout, findInstallSourceRoot };
export = { resolveRuntimeArtifactLayout, findInstallSourceRoot, getInstallExports };

View File

@@ -23,6 +23,7 @@
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { platformWriteSync } from './shell-command-projection.cjs';
// eslint-disable-next-line @typescript-eslint/no-require-imports
import installProfiles = require('./install-profiles.cjs');
@@ -35,7 +36,7 @@ import { CLUSTERS } from './clusters.cjs';
import type { ClusterMap } from './clusters.cjs';
// eslint-disable-next-line @typescript-eslint/no-require-imports
import runtimeArtifactLayout = require('./runtime-artifact-layout.cjs');
const { findInstallSourceRoot } = runtimeArtifactLayout;
const { findInstallSourceRoot, getInstallExports } = runtimeArtifactLayout;
const SURFACE_FILE_NAME = '.gsd-surface.json';
@@ -53,6 +54,7 @@ interface ArtifactKind {
interface Layout {
runtime: string;
configDir: string;
scope?: 'local' | 'global';
kinds: ArtifactKind[];
}
@@ -256,8 +258,29 @@ function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map<st
}
const skillManifest = normalizeSkillManifest(layout.configDir, manifest);
const resolved = resolveSurface(layout.configDir, skillManifest, clusterMap);
// Mirror installRuntimeArtifacts: skills kinds get per-runtime path rewrites
// so SKILL.md bodies reference the install target (pathPrefix), not the
// converter's default ~/.claude paths (#813). Computed lazily so command-only
// runtimes do not trigger the install.js require.
let pathPrefix: string | null = null;
for (const kind of layout.kinds) {
const staged = kind.stage(resolved);
if (kind.kind === 'skills') {
const installExports = getInstallExports();
if (pathPrefix === null) {
const scope = layout.scope ?? 'global';
const resolvedTarget = path.resolve(layout.configDir).replace(/\\/g, '/');
const homeDir = os.homedir().replace(/\\/g, '/');
pathPrefix = installExports.computePathPrefix({
isGlobal: scope === 'global',
isOpencode: layout.runtime === 'opencode',
isWindowsHost: process.platform === 'win32',
resolvedTarget,
homeDir,
});
}
installExports.applyRuntimeContentRewritesInPlace(staged, layout.runtime, pathPrefix);
}
const dest = path.join(layout.configDir, kind.destSubpath);
_syncGsdDir(staged, dest, kind, skillManifest);
}