Repository navigation
Expand file tree
/
Copy pathpaths.go
More file actions
373 lines (332 loc) · 13.1 KB
/
Copy pathpaths.go
File metadata and controls
373 lines (332 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
package codingagent
import (
"io"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/MichaelKinsy/PiG/internal/linkerexec"
"github.com/MichaelKinsy/PiG/internal/nodepath"
"github.com/MichaelKinsy/PiG/internal/resolvepath"
)
// AppName is the binary/CLI name, independent of the selected configuration directories.
const AppName = "pig"
// PackageName is PiG's npm package: the launcher that a global npm-family
// install owns and that the signed update manifest names. Mirrors upstream
// PACKAGE_NAME export. automation/release/pig_package.py reads this line, so keep
// the declaration on one line.
const PackageName = "@pi-in-go/pig"
// CONFIG_DIR_NAME is the default per-project config directory name.
const CONFIG_DIR_NAME = "." + AppName
// UsePiDirs reports whether the user explicitly selected Pi's configuration directories.
func UsePiDirs() bool {
// pig divergence (D2): sharing Pi state requires an explicit process-level opt-in.
return os.Getenv("PIG_USE_PI_DIRS") == "1"
}
// ConfigDirName returns the selected per-project configuration directory name.
func ConfigDirName() string {
if UsePiDirs() {
return ".pi"
}
return CONFIG_DIR_NAME
}
// ENV_AGENT_DIR overrides the config directory.
// Mirrors upstream ENV_AGENT_DIR export.
const ENV_AGENT_DIR = "PIG_CODING_AGENT_DIR"
// ENV_SESSION_DIR overrides the session storage directory.
// Mirrors upstream ENV_SESSION_DIR export.
const ENV_SESSION_DIR = "PIG_CODING_AGENT_SESSION_DIR"
// pig divergence (D2): title says "PiG", not "pi": separate binary and config root.
const APP_TITLE = "PiG"
// AgentDir returns the writable agent directory. Shared mode uses Pi's environment override; otherwise it uses PiG's. Both expand a leading tilde.
func AgentDir() string {
if configured := os.Getenv(agentDirEnvName()); configured != "" {
return ExpandTildePath(configured)
}
return DefaultAgentDir()
}
// AgentDirConfigured reports whether the agent directory comes from its environment variable rather than the default.
func AgentDirConfigured() bool { return os.Getenv(agentDirEnvName()) != "" }
func agentDirEnvName() string {
if UsePiDirs() {
return "PI_CODING_AGENT_DIR"
}
return ENV_AGENT_DIR
}
// ProjectConfigDir returns the selected workspace-local configuration root.
func ProjectConfigDir(cwd string) string {
return filepath.Join(cwd, ConfigDirName())
}
// NPMInstallRoot returns the managed npm project for one settings scope.
func NPMInstallRoot(cwd, agentDir string, project bool) string {
if project {
return filepath.Join(ProjectConfigDir(cwd), "npm")
}
return filepath.Join(agentDir, "npm")
}
// GitInstallRoot returns the managed Git checkout root for one settings scope.
func GitInstallRoot(cwd, agentDir string, project bool) string {
if project {
return filepath.Join(ProjectConfigDir(cwd), "git")
}
return filepath.Join(agentDir, "git")
}
// CatalogRoot returns the managed catalog materialization root.
func CatalogRoot(agentDir string) string {
return filepath.Join(agentDir, "catalog")
}
// StateDir returns one additive capability's user-scoped state directory.
// Namespaces are code-owned identifiers, not user-provided paths.
func StateDir(namespace string) string {
if namespace == "" || namespace == "." || namespace == ".." || filepath.Clean(namespace) != namespace || strings.ContainsAny(namespace, `/\\`) {
panic("invalid Pig state namespace: " + namespace)
}
return filepath.Join(ConfigRoot(), "state", namespace)
}
// ProjectStateDir returns one additive capability's workspace-scoped state directory.
func ProjectStateDir(cwd, namespace string) string {
if namespace == "" || namespace == "." || namespace == ".." || filepath.Clean(namespace) != namespace || strings.ContainsAny(namespace, `/\\`) {
panic("invalid Pig state namespace: " + namespace)
}
return filepath.Join(ProjectConfigDir(cwd), "state", namespace)
}
// PigletArtifactsDir returns the managed Piglet artifact store.
func PigletArtifactsDir() string {
return filepath.Join(ConfigRoot(), "artifacts", "piglets")
}
// PigletRecordsDir returns the managed Piglet record store. Records live
// outside Piglet discovery so they never masquerade as editable source.
func PigletRecordsDir() string {
return filepath.Join(ConfigRoot(), "receipts", "piglets")
}
// PigletsDir returns the user-owned Piglet source directory.
func PigletsDir() string {
return filepath.Join(ConfigRoot(), "piglets")
}
// SelfUpdateCommand describes the command a proven package-manager installation runs to update itself.
//
// Mirrors upstream SelfUpdateCommand (config.ts). Steps holds an optional
// sequence of sub-commands (e.g. uninstall old name, then install new name).
type SelfUpdateCommand struct {
Command string
Args []string
Display string
// Steps holds an optional ordered list of commands to run in sequence.
// Mirrors upstream SelfUpdateCommand.steps (config.ts v0.73.1).
Steps []*SelfUpdateCommand
}
func makeSelfUpdateCommandStep(command string, args []string) *SelfUpdateCommand {
var display strings.Builder
for i, arg := range append([]string{command}, args...) {
if i > 0 {
display.WriteByte(' ')
}
if strings.ContainsFunc(arg, isJSWhitespace) {
display.WriteString(`"` + arg + `"`)
continue
}
display.WriteString(arg)
}
return &SelfUpdateCommand{Command: command, Args: args, Display: display.String()}
}
func makeSelfUpdateCommand(installStep, uninstallStep *SelfUpdateCommand) *SelfUpdateCommand {
if uninstallStep == nil {
return installStep
}
return &SelfUpdateCommand{
Command: installStep.Command,
Args: installStep.Args,
Display: uninstallStep.Display + " && " + installStep.Display,
Steps: []*SelfUpdateCommand{uninstallStep, installStep},
}
}
// SelfUpdatePackageTarget is the package a self-update installs: its name, and
// the spec the package manager installs, which is the name when empty.
// Mirrors upstream SelfUpdatePackageTarget (config.ts).
type SelfUpdatePackageTarget struct {
PackageName string
InstallSpec string
}
func normalizeSelfUpdatePackageTarget(target SelfUpdatePackageTarget) SelfUpdatePackageTarget {
if target.InstallSpec == "" {
target.InstallSpec = target.PackageName
}
return target
}
// packageManagerSelfUpdateCommand mirrors upstream getSelfUpdateCommandForMethod
// command construction, including --ignore-scripts and package-manager release
// age controls. It uninstalls installedPackageName first only when target
// renames the package.
func packageManagerSelfUpdateCommand(owner PackageManagerOwner, installedPackageName string, npmCommand []string, target SelfUpdatePackageTarget) *SelfUpdateCommand {
if installedPackageName == "" {
return nil
}
if target.PackageName == "" {
target.PackageName = installedPackageName
}
target = normalizeSelfUpdatePackageTarget(target)
command := "npm"
var baseArgs []string
if len(npmCommand) > 0 {
command = npmCommand[0]
baseArgs = append(baseArgs, npmCommand[1:]...)
}
switch owner {
case ownerPNPM:
return makeSelfUpdateCommand(
makeSelfUpdateCommandStep("pnpm", []string{"install", "-g", "--ignore-scripts", "--config.minimumReleaseAge=0", target.InstallSpec}),
selfUpdateUninstallStep("pnpm", []string{"remove", "-g"}, installedPackageName, target.PackageName),
)
case ownerYarn:
return makeSelfUpdateCommand(
makeSelfUpdateCommandStep("yarn", []string{"global", "add", "--ignore-scripts", target.InstallSpec}),
selfUpdateUninstallStep("yarn", []string{"global", "remove"}, installedPackageName, target.PackageName),
)
case ownerBun:
return makeSelfUpdateCommand(
makeSelfUpdateCommandStep("bun", []string{"install", "-g", "--ignore-scripts", "--minimum-release-age=0", target.InstallSpec}),
selfUpdateUninstallStep("bun", []string{"uninstall", "-g"}, installedPackageName, target.PackageName),
)
default:
installArgs := append(append([]string{}, baseArgs...), "install", "-g", "--ignore-scripts", "--min-release-age=0", target.InstallSpec)
uninstallPrefix := append([]string{}, baseArgs...)
return makeSelfUpdateCommand(
makeSelfUpdateCommandStep(command, installArgs),
selfUpdateUninstallStep(command, append(uninstallPrefix, "uninstall", "-g"), installedPackageName, target.PackageName),
)
}
}
func selfUpdateUninstallStep(command string, argsPrefix []string, installedPackageName, updatePackageName string) *SelfUpdateCommand {
if updatePackageName == installedPackageName {
return nil
}
args := append(append([]string{}, argsPrefix...), installedPackageName)
return makeSelfUpdateCommandStep(command, args)
}
// ─── Config Root ──────────────────────────────────────────────────────────────
// ConfigRoot returns the pig configuration root directory.
//
// Resolution order:
// 1. $PIG_HOME if set and non-empty
// 2. $XDG_CONFIG_HOME/pig if XDG_CONFIG_HOME is set
// 3. ~/.pig (default)
//
// PiG-owned state stays here even when the agent and project directories are shared with Pi.
func ConfigRoot() string {
if v := os.Getenv("PIG_HOME"); v != "" {
return ExpandTildePath(v)
}
if v := os.Getenv("XDG_CONFIG_HOME"); v != "" {
return filepath.Join(ExpandTildePath(v), "pig")
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".pig")
}
// CanonicalizePath resolves a nonempty path to its absolute canonical filesystem form, following symlinks and drive junctions while preserving Windows volume mount points as directories. It preserves the raw input if resolution fails, including an empty or missing path.
// Mirrors upstream canonicalizePath.
func CanonicalizePath(path string) string {
if path == "" {
return path
}
absolute, err := nodepath.Resolve(path)
if err != nil {
return path
}
canonical, err := evalCanonicalPath(absolute)
if err != nil {
return path
}
return canonical
}
// IsLocalPath reports whether value is a local path rather than a package source, a built-in extension or a remote URL. Bare names, relative paths, and file URLs are local.
// Ports .upstream/v0.99.1/packages/coding-agent/src/utils/paths.ts:45-64 (a `builtin:` value names a built-in extension).
func IsLocalPath(value string) bool {
trimmed := jsTrim(value)
for _, prefix := range []string{"npm:", "git:", "github:", "http:", "https:", "ssh:", BuiltinPathPrefix} {
if strings.HasPrefix(trimmed, prefix) {
return false
}
}
return true
}
// ResolvePath normalizes input and baseDir, then resolves the input to an absolute path. An empty baseDir uses the process working directory. Invalid file URLs return an error.
// Ports packages/coding-agent/src/utils/paths.ts:102-106.
func ResolvePath(input, baseDir string) (string, error) {
return resolvepath.Resolve(input, baseDir)
}
// ExpandTildePath expands a leading ~ in a filesystem path.
// Mirrors upstream expandTildePath.
func ExpandTildePath(path string) string {
if path == "~" {
home, _ := os.UserHomeDir()
return home
}
if rest, ok := strings.CutPrefix(path, "~/"); ok {
home, _ := os.UserHomeDir()
return filepath.Join(home, rest)
}
return path
}
// resolveAgainstCwd is Pi's resolvePath(filePath, cwd) without input normalization. On Windows Node's isAbsolute accepts a rooted path without a drive (/x or \x), which then resolves on the process's drive, not cwd's.
func resolveAgainstCwd(filePath, cwd string) (string, error) {
if nodepath.IsAbsolute(filePath) {
return nodepath.Resolve(filePath)
}
return nodepath.Resolve(cwd, filePath)
}
// GetCwdRelativePath returns the path relative to cwd, with the platform
// separator as Pi's path.relative gives it, when filePath resolves inside
// cwd, or "" when it is outside cwd.
func GetCwdRelativePath(filePath, cwd string) string {
resolvedCwd, err := nodepath.Resolve(cwd)
if err != nil {
return ""
}
resolvedPath, err := resolveAgainstCwd(filePath, resolvedCwd)
if err != nil {
return ""
}
relativePath, err := filepath.Rel(resolvedCwd, resolvedPath)
if err != nil {
return ""
}
isInsideCwd := relativePath == "." || (relativePath != ".." && !strings.HasPrefix(relativePath, ".."+string(filepath.Separator)) && !filepath.IsAbs(relativePath))
if !isInsideCwd {
return ""
}
return relativePath
}
// FormatPathRelativeToCwdOrAbsolute returns a slash-normalized path relative to
// cwd when possible, otherwise the cleaned absolute path.
func FormatPathRelativeToCwdOrAbsolute(filePath, cwd string) string {
absolutePath, err := resolveAgainstCwd(filePath, cwd)
if err != nil {
return filepath.ToSlash(filePath)
}
if relativePath := GetCwdRelativePath(absolutePath, cwd); relativePath != "" {
return filepath.ToSlash(relativePath)
}
return filepath.ToSlash(absolutePath)
}
// MarkPathIgnoredByCloudSync best-effort marks a directory as ignored by
// cloud sync providers. Mirrors upstream markPathIgnoredByCloudSync.
func MarkPathIgnoredByCloudSync(path string) {
var commands [][]string
switch runtime.GOOS {
case "darwin":
commands = [][]string{
{"xattr", "-w", "com.dropbox.ignored", "1", path},
{"xattr", "-w", "com.apple.fileprovider.ignore#P", "1", path},
}
case "linux":
commands = [][]string{{"setfattr", "-n", "user.com.dropbox.ignored", "-v", "1", path}}
default:
return
}
for _, args := range commands {
cmd := linkerexec.Command(args[0], args[1:]...)
cmd.Stdout = io.Discard
cmd.Stderr = io.Discard
_ = cmd.Run()
}
}