pi-code 1.0.60 → 1.0.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extensions/claude-rules.ts +9 -8
- package/extensions/internal/path-rules.ts +2 -4
- package/extensions/internal/project-approval.ts +8 -3
- package/extensions/internal/project-root.ts +45 -10
- package/extensions/internal/settings-chain.ts +33 -6
- package/extensions/status-line.ts +4 -2
- package/package.json +1 -1
|
@@ -111,14 +111,15 @@ export function parseFrontmatter(content: string): Frontmatter {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
|
-
* Whether a file path matches at least one of a rule's `paths:` globs. `*` stays in
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* that directory's contents. `./` and
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* the rule set's
|
|
114
|
+
* Whether a file path matches at least one of a rule's `paths:` globs. `*` stays in a
|
|
115
|
+
* segment and `**` crosses directories. A slashless pattern (`*.ts`, `README.md`) is
|
|
116
|
+
* the project root, per Claude's table; a leading globstar segment is how a rule says
|
|
117
|
+
* any depth. A trailing slash (`docs/`) scopes to that directory's contents. `./` and
|
|
118
|
+
* a leading `/` are stripped so a project-root anchored glob resolves the same as a
|
|
119
|
+
* bare one. A bare directory name without a trailing slash or a globstar matches a
|
|
120
|
+
* file of that name, not the directory's contents; write `dir` followed by a globstar
|
|
121
|
+
* to scope to a directory. `relPath` is the touched file relative to the rule set's
|
|
122
|
+
* root.
|
|
122
123
|
*/
|
|
123
124
|
export function pathMatchesGlobs(relPath: string, globs: string[]): boolean {
|
|
124
125
|
return matchesCompiledGlobs(relPath, compileGlobs(globs))
|
|
@@ -188,7 +188,6 @@ function resolveRule(rule: string, anchors: PathAnchors): string {
|
|
|
188
188
|
* root-relative path. */
|
|
189
189
|
export interface CompiledGlob {
|
|
190
190
|
regex: RegExp
|
|
191
|
-
matchesBasename: boolean
|
|
192
191
|
}
|
|
193
192
|
|
|
194
193
|
let globsCompiled = 0
|
|
@@ -219,7 +218,7 @@ export function compileGlobs(globs: string[]): CompiledGlob[] {
|
|
|
219
218
|
// would compile to `^docs/$` and match nothing.
|
|
220
219
|
if (glob.endsWith('/')) glob += '**'
|
|
221
220
|
globsCompiled += 1
|
|
222
|
-
compiled.push({ regex: new RegExp(`^${globToRegExpSource(glob)}$`)
|
|
221
|
+
compiled.push({ regex: new RegExp(`^${globToRegExpSource(glob)}$`) })
|
|
223
222
|
}
|
|
224
223
|
return compiled
|
|
225
224
|
}
|
|
@@ -229,8 +228,7 @@ export function compileGlobs(globs: string[]): CompiledGlob[] {
|
|
|
229
228
|
export function matchesCompiledGlobs(relPath: string, globs: CompiledGlob[]): boolean {
|
|
230
229
|
globsEvaluated += 1
|
|
231
230
|
const posix = relPath.split(path.sep).join('/')
|
|
232
|
-
|
|
233
|
-
return globs.some((glob) => glob.regex.test(glob.matchesBasename ? base : posix))
|
|
231
|
+
return globs.some((glob) => glob.regex.test(posix))
|
|
234
232
|
}
|
|
235
233
|
|
|
236
234
|
/** Whether the accessed file matches at least one rule. No rules means no match:
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import * as fs from 'node:fs'
|
|
21
|
+
import * as os from 'node:os'
|
|
21
22
|
import * as path from 'node:path'
|
|
22
23
|
import { getAgentDir, hasTrustRequiringProjectResources, ProjectTrustStore } from '@earendil-works/pi-coding-agent'
|
|
23
24
|
|
|
@@ -46,11 +47,15 @@ const CLAUDE_SHAPED = [
|
|
|
46
47
|
* The walk matters: agent discovery already searches upward, so starting pi in a
|
|
47
48
|
* subdirectory of a repository whose `.claude/agents` sits at the root found those
|
|
48
49
|
* agents while a cwd-only check reported nothing to gate, and the short-circuit
|
|
49
|
-
* approved the project without ever asking. The bound is the repository root
|
|
50
|
-
* directory
|
|
51
|
-
|
|
50
|
+
* approved the project without ever asking. The bound is the repository root and the home
|
|
51
|
+
* directory, because `~/.claude` is the user's own configuration: a directory under
|
|
52
|
+
* home that is in no repository would otherwise walk up into it and report the user's
|
|
53
|
+
* own settings as a project waiting to be approved. */
|
|
54
|
+
export function hasClaudeShapedConfig(cwd: string, home: string = os.homedir()): boolean {
|
|
52
55
|
let currentDir = cwd
|
|
53
56
|
while (true) {
|
|
57
|
+
// The home check comes first: at home itself the .claude found is the user's own.
|
|
58
|
+
if (currentDir === home) return false
|
|
54
59
|
if (CLAUDE_SHAPED.some((entry) => fs.existsSync(path.join(currentDir, entry)))) return true
|
|
55
60
|
if (ROOT_MARKERS.some((marker) => fs.existsSync(path.join(currentDir, marker)))) return false
|
|
56
61
|
const parentDir = path.dirname(currentDir)
|
|
@@ -13,18 +13,21 @@
|
|
|
13
13
|
import * as fs from 'node:fs'
|
|
14
14
|
import * as path from 'node:path'
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/** The project root marker. `.git` is a file in worktrees and submodules, a directory
|
|
17
|
+
* in an ordinary clone.
|
|
18
|
+
*
|
|
19
|
+
* `package.json` used to count too, which made every package of a monorepo its own
|
|
20
|
+
* project: its own memory directory, its own settings.local.json, its own
|
|
21
|
+
* CLAUDE_PROJECT_DIR, its own trust decision. Claude's project is the repository, and
|
|
22
|
+
* a repository can add a package.json wherever it likes, so a marker it controls was
|
|
23
|
+
* also a marker it could move. */
|
|
24
|
+
export const ROOT_MARKERS = ['.git']
|
|
18
25
|
|
|
19
|
-
/** Project root at or above `from`, or undefined
|
|
26
|
+
/** Project root at or above `from`, or undefined outside a repository. */
|
|
20
27
|
export function repoRoot(from: string): string | undefined {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const parentDir = path.dirname(currentDir)
|
|
25
|
-
if (parentDir === currentDir) return undefined
|
|
26
|
-
currentDir = parentDir
|
|
27
|
-
}
|
|
28
|
+
const root = gitRoot(from)
|
|
29
|
+
if (root === undefined) return undefined
|
|
30
|
+
return mainCheckout(root)
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/** The git checkout at or above `from`, or undefined outside one.
|
|
@@ -45,6 +48,38 @@ export function gitRoot(from: string): string | undefined {
|
|
|
45
48
|
}
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
/** The main checkout for a git directory.
|
|
52
|
+
*
|
|
53
|
+
* A worktree carries a `.git` FILE holding `gitdir: <main>/.git/worktrees/<name>`, so
|
|
54
|
+
* resolving it gives the checkout the repository's state actually belongs to. Claude
|
|
55
|
+
* reads settings.local.json from "the file at the main checkout's root" and shares one
|
|
56
|
+
* auto memory directory across "all worktrees and subdirectories within the same repo",
|
|
57
|
+
* so a worktree is not its own project. An unreadable or unexpected `.git` file leaves
|
|
58
|
+
* the directory as its own root, which is the safe direction. */
|
|
59
|
+
function mainCheckout(root: string): string {
|
|
60
|
+
const dotGit = path.join(root, '.git')
|
|
61
|
+
let pointer: string
|
|
62
|
+
try {
|
|
63
|
+
if (!fs.statSync(dotGit).isFile()) return root
|
|
64
|
+
pointer = fs.readFileSync(dotGit, 'utf-8')
|
|
65
|
+
} catch {
|
|
66
|
+
return root
|
|
67
|
+
}
|
|
68
|
+
// Parsed rather than matched: the file is one `gitdir: <path>` line, and a regex
|
|
69
|
+
// over an arbitrary-length path is a backtracking cost for nothing.
|
|
70
|
+
const [firstLine = ''] = pointer.split('\n')
|
|
71
|
+
const prefix = 'gitdir:'
|
|
72
|
+
if (!firstLine.startsWith(prefix)) return root
|
|
73
|
+
const target = firstLine.slice(prefix.length).trim()
|
|
74
|
+
if (!target) return root
|
|
75
|
+
// <main>/.git/worktrees/<name> -> <main>
|
|
76
|
+
const worktreeDir = path.resolve(root, target)
|
|
77
|
+
const marker = `${path.sep}.git${path.sep}worktrees${path.sep}`
|
|
78
|
+
const cut = worktreeDir.lastIndexOf(marker)
|
|
79
|
+
if (cut === -1) return root
|
|
80
|
+
return worktreeDir.slice(0, cut)
|
|
81
|
+
}
|
|
82
|
+
|
|
48
83
|
function statOf(target: string): fs.Stats | null {
|
|
49
84
|
try {
|
|
50
85
|
return fs.statSync(target)
|
|
@@ -13,19 +13,46 @@ import { claudeConfigDir } from './config-dir.js'
|
|
|
13
13
|
import { repoRoot } from './project-root.js'
|
|
14
14
|
import { isRecord } from './values.js'
|
|
15
15
|
|
|
16
|
+
/** Whether every path given exists and belongs to the user running this process.
|
|
17
|
+
* A path that is absent is not someone else's, so it does not disqualify the root. */
|
|
18
|
+
function ownedByUser(paths: string[]): boolean {
|
|
19
|
+
const uid = process.getuid?.()
|
|
20
|
+
if (uid === undefined) return true
|
|
21
|
+
return paths.every((target) => {
|
|
22
|
+
try {
|
|
23
|
+
return fs.statSync(target).uid === uid
|
|
24
|
+
} catch {
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Where `settings.local.json` lives, per Claude's four exceptions: it sits at the
|
|
31
|
+
* repository root, except outside a repository, when that root is the home directory,
|
|
32
|
+
* on Windows, or when the root or its `.git` or `.claude` entry belongs to someone
|
|
33
|
+
* else. In each of those it stays beside `.claude/settings.json` in the working
|
|
34
|
+
* directory instead. In a worktree the root is the main checkout, which repoRoot
|
|
35
|
+
* resolves. */
|
|
36
|
+
function localSettingsDir(cwd: string, home: string, platform: NodeJS.Platform, owned: (paths: string[]) => boolean): string {
|
|
37
|
+
const root = repoRoot(cwd)
|
|
38
|
+
if (root === undefined || root === home) return cwd
|
|
39
|
+
if (platform === 'win32') return cwd
|
|
40
|
+
if (!owned([root, path.join(root, '.git'), path.join(root, '.claude')])) return cwd
|
|
41
|
+
return root
|
|
42
|
+
}
|
|
43
|
+
|
|
16
44
|
/** The user settings.json, then (only when `includeProject`) the project files by
|
|
17
45
|
* Claude's placement rules: the shared `.claude/settings.json` is read from the
|
|
18
46
|
* session's primary working directory (never an ancestor; "to use a file committed
|
|
19
47
|
* at the repository root, start Claude Code there"), while `settings.local.json`
|
|
20
|
-
* lives at the repository root,
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
export function claudeSettingsChain(cwd: string, home: string, includeProject: boolean): string[] {
|
|
48
|
+
* lives at the repository root, subject to the exceptions in localSettingsDir. A
|
|
49
|
+
* legacy local file at the primary directory is still read, with the root's values
|
|
50
|
+
* winning. Later files win. */
|
|
51
|
+
export function claudeSettingsChain(cwd: string, home: string, includeProject: boolean, platform: NodeJS.Platform = process.platform, owned: (paths: string[]) => boolean = ownedByUser): string[] {
|
|
24
52
|
const files = [path.join(claudeConfigDir(home), 'settings.json')]
|
|
25
53
|
if (!includeProject) return files
|
|
26
54
|
files.push(path.join(cwd, '.claude', 'settings.json'))
|
|
27
|
-
const
|
|
28
|
-
const localDir = root !== undefined && root !== home ? root : cwd
|
|
55
|
+
const localDir = localSettingsDir(cwd, home, platform, owned)
|
|
29
56
|
if (localDir !== cwd) files.push(path.join(cwd, '.claude', 'settings.local.json'))
|
|
30
57
|
files.push(path.join(localDir, '.claude', 'settings.local.json'))
|
|
31
58
|
return files
|
|
@@ -36,7 +36,7 @@ import { claudeEffortLevel } from './internal/effort.js'
|
|
|
36
36
|
import { readManagedSettings } from './internal/managed-settings.js'
|
|
37
37
|
import { isPlanModeState, PLAN_MODE_CHANNEL } from './internal/plan-mode-state.js'
|
|
38
38
|
import { isProjectApprovedSilently } from './internal/project-approval.js'
|
|
39
|
-
import { repoRoot } from './internal/project-root.js'
|
|
39
|
+
import { gitRoot, repoRoot } from './internal/project-root.js'
|
|
40
40
|
import { readSettingsChain } from './internal/settings-chain.js'
|
|
41
41
|
import { watchSettingsFiles } from './internal/settings-watch.js'
|
|
42
42
|
import { readActiveStyleName, settingsFiles } from './output-styles.js'
|
|
@@ -47,7 +47,9 @@ const DEBOUNCE_MS = 300
|
|
|
47
47
|
/** Whether cwd sits in a git worktree: `.git` is a file pointing at the main
|
|
48
48
|
* checkout there, and a directory in an ordinary clone. */
|
|
49
49
|
function isGitWorktree(cwd: string): boolean {
|
|
50
|
-
|
|
50
|
+
// gitRoot, not repoRoot: repoRoot resolves a worktree to its main checkout, which is
|
|
51
|
+
// exactly the thing this needs to see before it is resolved away.
|
|
52
|
+
const root = gitRoot(cwd)
|
|
51
53
|
if (root === undefined) return false
|
|
52
54
|
try {
|
|
53
55
|
return fs.statSync(path.join(root, '.git')).isFile()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.61",
|
|
4
4
|
"description": "Claude Code experience for the pi coding agent: reads your .claude config (rules, commands, skills, hooks, output styles, MCP servers, agents) and adds todo, checkpoints, memory, web, subagents, and goals",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|