From 9ed5c12efe6e169e630c61b34861876c8b9ed5ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 01:45:38 +0000 Subject: [PATCH] Fix read-only file permissions after cpSync from Nix store Co-authored-by: faldor20 <26968035+faldor20@users.noreply.github.com> --- src/resource-loader.ts | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/resource-loader.ts b/src/resource-loader.ts index c5e02c29b..55325b45b 100644 --- a/src/resource-loader.ts +++ b/src/resource-loader.ts @@ -1,6 +1,6 @@ import { DefaultResourceLoader } from '@gsd/pi-coding-agent' import { homedir } from 'node:os' -import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs' +import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs' import { dirname, join, relative, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { compareSemver } from './update-check.js' @@ -133,6 +133,26 @@ export function getNewerManagedResourceVersion(agentDir: string, currentVersion: return compareSemver(managedVersion, currentVersion) > 0 ? managedVersion : null } +/** + * Recursively makes all files and directories under dirPath owner-writable. + * + * Files copied from the Nix store inherit read-only modes (0444/0555). + * Calling this before cpSync prevents overwrite failures on subsequent upgrades, + * and calling it after ensures the next run can overwrite the copies too. + */ +function makeTreeWritable(dirPath: string): void { + if (!existsSync(dirPath)) return + chmodSync(dirPath, 0o755) + for (const entry of readdirSync(dirPath, { withFileTypes: true })) { + const entryPath = join(dirPath, entry.name) + if (entry.isDirectory()) { + makeTreeWritable(entryPath) + } else { + chmodSync(entryPath, 0o644) + } + } +} + /** * Syncs all bundled resources to agentDir (~/.gsd/agent/) on every launch. * @@ -155,6 +175,7 @@ export function initResources(agentDir: string): void { // then overwrite so updates land on next launch. Only bundled subdirs are removed; // user-created extension directories are preserved. const destExtensions = join(agentDir, 'extensions') + makeTreeWritable(destExtensions) for (const entry of readdirSync(bundledExtensionsDir, { withFileTypes: true })) { if (entry.isDirectory()) { const target = join(destExtensions, entry.name) @@ -162,11 +183,13 @@ export function initResources(agentDir: string): void { } } cpSync(bundledExtensionsDir, destExtensions, { recursive: true, force: true }) + makeTreeWritable(destExtensions) // Sync agents const destAgents = join(agentDir, 'agents') const srcAgents = join(resourcesDir, 'agents') if (existsSync(srcAgents)) { + makeTreeWritable(destAgents) for (const entry of readdirSync(srcAgents, { withFileTypes: true })) { if (entry.isDirectory()) { const target = join(destAgents, entry.name) @@ -174,12 +197,14 @@ export function initResources(agentDir: string): void { } } cpSync(srcAgents, destAgents, { recursive: true, force: true }) + makeTreeWritable(destAgents) } // Sync skills const destSkills = join(agentDir, 'skills') const srcSkills = join(resourcesDir, 'skills') if (existsSync(srcSkills)) { + makeTreeWritable(destSkills) for (const entry of readdirSync(srcSkills, { withFileTypes: true })) { if (entry.isDirectory()) { const target = join(destSkills, entry.name) @@ -187,6 +212,7 @@ export function initResources(agentDir: string): void { } } cpSync(srcSkills, destSkills, { recursive: true, force: true }) + makeTreeWritable(destSkills) } writeManagedResourceManifest(agentDir)