dsh-cc-loader 0.1.0 → 0.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-cc-loader",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Shared parse layer for the dsh-cc ecosystem: parses Claude Code .claude/ (project + global) into a standalone .dsh intermediate representation (IR), classifying every component DIRECT/ADAPTED/UNSUPPORTED/BLOCKED and filtering unsupported ones out.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -12,7 +12,7 @@
12
12
  export { loadClaude, loadPermissions } from './load.js'
13
13
  export {
14
14
  discoverSkills, discoverCommands, collectClaudeDir, discoverRules,
15
- findProjectRoot, parseFrontmatter, isSkillName, pathExists, readTextSafe,
15
+ findProjectRoot, findClaudeProjectRoot, parseFrontmatter, isSkillName, pathExists, readTextSafe,
16
16
  } from './skills.js'
17
17
  export { discoverSettings, mergeSettings } from './settings.js'
18
18
  export {
package/src/load.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { homedir } from 'node:os'
4
4
  import { join } from 'node:path'
5
- import { findProjectRoot, collectClaudeDir, discoverRules } from './skills.js'
5
+ import { findProjectRoot, findClaudeProjectRoot, collectClaudeDir, discoverRules } from './skills.js'
6
6
  import { discoverSettings, mergeSettings } from './settings.js'
7
7
  import { parseRulesFor, removedToolNames, classifyComponents } from './classify.js'
8
8
  import { STATUS } from './classify.js'
@@ -30,12 +30,16 @@ import { discoverPluginRoot, discoverMarketplace } from './plugin.js'
30
30
  export async function loadClaude(opts = {}) {
31
31
  const cwd = opts.cwd ?? process.cwd()
32
32
  const homeDir = opts.homeDir ?? homedir()
33
- const markers = opts.projectRootMarkers ?? ['.git']
34
33
  const projectSkillRank = opts.projectSkillRank ?? 150
35
34
  const globalSkillRank = opts.globalSkillRank ?? 160
36
35
  const warnings = []
37
36
 
38
- const projectRoot = await findProjectRoot(cwd, markers)
37
+ // Project root: explicit projectRootMarkers wins (caller control, e.g.
38
+ // cc-mcp's configurable markers); otherwise CC semantics — closest
39
+ // .claude/ directory, falling back to the .git root, home excluded.
40
+ const projectRoot = opts.projectRootMarkers !== undefined
41
+ ? await findProjectRoot(cwd, opts.projectRootMarkers)
42
+ : await findClaudeProjectRoot(cwd, { homeDir })
39
43
  const skills = []
40
44
  const commands = []
41
45
  const rules = []
@@ -66,7 +70,10 @@ export async function loadClaude(opts = {}) {
66
70
  warnings.push(...catalog.warnings)
67
71
 
68
72
  // Permissions: discover the three settings files and merge.
69
- const discovered = await discoverSettings(cwd, { homeDir, projectRootMarkers: markers })
73
+ const discovered = await discoverSettings(cwd, {
74
+ homeDir,
75
+ ...(opts.projectRootMarkers !== undefined ? { projectRootMarkers: opts.projectRootMarkers } : {}),
76
+ })
70
77
  const merged = mergeSettings(discovered)
71
78
  const parsed = parseRulesFor(merged)
72
79
  for (const invalid of parsed.invalid) {
@@ -147,9 +154,11 @@ export async function loadClaude(opts = {}) {
147
154
  export async function loadPermissions(opts = {}) {
148
155
  const cwd = opts.cwd ?? process.cwd()
149
156
  const homeDir = opts.homeDir ?? homedir()
150
- const markers = opts.projectRootMarkers ?? ['.git']
151
157
  const warnings = []
152
- const discovered = await discoverSettings(cwd, { homeDir, projectRootMarkers: markers })
158
+ const discovered = await discoverSettings(cwd, {
159
+ homeDir,
160
+ ...(opts.projectRootMarkers !== undefined ? { projectRootMarkers: opts.projectRootMarkers } : {}),
161
+ })
153
162
  const merged = mergeSettings(discovered)
154
163
  const parsed = parseRulesFor(merged)
155
164
  for (const invalid of parsed.invalid) {
package/src/settings.js CHANGED
@@ -9,18 +9,22 @@
9
9
 
10
10
  import { join } from 'node:path'
11
11
  import { homedir } from 'node:os'
12
- import { findProjectRoot, pathExists, readTextSafe } from './skills.js'
12
+ import { findProjectRoot, findClaudeProjectRoot, pathExists, readTextSafe } from './skills.js'
13
13
 
14
14
  /**
15
15
  * Discover the three settings files for a cwd.
16
16
  * @param {string} cwd
17
17
  * @param {{ homeDir?: string, projectRootMarkers?: string[] }} [opts]
18
+ * projectRootMarkers: explicit markers use findProjectRoot; otherwise CC
19
+ * semantics (closest .claude/ directory, .git fallback, home excluded).
18
20
  * @returns {Promise<{ projectRoot?: string, user?: Source, project?: Source, local?: Source }>}
19
21
  * Source = { scope, path, data } or undefined when absent/unreadable.
20
22
  */
21
23
  export async function discoverSettings(cwd, opts = {}) {
22
24
  const homeDir = opts.homeDir ?? homedir()
23
- const projectRoot = await findProjectRoot(cwd, opts.projectRootMarkers)
25
+ const projectRoot = opts.projectRootMarkers !== undefined
26
+ ? await findProjectRoot(cwd, opts.projectRootMarkers)
27
+ : await findClaudeProjectRoot(cwd, { homeDir })
24
28
  const userPath = join(homeDir, '.claude', 'settings.json')
25
29
  const out = {}
26
30
  if (projectRoot !== undefined) {
package/src/skills.js CHANGED
@@ -24,6 +24,52 @@ export async function findProjectRoot(cwd, markers = ['.git']) {
24
24
  }
25
25
  }
26
26
 
27
+ /**
28
+ * Claude Code project-root discovery: the closest directory containing a
29
+ * `.claude/` directory IS the project configuration root — Claude Code walks
30
+ * up from the cwd looking for a `.claude/` directory (confirmed by
31
+ * anthropics/claude-code#80791: "Claude Code discovers project-level settings
32
+ * by walking up from the current working directory looking for a .claude/
33
+ * directory"), NOT the git root.
34
+ *
35
+ * Fallback: when no `.claude/` exists on the walk, fall back to the closest
36
+ * `.git` root (compat: repositories without a `.claude/` still resolve).
37
+ *
38
+ * Home-directory guard: `~/.claude` (Claude Code's global config dir) and a
39
+ * git-backed home are NEVER treated as a project root — the walk stops at
40
+ * home. Without the guard, every session outside a project would resolve its
41
+ * root to the user's home directory.
42
+ *
43
+ * @param {string} cwd - session working directory.
44
+ * @param {object} [opts] - { homeDir } excluded from root resolution;
45
+ * { stopAt } optional upper walk boundary (directory at or above which the
46
+ * walk stops — primarily a test seam so walk-top markers on the real
47
+ * machine cannot interfere; callers normally leave it unset).
48
+ * @returns {Promise<string | undefined>} project root directory.
49
+ */
50
+ export async function findClaudeProjectRoot(cwd, opts = {}) {
51
+ const homeDir = opts.homeDir
52
+ const stopAt = opts.stopAt
53
+ const isHome = (dir) => homeDir !== undefined && dir === homeDir
54
+ const atStop = (dir) => stopAt !== undefined && dir === stopAt
55
+ // 1. Closest .claude/ directory (CC project-scope semantics), home excluded.
56
+ let current = resolve(cwd)
57
+ while (true) {
58
+ if (!isHome(current) && !atStop(current) && await pathExists(join(current, '.claude'))) return current
59
+ const parent = dirname(current)
60
+ if (parent === current || atStop(current)) break
61
+ current = parent
62
+ }
63
+ // 2. Fallback: closest .git root, home excluded.
64
+ current = resolve(cwd)
65
+ while (true) {
66
+ if (!isHome(current) && !atStop(current) && await pathExists(join(current, '.git'))) return current
67
+ const parent = dirname(current)
68
+ if (parent === current || atStop(current)) return undefined
69
+ current = parent
70
+ }
71
+ }
72
+
27
73
  export async function pathExists(path) {
28
74
  try { await stat(path); return true } catch { return false }
29
75
  }