Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/issues/skills-path-cross-platform-repair/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Skills Path Cross-Platform Repair Plan

## Approach

Extend `SkillPresenter.resolveSkillsDir()` with a small default-path repair step. The repair only
matches paths that look like DeepChat's default skills location under an OS user home directory:

- `/Users/<name>/.deepchat/skills`
- `<drive>:\Users\<name>\.deepchat\skills`

If matched, return the same suffix under the current `app.getPath('home')` default skills root.
This keeps intentionally custom paths unchanged while covering stale OS/account defaults.

## Compatibility

The current malformed path repair for `C:\Users\name.deepchat\skills` is preserved. Existing valid
configured paths continue to resolve through `path.resolve()`.

## Test Strategy

Add constructor-level `getSkillsDir()` assertions in `skillPresenter.test.ts` for:

- POSIX stale default path repair.
- Windows stale default path repair.
- Existing malformed `.deepchat` repair remains unchanged.
32 changes: 32 additions & 0 deletions docs/issues/skills-path-cross-platform-repair/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Skills Path Cross-Platform Repair

## Problem

DeepChat can fail during startup when the persisted `skillsPath` points to a default skills
directory from another OS or user profile, such as `/Users/old-user/.deepchat/skills` on Windows.
Windows resolves that POSIX-looking path under the current drive, then startup attempts to create a
directory like `C:\Users\old-user\.deepchat\skills` and can fail with `EPERM`.

## User Story

As a user who moved configuration between machines or OS accounts, I want DeepChat to recover from
stale default skills paths so the app still opens and uses the current profile's skills directory.

## Acceptance Criteria

- Startup repairs stale default skills paths from POSIX `/Users/<name>/.deepchat/skills`.
- Startup repairs stale default skills paths from Windows `C:\Users\<name>\.deepchat\skills`.
- Repair keeps any path suffix below `skills`.
- Non-default custom skills paths remain unchanged.
- Existing malformed `.deepchat` path repair keeps working.

## Non-Goals

- Do not migrate arbitrary custom directories.
- Do not change skill discovery, installation, or sync behavior.
- Do not add a new settings migration framework.

## Constraints

- Keep the change localized to the existing `SkillPresenter` startup path handling.
- Add focused unit coverage for the repaired path patterns.
6 changes: 6 additions & 0 deletions docs/issues/skills-path-cross-platform-repair/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Skills Path Cross-Platform Repair Tasks

- [x] Document the issue and repair scope.
- [x] Extend `SkillPresenter.resolveSkillsDir()` repair logic.
- [x] Add focused unit tests for cross-platform stale default paths.
- [x] Run formatting and the targeted SkillPresenter test.
21 changes: 21 additions & 0 deletions src/main/presenter/skillPresenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,13 @@ export class SkillPresenter implements ISkillPresenter {
const homeDir = homePath ? path.resolve(homePath) : path.resolve('.')
const fallbackDir = path.join(homeDir, '.deepchat', 'skills')
const resolved = normalized ? path.resolve(normalized) : fallbackDir
const repairedDefaultPath = normalized
? this.repairPortableDefaultSkillsPath(normalized, homeDir)
: null

if (repairedDefaultPath) {
return repairedDefaultPath
}

// Repair malformed paths like: C:\Users\name.deepchat\skills
const brokenPrefix = `${homeDir}.deepchat`
Expand All @@ -246,6 +253,20 @@ export class SkillPresenter implements ISkillPresenter {
return resolved
}

private repairPortableDefaultSkillsPath(configuredPath: string, homeDir: string): string | null {
const slashPath = configuredPath.replace(/\\/g, '/')
const match =
slashPath.match(/^\/Users\/[^/]+\/\.deepchat\/skills(?:\/(.*))?$/i) ??
slashPath.match(/^[A-Za-z]:\/Users\/[^/]+\/\.deepchat\/skills(?:\/(.*))?$/i)

if (!match) {
return null
}

const suffixParts = (match[1] ?? '').split('/').filter(Boolean)
return path.join(homeDir, '.deepchat', 'skills', ...suffixParts)
}

/**
* Ensure the skills directory exists
*/
Expand Down
33 changes: 32 additions & 1 deletion test/main/presenter/skillPresenter/skillPresenter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,11 +284,12 @@ describe('SkillPresenter', () => {
expect(fs.existsSync).toHaveBeenCalled()
})

it('should use configured skills path when provided', () => {
it('should use configured skills path when provided', async () => {
;(mockConfigPresenter.getSkillsPath as Mock).mockReturnValue('/custom/skills/path')

const presenter = new SkillPresenter(mockConfigPresenter, skillSessionStatePort as any)
expect(mockConfigPresenter.getSkillsPath).toHaveBeenCalled()
await expect(presenter.getSkillsDir()).resolves.toBe('/custom/skills/path')
presenter.destroy()
})

Expand All @@ -312,6 +313,36 @@ describe('SkillPresenter', () => {
await expect(presenter.getSkillsDir()).resolves.toBe('/mock/home/.deepchat/skills')
presenter.destroy()
})

it('should repair stale POSIX default skills paths from another user profile', async () => {
;(mockConfigPresenter.getSkillsPath as Mock).mockReturnValue(
'/Users/legacy-user/.deepchat/skills'
)
;(app.getPath as Mock).mockImplementation((name: string) => {
if (name === 'home') return '/mock/home'
if (name === 'temp') return '/mock/temp'
return '/mock/' + name
})

const presenter = new SkillPresenter(mockConfigPresenter, skillSessionStatePort as any)
await expect(presenter.getSkillsDir()).resolves.toBe('/mock/home/.deepchat/skills')
presenter.destroy()
})

it('should repair stale Windows default skills paths from another user profile', async () => {
;(mockConfigPresenter.getSkillsPath as Mock).mockReturnValue(
'C:\\Users\\legacy-user\\.deepchat\\skills\\nested'
)
;(app.getPath as Mock).mockImplementation((name: string) => {
if (name === 'home') return '/mock/home'
if (name === 'temp') return '/mock/temp'
return '/mock/' + name
})

const presenter = new SkillPresenter(mockConfigPresenter, skillSessionStatePort as any)
await expect(presenter.getSkillsDir()).resolves.toBe('/mock/home/.deepchat/skills/nested')
presenter.destroy()
})
})

describe('getSkillsDir', () => {
Expand Down